From 6a1e0c591a1cd3a3b97de9fc00a09cf06827113e Mon Sep 17 00:00:00 2001 From: Shana Moore Date: Mon, 11 Sep 2023 15:49:05 -0700 Subject: [PATCH 01/29] =?UTF-8?q?=F0=9F=8E=81=20Contributions=20from=20PAL?= =?UTF-8?q?NI/PALCI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Which were resolved via the following PRs: - https://github.com/scientist-softserv/palni-palci/pull/768 - https://github.com/scientist-softserv/palni-palci/pull/786 - https://github.com/scientist-softserv/palni-palci/pull/790 - https://github.com/scientist-softserv/palni-palci/pull/807 - https://github.com/scientist-softserv/palni-palci/pull/850 - https://github.com/scientist-softserv/palni-palci/pull/781 - https://github.com/scientist-softserv/palni-palci/pull/787 - https://github.com/scientist-softserv/palni-palci/pull/795 - https://github.com/scientist-softserv/palni-palci/pull/788 - https://github.com/scientist-softserv/palni-palci/pull/793 - https://github.com/scientist-softserv/palni-palci/pull/782 - https://github.com/scientist-softserv/palni-palci/pull/798 - https://github.com/scientist-softserv/palni-palci/pull/866 - https://github.com/scientist-softserv/palni-palci/pull/846 - https://github.com/scientist-softserv/palni-palci/pull/849 - https://github.com/scientist-softserv/palni-palci/pull/868 - https://github.com/scientist-softserv/palni-palci/pull/875 - https://github.com/scientist-softserv/palni-palci/pull/884 - https://github.com/scientist-softserv/palni-palci/pull/876 - https://github.com/scientist-softserv/palni-palci/pull/952 Contribute back the following features: - https://github.com/scientist-softserv/palni-palci/issues/125 - https://github.com/scientist-softserv/palni-palci/issues/740 - https://github.com/scientist-softserv/palni-palci/issues/742 - https://github.com/scientist-softserv/palni-palci/issues/746 - https://github.com/scientist-softserv/palni-palci/issues/772 - https://github.com/scientist-softserv/palni-palci/issues/773 - https://github.com/scientist-softserv/palni-palci/issues/774 - https://github.com/scientist-softserv/palni-palci/issues/776 - https://github.com/scientist-softserv/palni-palci/issues/778 - https://github.com/scientist-softserv/palni-palci/issues/839 - https://github.com/scientist-softserv/palni-palci/issues/840 - https://github.com/scientist-softserv/palni-palci/issues/864 Co-authored-by: Shana Moore Co-authored-by: Kirk Wang --- Gemfile | 1 + Gemfile.lock | 2 + app/assets/javascripts/application.js | 2 + app/assets/javascripts/cropper.min.js | 10 + app/assets/javascripts/fabric.min.js | 1 + app/assets/stylesheets/application.css | 1 + app/assets/stylesheets/cropper.min.css | 9 + app/assets/stylesheets/hyku.scss | 132 ++++++ app/assets/stylesheets/hyrax.scss | 6 + app/controllers/catalog_controller.rb | 7 +- .../strategies_controller_decorator.rb | 16 + .../hyrax/admin/appearances_controller.rb | 5 +- app/controllers/hyrax/homepage_controller.rb | 33 +- app/forms/hyrax/forms/admin/appearance.rb | 12 + app/forms/hyrax/generic_work_form.rb | 4 +- app/forms/hyrax/image_form.rb | 4 +- app/forms/hyrax/pdf_form_behavior.rb | 18 + app/helpers/application_helper.rb | 21 +- .../blacklight/advanced_search_helper.rb | 50 ++ app/helpers/features_helper.rb | 21 + app/helpers/hyku_helper.rb | 6 + app/helpers/iiif_print_helper.rb | 5 + app/helpers/pdf_js_helper.rb | 32 ++ app/helpers/shared_search_helper.rb | 5 +- app/indexers/app_indexer.rb | 21 +- app/indexers/generic_work_indexer.rb | 6 +- .../hyrax/file_set_indexer_decorator.rb | 39 ++ app/jobs/reindex_works_job.rb | 12 +- app/models/concerns/pdf_behavior.rb | 40 ++ app/models/generic_work.rb | 5 +- app/models/image.rb | 5 +- app/models/solr_document.rb | 20 + app/presenters/hyku/work_show_presenter.rb | 70 ++- .../hyrax/generic_work_presenter.rb | 1 + app/services/iiif_print/tenant_config.rb | 202 ++++++++ .../advanced/_advanced_search_fields.html.erb | 35 ++ .../_advanced_search_fields_qa.html.erb | 4 + .../appearances/_banner_image_form.html.erb | 78 ++- app/views/hyrax/admin/features/index.html.erb | 76 +++ .../hyrax/base/_analytics_button.html.erb | 5 + app/views/hyrax/base/_download_pdf.html.erb | 14 + app/views/hyrax/base/_form_files.html.erb | 69 +++ app/views/hyrax/base/_pdf_js.erb | 7 + app/views/hyrax/base/_relationships.html.erb | 39 ++ .../_relationships_parent_works_rows.html.erb | 12 + .../hyrax/base/_representative_media.html.erb | 12 + app/views/hyrax/base/_show_actions.html.erb | 5 - .../hyrax/base/_show_pdf_download_button.erb | 4 + .../hyrax/base/_show_pdf_viewer.html.erb | 4 + app/views/hyrax/base/show.html.erb | 9 +- app/views/hyrax/file_sets/_actions.html.erb | 58 +++ .../file_sets/media_display/_pdf.html.erb | 17 + .../records/edit_fields/_default.html.erb | 19 + app/views/shared/_appearance_styles.html.erb | 6 + .../layouts/homepage.html.erb | 3 +- .../hyrax/base/_relationships.html.erb | 12 + .../cultural_show/hyrax/base/show.html.erb | 30 +- .../image_show/hyrax/base/show.html.erb | 65 +++ .../homepage/_resource_type_stats.html.erb | 12 +- .../hyrax/homepage/all_collections.html.erb | 4 +- .../layouts/homepage.html.erb | 2 +- .../scholarly_show/hyrax/base/show.html.erb | 22 +- config/application.rb | 17 + config/features.rb | 10 + config/locales/de.yml | 2 + config/locales/devise_invitable.es.yml | 2 +- config/locales/en.yml | 6 +- config/locales/es.yml | 2 + config/locales/fr.yml | 2 + config/locales/hyrax.de.yml | 4 +- config/locales/hyrax.en.yml | 4 +- config/locales/hyrax.es.yml | 4 +- config/locales/hyrax.fr.yml | 4 +- config/locales/hyrax.it.yml | 4 +- config/locales/hyrax.pt-BR.yml | 4 +- config/locales/hyrax.zh.yml | 4 +- config/locales/it.yml | 2 + config/locales/pt-BR.yml | 2 + config/locales/simple_form.de.yml | 74 ++- config/locales/simple_form.es.yml | 39 +- config/locales/simple_form.fr.yml | 48 +- config/locales/simple_form.it.yml | 74 +-- config/locales/simple_form.pt-BR.yml | 46 +- config/locales/simple_form.zh.yml | 50 +- config/locales/zh.yml | 2 + public/pdf.js/viewer.html | 443 ++++++++++++++++++ spec/features/create_work_spec.rb | 20 +- spec/features/feature_flag_spec.rb | 20 +- spec/fixtures/pdf/archive.pdf | Bin 0 -> 324315 bytes spec/helpers/application_helper_spec.rb | 13 + .../blacklight/advanced_search_helper_spec.rb | 78 +++ spec/helpers/hyku_helper_spec.rb | 11 + spec/helpers/shared_search_helper_spec.rb | 40 +- .../hyrax/file_set_indexer_decorator_spec.rb | 25 + spec/models/solr_document_spec.rb | 44 ++ .../hyku/work_show_presenter_spec.rb | 74 ++- .../services/iiif_print/tenant_config_spec.rb | 208 ++++++++ .../base/_relationships.html.erb_spec.rb | 43 ++ 98 files changed, 2635 insertions(+), 237 deletions(-) create mode 100644 app/assets/javascripts/cropper.min.js create mode 100644 app/assets/javascripts/fabric.min.js create mode 100644 app/assets/stylesheets/cropper.min.css create mode 100644 app/controllers/flipflop/strategies_controller_decorator.rb create mode 100644 app/forms/hyrax/pdf_form_behavior.rb create mode 100644 app/helpers/blacklight/advanced_search_helper.rb create mode 100644 app/helpers/features_helper.rb create mode 100644 app/helpers/iiif_print_helper.rb create mode 100644 app/helpers/pdf_js_helper.rb create mode 100644 app/indexers/hyrax/file_set_indexer_decorator.rb create mode 100644 app/models/concerns/pdf_behavior.rb create mode 100644 app/services/iiif_print/tenant_config.rb create mode 100644 app/views/advanced/_advanced_search_fields.html.erb create mode 100644 app/views/advanced/_advanced_search_fields_qa.html.erb create mode 100644 app/views/hyrax/admin/features/index.html.erb create mode 100644 app/views/hyrax/base/_analytics_button.html.erb create mode 100644 app/views/hyrax/base/_download_pdf.html.erb create mode 100644 app/views/hyrax/base/_form_files.html.erb create mode 100644 app/views/hyrax/base/_pdf_js.erb create mode 100644 app/views/hyrax/base/_relationships.html.erb create mode 100644 app/views/hyrax/base/_relationships_parent_works_rows.html.erb create mode 100644 app/views/hyrax/base/_representative_media.html.erb create mode 100644 app/views/hyrax/base/_show_pdf_download_button.erb create mode 100644 app/views/hyrax/base/_show_pdf_viewer.html.erb create mode 100644 app/views/hyrax/file_sets/_actions.html.erb create mode 100644 app/views/hyrax/file_sets/media_display/_pdf.html.erb create mode 100644 app/views/records/edit_fields/_default.html.erb create mode 100644 app/views/themes/image_show/hyrax/base/show.html.erb create mode 100644 public/pdf.js/viewer.html create mode 100644 spec/fixtures/pdf/archive.pdf create mode 100644 spec/helpers/application_helper_spec.rb create mode 100644 spec/helpers/blacklight/advanced_search_helper_spec.rb create mode 100644 spec/helpers/hyku_helper_spec.rb create mode 100644 spec/indexers/hyrax/file_set_indexer_decorator_spec.rb create mode 100644 spec/models/solr_document_spec.rb create mode 100644 spec/services/iiif_print/tenant_config_spec.rb create mode 100644 spec/views/hyrax/base/_relationships.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index 3fd4a64535..9be52ea913 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'puma', '~> 5.6' # Use Puma as the app server gem 'rack-test', '0.7.0', group: %i[test] # rack-test >= 0.71 does not work with older Capybara versions (< 2.17). See #214 for more details gem 'rails-controller-testing', group: %i[test] gem 'rdf', '~> 3.1.15' # rdf 3.2.0 removed SerializedTransaction which ldp requires +gem 'redcarpet' # for Markdown constant gem 'redlock', '>= 0.1.2', '< 2.0' # lock redlock per https://github.com/samvera/hyrax/pull/5961 gem 'riiif', '~> 1.1' gem 'rolify' diff --git a/Gemfile.lock b/Gemfile.lock index 651139ee28..b14f534bc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1019,6 +1019,7 @@ GEM rdf-xsd (3.1.1) rdf (~> 3.1) rexml (~> 3.2) + redcarpet (3.6.0) redic (1.5.3) hiredis redis (4.8.1) @@ -1351,6 +1352,7 @@ DEPENDENCIES rails (~> 5.2.5) rails-controller-testing rdf (~> 3.1.15) + redcarpet redlock (>= 0.1.2, < 2.0) riiif (~> 1.1) rolify diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index df9e83c18a..c7c8860d1b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,6 +16,8 @@ //= require jquery3 //= require jquery_ujs //= require jquery.fontselect +//= require cropper.min + //= require dataTables/jquery.dataTables //= require dataTables/bootstrap/3/jquery.dataTables.bootstrap //= require stat_slider diff --git a/app/assets/javascripts/cropper.min.js b/app/assets/javascripts/cropper.min.js new file mode 100644 index 0000000000..959d89fe8e --- /dev/null +++ b/app/assets/javascripts/cropper.min.js @@ -0,0 +1,10 @@ +/*! + * Cropper.js v1.6.1 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2023-09-17T03:44:19.860Z + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Cropper=e()}(this,function(){"use strict";function C(e,t){var i,a=Object.keys(e);return Object.getOwnPropertySymbols&&(i=Object.getOwnPropertySymbols(e),t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),a.push.apply(a,i)),a}function S(a){for(var t=1;tt.length)&&(e=t.length);for(var i=0,a=new Array(e);it.width?3===i?o=t.height*e:h=t.width/e:3===i?h=t.width/e:o=t.height*e,{aspectRatio:e,naturalWidth:n,naturalHeight:a,width:o,height:h});this.canvasData=e,this.limited=1===i||2===i,this.limitCanvas(!0,!0),e.width=Math.min(Math.max(e.width,e.minWidth),e.maxWidth),e.height=Math.min(Math.max(e.height,e.minHeight),e.maxHeight),e.left=(t.width-e.width)/2,e.top=(t.height-e.height)/2,e.oldLeft=e.left,e.oldTop=e.top,this.initialCanvasData=g({},e)},limitCanvas:function(t,e){var i=this.options,a=this.containerData,n=this.canvasData,o=this.cropBoxData,h=i.viewMode,r=n.aspectRatio,s=this.cropped&&o;t&&(t=Number(i.minCanvasWidth)||0,i=Number(i.minCanvasHeight)||0,1=a.width&&(n.minLeft=Math.min(0,r),n.maxLeft=Math.max(0,r)),n.height>=a.height)&&(n.minTop=Math.min(0,t),n.maxTop=Math.max(0,t))):(n.minLeft=-n.width,n.minTop=-n.height,n.maxLeft=a.width,n.maxTop=a.height))},renderCanvas:function(t,e){var i,a,n,o,h=this.canvasData,r=this.imageData;e&&(e={width:r.naturalWidth*Math.abs(r.scaleX||1),height:r.naturalHeight*Math.abs(r.scaleY||1),degree:r.rotate||0},r=e.width,o=e.height,e=e.degree,i=90==(e=Math.abs(e)%180)?{width:o,height:r}:(a=e%90*Math.PI/180,i=Math.sin(a),n=r*(a=Math.cos(a))+o*i,r=r*i+o*a,90h.maxWidth||h.widthh.maxHeight||h.heighte.width?a.height=a.width/i:a.width=a.height*i),this.cropBoxData=a,this.limitCropBox(!0,!0),a.width=Math.min(Math.max(a.width,a.minWidth),a.maxWidth),a.height=Math.min(Math.max(a.height,a.minHeight),a.maxHeight),a.width=Math.max(a.minWidth,a.width*t),a.height=Math.max(a.minHeight,a.height*t),a.left=e.left+(e.width-a.width)/2,a.top=e.top+(e.height-a.height)/2,a.oldLeft=a.left,a.oldTop=a.top,this.initialCropBoxData=g({},a)},limitCropBox:function(t,e){var i,a,n=this.options,o=this.containerData,h=this.canvasData,r=this.cropBoxData,s=this.limited,c=n.aspectRatio;t&&(t=Number(n.minCropBoxWidth)||0,n=Number(n.minCropBoxHeight)||0,i=s?Math.min(o.width,h.width,h.width+h.left,o.width-h.left):o.width,a=s?Math.min(o.height,h.height,h.height+h.top,o.height-h.top):o.height,t=Math.min(t,o.width),n=Math.min(n,o.height),c&&(t&&n?ti.maxWidth||i.widthi.maxHeight||i.height=e.width&&i.height>=e.height?q:I),f(this.cropBox,g({width:i.width,height:i.height},x({translateX:i.left,translateY:i.top}))),this.cropped&&this.limited&&this.limitCanvas(!0,!0),this.disabled||this.output()},output:function(){this.preview(),y(this.element,tt,this.getData())}},i={initPreview:function(){var t=this.element,i=this.crossOrigin,e=this.options.preview,a=i?this.crossOriginUrl:this.url,n=t.alt||"The image to preview",o=document.createElement("img");i&&(o.crossOrigin=i),o.src=a,o.alt=n,this.viewBox.appendChild(o),this.viewBoxImage=o,e&&("string"==typeof(o=e)?o=t.ownerDocument.querySelectorAll(e):e.querySelector&&(o=[e]),z(this.previews=o,function(t){var e=document.createElement("img");w(t,m,{width:t.offsetWidth,height:t.offsetHeight,html:t.innerHTML}),i&&(e.crossOrigin=i),e.src=a,e.alt=n,e.style.cssText='display:block;width:100%;height:auto;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation:0deg!important;"',t.innerHTML="",t.appendChild(e)}))},resetPreview:function(){z(this.previews,function(e){var i=Bt(e,m),i=(f(e,{width:i.width,height:i.height}),e.innerHTML=i.html,e),e=m;if(o(i[e]))try{delete i[e]}catch(t){i[e]=void 0}else if(i.dataset)try{delete i.dataset[e]}catch(t){i.dataset[e]=void 0}else i.removeAttribute("data-".concat(Dt(e)))})},preview:function(){var h=this.imageData,t=this.canvasData,e=this.cropBoxData,r=e.width,s=e.height,c=h.width,d=h.height,l=e.left-t.left-h.left,p=e.top-t.top-h.top;this.cropped&&!this.disabled&&(f(this.viewBoxImage,g({width:c,height:d},x(g({translateX:-l,translateY:-p},h)))),z(this.previews,function(t){var e=Bt(t,m),i=e.width,e=e.height,a=i,n=e,o=1;r&&(n=s*(o=i/r)),s&&eMath.abs(a-1)?i:a)&&(t.restore&&(o=this.getCanvasData(),h=this.getCropBoxData()),this.render(),t.restore)&&(this.setCanvasData(z(o,function(t,e){o[e]=t*n})),this.setCropBoxData(z(h,function(t,e){h[e]=t*n}))))},dblclick:function(){var t,e;this.disabled||this.options.dragMode===_||this.setDragMode((t=this.dragBox,e=Q,(t.classList?t.classList.contains(e):-1y&&(D.x=y-f);break;case k:p+D.xx&&(D.y=x-v)}}var i,a,o,n=this.options,h=this.canvasData,r=this.containerData,s=this.cropBoxData,c=this.pointers,d=this.action,l=n.aspectRatio,p=s.left,m=s.top,u=s.width,g=s.height,f=p+u,v=m+g,w=0,b=0,y=r.width,x=r.height,M=!0,C=(!l&&t.shiftKey&&(l=u&&g?u/g:1),this.limited&&(w=s.minLeft,b=s.minTop,y=w+Math.min(r.width,h.width,h.left+h.width),x=b+Math.min(r.height,h.height,h.top+h.height)),c[Object.keys(c)[0]]),D={x:C.endX-C.startX,y:C.endY-C.startY};switch(d){case I:p+=D.x,m+=D.y;break;case B:0<=D.x&&(y<=f||l&&(m<=b||x<=v))?M=!1:(e(B),(u+=D.x)<0&&(d=k,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case T:D.y<=0&&(m<=b||l&&(p<=w||y<=f))?M=!1:(e(T),g-=D.y,m+=D.y,g<0&&(d=O,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case k:D.x<=0&&(p<=w||l&&(m<=b||x<=v))?M=!1:(e(k),u-=D.x,p+=D.x,u<0&&(d=B,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case O:0<=D.y&&(x<=v||l&&(p<=w||y<=f))?M=!1:(e(O),(g+=D.y)<0&&(d=T,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case E:if(l){if(D.y<=0&&(m<=b||y<=f)){M=!1;break}e(T),g-=D.y,m+=D.y,u=g*l}else e(T),e(B),!(0<=D.x)||fMath.abs(o)&&(o=i)})}),o),t),M=!1;break;case U:D.x&&D.y?(i=Wt(this.cropper),p=C.startX-i.left,m=C.startY-i.top,u=s.minWidth,g=s.minHeight,0 or element.");this.element=t,this.options=g({},ut,u(e)&&e),this.cropped=!1,this.disabled=!1,this.pointers={},this.ready=!1,this.reloading=!1,this.replaced=!1,this.sized=!1,this.sizing=!1,this.init()}var t,e,i;return t=n,i=[{key:"noConflict",value:function(){return window.Cropper=Pt,n}},{key:"setDefaults",value:function(t){g(ut,u(t)&&t)}}],(e=[{key:"init",value:function(){var t,e=this.element,i=e.tagName.toLowerCase();if(!e[c]){if(e[c]=this,"img"===i){if(this.isImg=!0,t=e.getAttribute("src")||"",!(this.originalUrl=t))return;t=e.src}else"canvas"===i&&window.HTMLCanvasElement&&(t=e.toDataURL());this.load(t)}}},{key:"load",value:function(t){var e,i,a,n,o,h,r=this;t&&(this.url=t,this.imageData={},e=this.element,(i=this.options).rotatable||i.scalable||(i.checkOrientation=!1),i.checkOrientation&&window.ArrayBuffer?lt.test(t)?pt.test(t)?this.read((h=(h=t).replace(Xt,""),a=atob(h),h=new ArrayBuffer(a.length),z(n=new Uint8Array(h),function(t,e){n[e]=a.charCodeAt(e)}),h)):this.clone():(o=new XMLHttpRequest,h=this.clone.bind(this),this.reloading=!0,(this.xhr=o).onabort=h,o.onerror=h,o.ontimeout=h,o.onprogress=function(){o.getResponseHeader("content-type")!==ct&&o.abort()},o.onload=function(){r.read(o.response)},o.onloadend=function(){r.reloading=!1,r.xhr=null},i.checkCrossOrigin&&Lt(t)&&e.crossOrigin&&(t=zt(t)),o.open("GET",t,!0),o.responseType="arraybuffer",o.withCredentials="use-credentials"===e.crossOrigin,o.send()):this.clone())}},{key:"read",value:function(t){var e=this.options,i=this.imageData,a=Rt(t),n=0,o=1,h=1;1
',o=(n=n.querySelector(".".concat(c,"-container"))).querySelector(".".concat(c,"-canvas")),h=n.querySelector(".".concat(c,"-drag-box")),s=(r=n.querySelector(".".concat(c,"-crop-box"))).querySelector(".".concat(c,"-face")),this.container=a,this.cropper=n,this.canvas=o,this.dragBox=h,this.cropBox=r,this.viewBox=n.querySelector(".".concat(c,"-view-box")),this.face=s,o.appendChild(i),v(t,L),a.insertBefore(n,t.nextSibling),X(i,Z),this.initPreview(),this.bind(),e.initialAspectRatio=Math.max(0,e.initialAspectRatio)||NaN,e.aspectRatio=Math.max(0,e.aspectRatio)||NaN,e.viewMode=Math.max(0,Math.min(3,Math.round(e.viewMode)))||0,v(r,L),e.guides||v(r.getElementsByClassName("".concat(c,"-dashed")),L),e.center||v(r.getElementsByClassName("".concat(c,"-center")),L),e.background&&v(n,"".concat(c,"-bg")),e.highlight||v(s,G),e.cropBoxMovable&&(v(s,V),w(s,d,I)),e.cropBoxResizable||(v(r.getElementsByClassName("".concat(c,"-line")),L),v(r.getElementsByClassName("".concat(c,"-point")),L)),this.render(),this.ready=!0,this.setDragMode(e.dragMode),e.autoCrop&&this.crop(),this.setData(e.data),l(e.ready)&&b(t,"ready",e.ready,{once:!0}),y(t,"ready"))}},{key:"unbuild",value:function(){var t;this.ready&&(this.ready=!1,this.unbind(),this.resetPreview(),(t=this.cropper.parentNode)&&t.removeChild(this.cropper),X(this.element,L))}},{key:"uncreate",value:function(){this.ready?(this.unbuild(),this.ready=!1,this.cropped=!1):this.sizing?(this.sizingImage.onload=null,this.sizing=!1,this.sized=!1):this.reloading?(this.xhr.onabort=null,this.xhr.abort()):this.image&&this.stop()}}])&&j(t.prototype,e),i&&j(t,i),Object.defineProperty(t,"prototype",{writable:!1}),n}();return g(It.prototype,t,i,e,St,jt,At),It}); \ No newline at end of file diff --git a/app/assets/javascripts/fabric.min.js b/app/assets/javascripts/fabric.min.js new file mode 100644 index 0000000000..9455a6f0e8 --- /dev/null +++ b/app/assets/javascripts/fabric.min.js @@ -0,0 +1 @@ +var fabric=fabric||{version:"5.2.1"};if("undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window)fabric.document=document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?document:document.implementation.createHTMLDocument(""),fabric.window=window;else{var jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser}fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&fabric.window.navigator.maxTouchPoints>0,fabric.isLikelyNode="undefined"!=typeof Buffer&&"undefined"==typeof window,fabric.SHARED_ATTRIBUTES=["display","transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-dashoffset","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","id","paint-order","vector-effect","instantiated_by_use","clip-path"],fabric.DPI=96,fabric.reNum="(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)",fabric.commaWsp="(?:\\s+,?\\s*|,\\s*)",fabric.rePathCommand=/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/gi,fabric.reNonWord=/[ \n\.,;!\?\-]/,fabric.fontPaths={},fabric.iMatrix=[1,0,0,1,0,0],fabric.svgNS="http://www.w3.org/2000/svg",fabric.perfLimitSizeTotal=2097152,fabric.maxCacheSideLimit=4096,fabric.minCacheSideLimit=256,fabric.charWidthsCache={},fabric.textureSize=2048,fabric.disableStyleCopyPaste=!1,fabric.enableGLFiltering=!0,fabric.devicePixelRatio=fabric.window.devicePixelRatio||fabric.window.webkitDevicePixelRatio||fabric.window.mozDevicePixelRatio||1,fabric.browserShadowBlurConstant=1,fabric.arcToSegmentsCache={},fabric.boundsOfCurveCache={},fabric.cachesBoundsOfCurve=!0,fabric.forceGLPutImageData=!1,fabric.initFilterBackend=function(){return fabric.enableGLFiltering&&fabric.isWebglSupported&&fabric.isWebglSupported(fabric.textureSize)?(console.log("max texture size: "+fabric.maxTextureSize),new fabric.WebglFilterBackend({tileSize:fabric.textureSize})):fabric.Canvas2dFilterBackend?new fabric.Canvas2dFilterBackend:void 0};"undefined"!=typeof document&&"undefined"!=typeof window&&(window.fabric=fabric);!function(){function t(t,e){if(this.__eventListeners[t]){var r=this.__eventListeners[t];e?r[r.indexOf(e)]=!1:fabric.util.array.fill(r,!1)}}function e(t,e){if(this.__eventListeners||(this.__eventListeners={}),1===arguments.length)for(var r in t)this.on(r,t[r]);else this.__eventListeners[t]||(this.__eventListeners[t]=[]),this.__eventListeners[t].push(e);return this}function r(t,e){var r=function(){e.apply(this,arguments),this.off(t,r)}.bind(this);this.on(t,r)}function n(t,e){if(1===arguments.length)for(var n in t)r.call(this,n,t[n]);else r.call(this,t,e);return this}function i(e,r){if(!this.__eventListeners)return this;if(0===arguments.length)for(e in this.__eventListeners)t.call(this,e);else if(1===arguments.length&&"object"==typeof arguments[0])for(var n in e)t.call(this,n,e[n]);else t.call(this,e,r);return this}function a(t,e){if(!this.__eventListeners)return this;var r=this.__eventListeners[t];if(!r)return this;for(var n=0,i=r.length;i>n;n++)r[n]&&r[n].call(this,e||{});return this.__eventListeners[t]=r.filter(function(t){return t!==!1}),this}fabric.Observable={fire:a,on:e,once:n,off:i}}();fabric.Collection={_objects:[],add:function(){if(this._objects.push.apply(this._objects,arguments),this._onObjectAdded)for(var e=0,t=arguments.length;t>e;e++)this._onObjectAdded(arguments[e]);return this.renderOnAddRemove&&this.requestRenderAll(),this},insertAt:function(e,t,r){var i=this._objects;return r?i[t]=e:i.splice(t,0,e),this._onObjectAdded&&this._onObjectAdded(e),this.renderOnAddRemove&&this.requestRenderAll(),this},remove:function(){for(var e,t=this._objects,r=!1,i=0,n=arguments.length;n>i;i++)e=t.indexOf(arguments[i]),-1!==e&&(r=!0,t.splice(e,1),this._onObjectRemoved&&this._onObjectRemoved(arguments[i]));return this.renderOnAddRemove&&r&&this.requestRenderAll(),this},forEachObject:function(e,t){for(var r=this.getObjects(),i=0,n=r.length;n>i;i++)e.call(t,r[i],i,r);return this},getObjects:function(e){return"undefined"==typeof e?this._objects.concat():this._objects.filter(function(t){return t.type===e})},item:function(e){return this._objects[e]},isEmpty:function(){return 0===this._objects.length},size:function(){return this._objects.length},contains:function(e,t){return this._objects.indexOf(e)>-1?!0:t?this._objects.some(function(t){return"function"==typeof t.contains&&t.contains(e,!0)}):!1},complexity:function(){return this._objects.reduce(function(e,t){return e+=t.complexity?t.complexity():0},0)}};fabric.CommonMethods={_setOptions:function(t){for(var e in t)this.set(e,t[e])},_initGradient:function(t,e){!t||!t.colorStops||t instanceof fabric.Gradient||this.set(e,new fabric.Gradient(t))},_initPattern:function(t,e,r){!t||!t.source||t instanceof fabric.Pattern?r&&r():this.set(e,new fabric.Pattern(t,r))},_setObject:function(t){for(var e in t)this._set(e,t[e])},set:function(t,e){return"object"==typeof t?this._setObject(t):this._set(t,e),this},_set:function(t,e){this[t]=e},toggle:function(t){var e=this.get(t);return"boolean"==typeof e&&this.set(t,!e),this},get:function(t){return this[t]}};!function(e){var t=Math.sqrt,r=Math.atan2,i=Math.pow,a=Math.PI/180,n=Math.PI/2;fabric.util={cos:function(e){if(0===e)return 1;0>e&&(e=-e);var t=e/n;switch(t){case 1:case 3:return 0;case 2:return-1}return Math.cos(e)},sin:function(e){if(0===e)return 0;var t=e/n,r=1;switch(0>e&&(r=-1),t){case 1:return r;case 2:return 0;case 3:return-r}return Math.sin(e)},removeFromArray:function(e,t){var r=e.indexOf(t);return-1!==r&&e.splice(r,1),e},getRandomInt:function(e,t){return Math.floor(Math.random()*(t-e+1))+e},degreesToRadians:function(e){return e*a},radiansToDegrees:function(e){return e/a},rotatePoint:function(e,t,r){var i=new fabric.Point(e.x-t.x,e.y-t.y),a=fabric.util.rotateVector(i,r);return new fabric.Point(a.x,a.y).addEquals(t)},rotateVector:function(e,t){var r=fabric.util.sin(t),i=fabric.util.cos(t),a=e.x*i-e.y*r,n=e.x*r+e.y*i;return{x:a,y:n}},createVector:function(e,t){return new fabric.Point(t.x-e.x,t.y-e.y)},calcAngleBetweenVectors:function(e,t){return Math.acos((e.x*t.x+e.y*t.y)/(Math.hypot(e.x,e.y)*Math.hypot(t.x,t.y)))},getHatVector:function(e){return new fabric.Point(e.x,e.y).multiply(1/Math.hypot(e.x,e.y))},getBisector:function(e,t,r){var i=fabric.util.createVector(e,t),a=fabric.util.createVector(e,r),n=fabric.util.calcAngleBetweenVectors(i,a),o=fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(i,n),a),c=n*(0===o?1:-1)/2;return{vector:fabric.util.getHatVector(fabric.util.rotateVector(i,c)),angle:n}},projectStrokeOnPoints:function(e,t,r){var i=[],a=t.strokeWidth/2,n=t.strokeUniform?new fabric.Point(1/t.scaleX,1/t.scaleY):new fabric.Point(1,1),o=function(e){var t=a/Math.hypot(e.x,e.y);return new fabric.Point(e.x*t*n.x,e.y*t*n.y)};return e.length<=1?i:(e.forEach(function(c,f){var l,s,u=new fabric.Point(c.x,c.y);0===f?(s=e[f+1],l=r?o(fabric.util.createVector(s,u)).addEquals(u):e[e.length-1]):f===e.length-1?(l=e[f-1],s=r?o(fabric.util.createVector(l,u)).addEquals(u):e[0]):(l=e[f-1],s=e[f+1]);var d,b,m=fabric.util.getBisector(u,l,s),h=m.vector,p=m.angle;return"miter"===t.strokeLineJoin&&(d=-a/Math.sin(p/2),b=new fabric.Point(h.x*d*n.x,h.y*d*n.y),Math.hypot(b.x,b.y)/a<=t.strokeMiterLimit)?(i.push(u.add(b)),void i.push(u.subtract(b))):(d=-a*Math.SQRT2,b=new fabric.Point(h.x*d*n.x,h.y*d*n.y),i.push(u.add(b)),void i.push(u.subtract(b)))}),i)},transformPoint:function(e,t,r){return r?new fabric.Point(t[0]*e.x+t[2]*e.y,t[1]*e.x+t[3]*e.y):new fabric.Point(t[0]*e.x+t[2]*e.y+t[4],t[1]*e.x+t[3]*e.y+t[5])},makeBoundingBoxFromPoints:function(e,t){if(t)for(var r=0;rr;++r)n=n[i[r]];return n},loadImage:function(e,t,r,i){if(!e)return void(t&&t.call(r,e));var a=fabric.util.createImage(),n=function(){t&&t.call(r,a,!1),a=a.onload=a.onerror=null};a.onload=n,a.onerror=function(){fabric.log("Error loading "+a.src),t&&t.call(r,null,!0),a=a.onload=a.onerror=null},0!==e.indexOf("data")&&void 0!==i&&null!==i&&(a.crossOrigin=i),"data:image/svg"===e.substring(0,14)&&(a.onload=null,fabric.util.loadImageInDom(a,n)),a.src=e},loadImageInDom:function(e,t){var r=fabric.document.createElement("div");r.style.width=r.style.height="1px",r.style.left=r.style.top="-100%",r.style.position="absolute",r.appendChild(e),fabric.document.querySelector("body").appendChild(r),e.onload=function(){t(),r.parentNode.removeChild(r),r=null}},enlivenObjects:function(e,t,r,i){function a(){++o===c&&t&&t(n.filter(function(e){return e}))}e=e||[];var n=[],o=0,c=e.length;return c?void e.forEach(function(e,t){if(!e||!e.type)return void a();var o=fabric.util.getKlass(e.type,r);o.fromObject(e,function(r,o){o||(n[t]=r),i&&i(e,r,o),a()})}):void(t&&t(n))},enlivenObjectEnlivables:function(e,t,r){var i=fabric.Object.ENLIVEN_PROPS.filter(function(t){return!!e[t]});fabric.util.enlivenObjects(i.map(function(t){return e[t]}),function(e){var a={};i.forEach(function(r,i){a[r]=e[i],t&&(t[r]=e[i])}),r&&r(a)})},enlivenPatterns:function(e,t){function r(){++a===n&&t&&t(i)}e=e||[];var i=[],a=0,n=e.length;return n?void e.forEach(function(e,t){e&&e.source?new fabric.Pattern(e,function(e){i[t]=e,r()}):(i[t]=e,r())}):void(t&&t(i))},groupSVGElements:function(e,t,r){var i;return e&&1===e.length?e[0]:(t&&(t.width&&t.height?t.centerPoint={x:t.width/2,y:t.height/2}:(delete t.width,delete t.height)),i=new fabric.Group(e,t),"undefined"!=typeof r&&(i.sourcePath=r),i)},populateWithProperties:function(e,t,r){if(r&&Array.isArray(r))for(var i=0,a=r.length;a>i;i++)r[i]in e&&(t[r[i]]=e[r[i]])},createCanvasElement:function(){return fabric.document.createElement("canvas")},copyCanvasElement:function(e){var t=fabric.util.createCanvasElement();return t.width=e.width,t.height=e.height,t.getContext("2d").drawImage(e,0,0),t},toDataURL:function(e,t,r){return e.toDataURL("image/"+t,r)},createImage:function(){return fabric.document.createElement("img")},multiplyTransformMatrices:function(e,t,r){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],r?0:e[0]*t[4]+e[2]*t[5]+e[4],r?0:e[1]*t[4]+e[3]*t[5]+e[5]]},qrDecompose:function(e){var n=r(e[1],e[0]),o=i(e[0],2)+i(e[1],2),c=t(o),f=(e[0]*e[3]-e[2]*e[1])/c,l=r(e[0]*e[2]+e[1]*e[3],o);return{angle:n/a,scaleX:c,scaleY:f,skewX:l/a,skewY:0,translateX:e[4],translateY:e[5]}},calcRotateMatrix:function(e){if(!e.angle)return fabric.iMatrix.concat();var t=fabric.util.degreesToRadians(e.angle),r=fabric.util.cos(t),i=fabric.util.sin(t);return[r,i,-i,r,0,0]},calcDimensionsMatrix:function(e){var t="undefined"==typeof e.scaleX?1:e.scaleX,r="undefined"==typeof e.scaleY?1:e.scaleY,i=[e.flipX?-t:t,0,0,e.flipY?-r:r,0,0],a=fabric.util.multiplyTransformMatrices,n=fabric.util.degreesToRadians;return e.skewX&&(i=a(i,[1,0,Math.tan(n(e.skewX)),1],!0)),e.skewY&&(i=a(i,[1,Math.tan(n(e.skewY)),0,1],!0)),i},composeMatrix:function(e){var t=[1,0,0,1,e.translateX||0,e.translateY||0],r=fabric.util.multiplyTransformMatrices;return e.angle&&(t=r(t,fabric.util.calcRotateMatrix(e))),(1!==e.scaleX||1!==e.scaleY||e.skewX||e.skewY||e.flipX||e.flipY)&&(t=r(t,fabric.util.calcDimensionsMatrix(e))),t},resetObjectTransform:function(e){e.scaleX=1,e.scaleY=1,e.skewX=0,e.skewY=0,e.flipX=!1,e.flipY=!1,e.rotate(0)},saveObjectTransform:function(e){return{scaleX:e.scaleX,scaleY:e.scaleY,skewX:e.skewX,skewY:e.skewY,angle:e.angle,left:e.left,flipX:e.flipX,flipY:e.flipY,top:e.top}},isTransparent:function(e,t,r,i){i>0&&(t>i?t-=i:t=0,r>i?r-=i:r=0);var a,n,o=!0,c=e.getImageData(t,r,2*i||1,2*i||1),f=c.data.length;for(a=3;f>a&&(n=c.data[a],o=0>=n,o!==!1);a+=4);return c=null,o},parsePreserveAspectRatioAttribute:function(e){var t,r="meet",i="Mid",a="Mid",n=e.split(" ");return n&&n.length&&(r=n.pop(),"meet"!==r&&"slice"!==r?(t=r,r="meet"):n.length&&(t=n.pop())),i="none"!==t?t.slice(1,4):"none",a="none"!==t?t.slice(5,8):"none",{meetOrSlice:r,alignX:i,alignY:a}},clearFabricFontCache:function(e){e=(e||"").toLowerCase(),e?fabric.charWidthsCache[e]&&delete fabric.charWidthsCache[e]:fabric.charWidthsCache={}},limitDimsByArea:function(e,t){var r=Math.sqrt(t*e),i=Math.floor(t/r);return{x:Math.floor(r),y:i}},capValue:function(e,t,r){return Math.max(e,Math.min(t,r))},findScaleToFit:function(e,t){return Math.min(t.width/e.width,t.height/e.height)},findScaleToCover:function(e,t){return Math.max(t.width/e.width,t.height/e.height)},matrixToSVG:function(e){return"matrix("+e.map(function(e){return fabric.util.toFixed(e,fabric.Object.NUM_FRACTION_DIGITS)}).join(" ")+")"},removeTransformFromObject:function(e,t){var r=fabric.util.invertTransform(t),i=fabric.util.multiplyTransformMatrices(r,e.calcOwnMatrix());fabric.util.applyTransformToObject(e,i)},addTransformToObject:function(e,t){fabric.util.applyTransformToObject(e,fabric.util.multiplyTransformMatrices(t,e.calcOwnMatrix()))},applyTransformToObject:function(e,t){var r=fabric.util.qrDecompose(t),i=new fabric.Point(r.translateX,r.translateY);e.flipX=!1,e.flipY=!1,e.set("scaleX",r.scaleX),e.set("scaleY",r.scaleY),e.skewX=r.skewX,e.skewY=r.skewY,e.angle=r.angle,e.setPositionByOrigin(i,"center","center")},sizeAfterTransform:function(e,t,r){var i=e/2,a=t/2,n=[{x:-i,y:-a},{x:i,y:-a},{x:-i,y:a},{x:i,y:a}],o=fabric.util.calcDimensionsMatrix(r),c=fabric.util.makeBoundingBoxFromPoints(n,o);return{x:c.width,y:c.height}},mergeClipPaths:function(e,t){var r=e,i=t;r.inverted&&!i.inverted&&(r=t,i=e),fabric.util.applyTransformToObject(i,fabric.util.multiplyTransformMatrices(fabric.util.invertTransform(r.calcTransformMatrix()),i.calcTransformMatrix()));var a=r.inverted&&i.inverted;return a&&(r.inverted=i.inverted=!1),new fabric.Group([r],{clipPath:i,inverted:a})},hasStyleChanged:function(e,t,r){return r=r||!1,e.fill!==t.fill||e.stroke!==t.stroke||e.strokeWidth!==t.strokeWidth||e.fontSize!==t.fontSize||e.fontFamily!==t.fontFamily||e.fontWeight!==t.fontWeight||e.fontStyle!==t.fontStyle||e.deltaY!==t.deltaY||r&&(e.overline!==t.overline||e.underline!==t.underline||e.linethrough!==t.linethrough)},stylesToArray:function(e,t){for(var e=fabric.util.object.clone(e,!0),r=t.split("\n"),i=-1,a={},n=[],o=0;ow){var P=Math.sqrt(1-w/(y*v));i*=P,a*=P}else M=(c===o?-1:1)*Math.sqrt(w/(y*g+v*x));var C=M*i*p/a,k=-M*a*m/i,T=d*C-l*k+.5*e,j=l*C+d*k+.5*n,O=r(1,0,(m-C)/i,(p-k)/a),S=r((m-C)/i,(p-k)/a,(-m-C)/i,(-p-k)/a);0===o&&S>0?S-=2*f:1===o&&0>S&&(S+=2*f);for(var E=Math.ceil(Math.abs(S/f*2)),F=[],Y=S/E,A=8/3*Math.sin(Y/4)*Math.sin(Y/4)/Math.sin(Y/2),D=O+Y,X=0;E>X;X++)F[X]=t(O,D,d,l,i,a,T,j,A,h,b),h=F[X][5],b=F[X][6],O=D,D+=Y;return F}function r(t,e,r,n){var i=Math.atan2(e,t),a=Math.atan2(n,r);return a>=i?a-i:2*Math.PI-(i-a)}function n(t,e,r,n,i,a,c,o){var s;if(fabric.cachesBoundsOfCurve&&(s=k.call(arguments),fabric.boundsOfCurveCache[s]))return fabric.boundsOfCurveCache[s];var f,u,l,d,h,b,m,p,y=Math.sqrt,v=Math.min,g=Math.max,x=Math.abs,w=[],M=[[],[]];u=6*t-12*r+6*i,f=-3*t+9*r-9*i+3*c,l=3*r-3*t;for(var P=0;2>P;++P)if(P>0&&(u=6*e-12*n+6*a,f=-3*e+9*n-9*a+3*o,l=3*n-3*e),x(f)<1e-12){if(x(u)<1e-12)continue;d=-l/u,d>0&&1>d&&w.push(d)}else m=u*u-4*l*f,0>m||(p=y(m),h=(-u+p)/(2*f),h>0&&1>h&&w.push(h),b=(-u-p)/(2*f),b>0&&1>b&&w.push(b));for(var C,T,j,O=w.length,S=O;O--;)d=w[O],j=1-d,C=j*j*j*t+3*j*j*d*r+3*j*d*d*i+d*d*d*c,M[0][O]=C,T=j*j*j*e+3*j*j*d*n+3*j*d*d*a+d*d*d*o,M[1][O]=T;M[0][S]=t,M[1][S]=e,M[0][S+1]=c,M[1][S+1]=o;var E=[{x:v.apply(null,M[0]),y:v.apply(null,M[1])},{x:g.apply(null,M[0]),y:g.apply(null,M[1])}];return fabric.cachesBoundsOfCurve&&(fabric.boundsOfCurveCache[s]=E),E}function i(t,r,n){for(var i=n[1],a=n[2],c=n[3],o=n[4],s=n[5],f=n[6],u=n[7],l=e(f-t,u-r,i,a,o,s,c),d=0,h=l.length;h>d;d++)l[d][1]+=t,l[d][2]+=r,l[d][3]+=t,l[d][4]+=r,l[d][5]+=t,l[d][6]+=r;return l}function a(t){var e,r,n,a,c,o,s=0,f=0,u=t.length,l=0,d=0,h=[];for(r=0;u>r;++r){switch(n=!1,e=t[r].slice(0),e[0]){case"l":e[0]="L",e[1]+=s,e[2]+=f;case"L":s=e[1],f=e[2];break;case"h":e[1]+=s;case"H":e[0]="L",e[2]=f,s=e[1];break;case"v":e[1]+=f;case"V":e[0]="L",f=e[1],e[1]=s,e[2]=f;break;case"m":e[0]="M",e[1]+=s,e[2]+=f;case"M":s=e[1],f=e[2],l=e[1],d=e[2];break;case"c":e[0]="C",e[1]+=s,e[2]+=f,e[3]+=s,e[4]+=f,e[5]+=s,e[6]+=f;case"C":c=e[3],o=e[4],s=e[5],f=e[6];break;case"s":e[0]="S",e[1]+=s,e[2]+=f,e[3]+=s,e[4]+=f;case"S":"C"===a?(c=2*s-c,o=2*f-o):(c=s,o=f),s=e[3],f=e[4],e[0]="C",e[5]=e[3],e[6]=e[4],e[3]=e[1],e[4]=e[2],e[1]=c,e[2]=o,c=e[3],o=e[4];break;case"q":e[0]="Q",e[1]+=s,e[2]+=f,e[3]+=s,e[4]+=f;case"Q":c=e[1],o=e[2],s=e[3],f=e[4];break;case"t":e[0]="T",e[1]+=s,e[2]+=f;case"T":"Q"===a?(c=2*s-c,o=2*f-o):(c=s,o=f),e[0]="Q",s=e[1],f=e[2],e[1]=c,e[2]=o,e[3]=s,e[4]=f;break;case"a":e[0]="A",e[6]+=s,e[7]+=f;case"A":n=!0,h=h.concat(i(s,f,e)),s=e[6],f=e[7];break;case"z":case"Z":s=l,f=d}n||h.push(e),a=e[0]}return h}function c(t,e,r,n){return Math.sqrt((r-t)*(r-t)+(n-e)*(n-e))}function o(t){return t*t*t}function s(t){return 3*t*t*(1-t)}function f(t){return 3*t*(1-t)*(1-t)}function u(t){return(1-t)*(1-t)*(1-t)}function l(t,e,r,n,i,a,c,l){return function(d){var h=o(d),b=s(d),m=f(d),p=u(d);return{x:c*h+i*b+r*m+t*p,y:l*h+a*b+n*m+e*p}}}function d(t,e,r,n,i,a,c,o){return function(s){var f=1-s,u=3*f*f*(r-t)+6*f*s*(i-r)+3*s*s*(c-i),l=3*f*f*(n-e)+6*f*s*(a-n)+3*s*s*(o-a);return Math.atan2(l,u)}}function h(t){return t*t}function b(t){return 2*t*(1-t)}function m(t){return(1-t)*(1-t)}function p(t,e,r,n,i,a){return function(c){var o=h(c),s=b(c),f=m(c);return{x:i*o+r*s+t*f,y:a*o+n*s+e*f}}}function y(t,e,r,n,i,a){return function(c){var o=1-c,s=2*o*(r-t)+2*c*(i-r),f=2*o*(n-e)+2*c*(a-n);return Math.atan2(f,s)}}function v(t,e,r){var n,i,a={x:e,y:r},o=0;for(i=1;100>=i;i+=1)n=t(i/100),o+=c(a.x,a.y,n.x,n.y),a=n;return o}function g(t,e){for(var r,n,i,a=0,o=0,s=t.iterator,f={x:t.x,y:t.y},u=.01,l=t.angleFinder;e>o&&u>1e-4;)r=s(a),i=a,n=c(f.x,f.y,r.x,r.y),n+o>e?(a-=u,u/=2):(f=r,a+=u,o+=n);return r.angle=l(i),r}function x(t){for(var e,r,n,i,a=0,o=t.length,s=0,f=0,u=0,h=0,b=[],m=0;o>m;m++){switch(e=t[m],n={x:s,y:f,command:e[0]},e[0]){case"M":n.length=0,u=s=e[1],h=f=e[2];break;case"L":n.length=c(s,f,e[1],e[2]),s=e[1],f=e[2];break;case"C":r=l(s,f,e[1],e[2],e[3],e[4],e[5],e[6]),i=d(s,f,e[1],e[2],e[3],e[4],e[5],e[6]),n.iterator=r,n.angleFinder=i,n.length=v(r,s,f),s=e[5],f=e[6];break;case"Q":r=p(s,f,e[1],e[2],e[3],e[4]),i=y(s,f,e[1],e[2],e[3],e[4]),n.iterator=r,n.angleFinder=i,n.length=v(r,s,f),s=e[3],f=e[4];break;case"Z":case"z":n.destX=u,n.destY=h,n.length=c(s,f,u,h),s=u,f=h}a+=n.length,b.push(n)}return b.push({length:a,x:s,y:f}),b}function w(t,e,r){r||(r=x(t));for(var n=0;e-r[n].length>0&&nm;m++){e=a[m],i=e.slice(1).trim(),o.length=0;var y=e.charAt(0);if(b=[y],"a"===y.toLowerCase())for(var v;v=h.exec(i);)for(var g=1;gg;g++)r=parseFloat(o[g]),isNaN(r)||b.push(r);var w=T[y.toLowerCase()],M=j[y]||y;if(b.length-1>w)for(var P=1,C=b.length;C>P;P+=w)c.push([y].concat(b.slice(P,P+w))),y=M;else c.push(b)}return c}function P(t,e){var r,n=[],i=new fabric.Point(t[0].x,t[0].y),a=new fabric.Point(t[1].x,t[1].y),c=t.length,o=1,s=0,f=c>2;for(e=e||0,f&&(o=t[2].xr;r++){if(!i.eq(a)){var u=i.midPointFrom(a);n.push(["Q",i.x,i.y,u.x,u.y])}i=t[r],r+1t[r-2].x?1:i.x===t[r-2].x?0:-1,s=i.y>t[r-2].y?1:i.y===t[r-2].y?0:-1),n.push(["L",i.x+o*e,i.y+s*e]),n}function C(t,e,r){return r&&(e=fabric.util.multiplyTransformMatrices(e,[1,0,0,1,-r.x,-r.y])),t.map(function(t){for(var r=t.slice(0),n={},i=1;ii;i++)n[i]=r.length?t[i][e].apply(t[i],r):t[i][e].call(t[i]);return n}function e(t,e){return i(t,e,function(t,e){return t>=e})}function r(t,e){return i(t,e,function(t,e){return e>t})}function n(t,e){for(var r=t.length;r--;)t[r]=e;return t}function i(t,e,r){if(t&&0!==t.length){var n=t.length-1,i=e?t[n][e]:t[n];if(e)for(;n--;)r(t[n][e],i)&&(i=t[n][e]);else for(;n--;)r(t[n],i)&&(i=t[n]);return i}}var a=Array.prototype.slice;fabric.util.array={fill:n,invoke:t,min:r,max:e}}();!function(){function t(e,r,n){if(n)if(!fabric.isLikelyNode&&r instanceof Element)e=r;else if(r instanceof Array){e=[];for(var i=0,a=r.length;a>i;i++)e[i]=t({},r[i],n)}else if(r&&"object"==typeof r)for(var c in r)"canvas"===c||"group"===c?e[c]=null:r.hasOwnProperty(c)&&(e[c]=t({},r[c],n));else e=r;else for(var c in r)e[c]=r[c];return e}function e(e,r){return t({},e,r)}fabric.util.object={extend:t,clone:e},fabric.util.object.extend(fabric.util,fabric.Observable)}();!function(){function t(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})}function e(t,e){return t.charAt(0).toUpperCase()+(e?t.slice(1):t.slice(1).toLowerCase())}function r(t){return t.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function n(t){var e,r=0,n=[];for(r=0,e;rr||r>57343)return t.charAt(e);if(r>=55296&&56319>=r){if(t.length<=e+1)throw"High surrogate without following low surrogate";var n=t.charCodeAt(e+1);if(56320>n||n>57343)throw"High surrogate without following low surrogate";return t.charAt(e)+t.charAt(e+1)}if(0===e)throw"Low surrogate without preceding high surrogate";var i=t.charCodeAt(e-1);if(55296>i||i>56319)throw"Low surrogate without preceding high surrogate";return!1}fabric.util.string={camelize:t,capitalize:e,escapeXml:r,graphemeSplit:n}}();!function(){function t(){}function e(t){for(var e=null,r=this;r.constructor.superclass;){var i=r.constructor.superclass.prototype[t];if(r[t]!==i){e=i;break}r=r.constructor.superclass.prototype}return e?arguments.length>1?e.apply(this,n.call(arguments,1)):e.call(this):console.log("tried to callSuper "+t+", method not found in prototype chain",this)}function r(){function r(){this.initialize.apply(this,arguments)}var a=null,c=n.call(arguments,0);"function"==typeof c[0]&&(a=c.shift()),r.superclass=a,r.subclasses=[],a&&(t.prototype=a.prototype,r.prototype=new t,a.subclasses.push(r));for(var s=0,l=c.length;l>s;s++)o(r,c[s],a);return r.prototype.initialize||(r.prototype.initialize=i),r.prototype.constructor=r,r.prototype.callSuper=e,r}var n=Array.prototype.slice,i=function(){},a=function(){for(var t in{toString:1})if("toString"===t)return!1;return!0}(),o=function(t,e,r){for(var n in e)t.prototype[n]=n in t.prototype&&"function"==typeof t.prototype[n]&&(e[n]+"").indexOf("callSuper")>-1?function(t){return function(){var n=this.constructor.superclass;this.constructor.superclass=r;var i=e[t].apply(this,arguments);return this.constructor.superclass=n,"initialize"!==t?i:void 0}}(n):e[n],a&&(e.toString!==Object.prototype.toString&&(t.prototype.toString=e.toString),e.valueOf!==Object.prototype.valueOf&&(t.prototype.valueOf=e.valueOf))};fabric.util.createClass=r}();!function(){function t(t){var e=t.changedTouches;return e&&e[0]?e[0]:t}var e=!!fabric.document.createElement("div").attachEvent,r=["touchstart","touchmove","touchend"];fabric.util.addListener=function(t,r,i,n){t&&t.addEventListener(r,i,e?!1:n)},fabric.util.removeListener=function(t,r,i,n){t&&t.removeEventListener(r,i,e?!1:n)},fabric.util.getPointer=function(e){var r=e.target,i=fabric.util.getScrollLeftTop(r),n=t(e);return{x:n.clientX+i.left,y:n.clientY+i.top}},fabric.util.isTouchEvent=function(t){return r.indexOf(t.type)>-1||"touch"===t.pointerType}}();!function(){function t(t,e){var i=t.style;if(!i)return t;if("string"==typeof e)return t.style.cssText+=";"+e,e.indexOf("opacity")>-1?a(t,e.match(/opacity:\s*(\d?\.?\d*)/)[1]):t;for(var n in e)if("opacity"===n)a(t,e[n]);else{var r="float"===n||"cssFloat"===n?"undefined"==typeof i.styleFloat?"cssFloat":"styleFloat":n;i.setProperty(r,e[n])}return t}var e=fabric.document.createElement("div"),i="string"==typeof e.style.opacity,n="string"==typeof e.style.filter,r=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,a=function(t){return t};i?a=function(t,e){return t.style.opacity=e,t}:n&&(a=function(t,e){var i=t.style;return t.currentStyle&&!t.currentStyle.hasLayout&&(i.zoom=1),r.test(i.filter)?(e=e>=.9999?"":"alpha(opacity="+100*e+")",i.filter=i.filter.replace(r,e)):i.filter+=" alpha(opacity="+100*e+")",t}),fabric.util.setStyle=t}();!function(){function t(t){return"string"==typeof t?fabric.document.getElementById(t):t}function e(t,e){var i=fabric.document.createElement(t);for(var n in e)"class"===n?i.className=e[n]:"for"===n?i.htmlFor=e[n]:i.setAttribute(n,e[n]);return i}function i(t,e){t&&-1===(" "+t.className+" ").indexOf(" "+e+" ")&&(t.className+=(t.className?" ":"")+e)}function n(t,i,n){return"string"==typeof i&&(i=e(i,n)),t.parentNode&&t.parentNode.replaceChild(i,t),i.appendChild(t),i}function r(t){for(var e=0,i=0,n=fabric.document.documentElement,r=fabric.document.body||{scrollLeft:0,scrollTop:0};t&&(t.parentNode||t.host)&&(t=t.parentNode||t.host,t===fabric.document?(e=r.scrollLeft||n.scrollLeft||0,i=r.scrollTop||n.scrollTop||0):(e+=t.scrollLeft||0,i+=t.scrollTop||0),1!==t.nodeType||"fixed"!==t.style.position););return{left:e,top:i}}function a(t){var e,i,n=t&&t.ownerDocument,a={left:0,top:0},o={left:0,top:0},s={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!n)return o;for(var c in s)o[s[c]]+=parseInt(d(t,c),10)||0;return e=n.documentElement,"undefined"!=typeof t.getBoundingClientRect&&(a=t.getBoundingClientRect()),i=r(t),{left:a.left+i.left-(e.clientLeft||0)+o.left,top:a.top+i.top-(e.clientTop||0)+o.top}}function o(t){var e=fabric.jsdomImplForWrapper(t);return e._canvas||e._image}function s(t){if(fabric.isLikelyNode){var e=fabric.jsdomImplForWrapper(t);e&&(e._image=null,e._canvas=null,e._currentSrc=null,e._attributes=null,e._classList=null)}}function c(t,e){t.imageSmoothingEnabled=t.imageSmoothingEnabled||t.webkitImageSmoothingEnabled||t.mozImageSmoothingEnabled||t.msImageSmoothingEnabled||t.oImageSmoothingEnabled,t.imageSmoothingEnabled=e}var l,f=Array.prototype.slice,u=function(t){return f.call(t,0)};try{l=u(fabric.document.childNodes)instanceof Array}catch(h){}l||(u=function(t){for(var e=new Array(t.length),i=t.length;i--;)e[i]=t[i];return e});var d;d=fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle?function(t,e){var i=fabric.document.defaultView.getComputedStyle(t,null);return i?i[e]:void 0}:function(t,e){var i=t.style[e];return!i&&t.currentStyle&&(i=t.currentStyle[e]),i},function(){function t(t){return"undefined"!=typeof t.onselectstart&&(t.onselectstart=fabric.util.falseFunction),n?t.style[n]="none":"string"==typeof t.unselectable&&(t.unselectable="on"),t}function e(t){return"undefined"!=typeof t.onselectstart&&(t.onselectstart=null),n?t.style[n]="":"string"==typeof t.unselectable&&(t.unselectable=""),t}var i=fabric.document.documentElement.style,n="userSelect"in i?"userSelect":"MozUserSelect"in i?"MozUserSelect":"WebkitUserSelect"in i?"WebkitUserSelect":"KhtmlUserSelect"in i?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=t,fabric.util.makeElementSelectable=e}(),fabric.util.setImageSmoothing=c,fabric.util.getById=t,fabric.util.toArray=u,fabric.util.addClass=i,fabric.util.makeElement=e,fabric.util.wrapElement=n,fabric.util.getScrollLeftTop=r,fabric.util.getElementOffset=a,fabric.util.getNodeCanvas=o,fabric.util.cleanUpJsdomNode=s}();!function(){function t(t,e){return t+(/\?/.test(t)?"&":"?")+e}function e(){}function i(i,n){n||(n={});var r=n.method?n.method.toUpperCase():"GET",a=n.onComplete||function(){},o=new fabric.window.XMLHttpRequest,s=n.body||n.parameters;return o.onreadystatechange=function(){4===o.readyState&&(a(o),o.onreadystatechange=e)},"GET"===r&&(s=null,"string"==typeof n.parameters&&(i=t(i,n.parameters))),o.open(r,i,!0),("POST"===r||"PUT"===r)&&o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),o.send(s),o}fabric.util.request=i}();fabric.log=console.log,fabric.warn=console.warn;!function(){function t(){return!1}function e(t,e,i,n){return-i*Math.cos(t/n*(Math.PI/2))+i+e}function i(i){i||(i={});var r,a=!1,c=function(){var t=fabric.runningAnimations.indexOf(r);return t>-1&&fabric.runningAnimations.splice(t,1)[0]};return r=s(o(i),{cancel:function(){return a=!0,c()},currentValue:"startValue"in i?i.startValue:0,completionRate:0,durationRate:0}),fabric.runningAnimations.push(r),n(function(s){var o,l=s||+new Date,h=i.duration||500,u=l+h,f=i.onChange||t,d=i.abort||t,p=i.onComplete||t,g=i.easing||e,m="startValue"in i?i.startValue.length>0:!1,v="startValue"in i?i.startValue:0,y="endValue"in i?i.endValue:100,b=i.byValue||(m?v.map(function(t,e){return y[e]-v[e]}):y-v);i.onStart&&i.onStart(),function x(t){o=t||+new Date;var e=o>u?h:o-l,i=e/h,s=m?v.map(function(t,i){return g(e,v[i],b[i],h)}):g(e,v,b,h),_=Math.abs(m?(s[0]-v[0])/b[0]:(s-v)/b);return r.currentValue=m?s.slice():s,r.completionRate=_,r.durationRate=i,a?void 0:d(s,_,i)?void c():o>u?(r.currentValue=m?y.slice():y,r.completionRate=1,r.durationRate=1,f(m?y.slice():y,1,1),p(y,1,1),void c()):(f(s,_,i),void n(x))}(l)}),r.cancel}function n(){return c.apply(fabric.window,arguments)}function r(){return l.apply(fabric.window,arguments)}var s=fabric.util.object.extend,o=fabric.util.object.clone,a=[];fabric.util.object.extend(a,{cancelAll:function(){var t=this.splice(0);return t.forEach(function(t){t.cancel()}),t},cancelByCanvas:function(t){if(!t)return[];var e=this.filter(function(e){return"object"==typeof e.target&&e.target.canvas===t});return e.forEach(function(t){t.cancel()}),e},cancelByTarget:function(t){var e=this.findAnimationsByTarget(t);return e.forEach(function(t){t.cancel()}),e},findAnimationIndex:function(t){return this.indexOf(this.findAnimation(t))},findAnimation:function(t){return this.find(function(e){return e.cancel===t})},findAnimationsByTarget:function(t){return t?this.filter(function(e){return e.target===t}):[]}});var c=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(t){return fabric.window.setTimeout(t,1e3/60)},l=fabric.window.cancelAnimationFrame||fabric.window.clearTimeout;fabric.util.animate=i,fabric.util.requestAnimFrame=n,fabric.util.cancelAnimFrame=r,fabric.runningAnimations=a}();!function(){function t(t,e,i){var n="rgba("+parseInt(t[0]+i*(e[0]-t[0]),10)+","+parseInt(t[1]+i*(e[1]-t[1]),10)+","+parseInt(t[2]+i*(e[2]-t[2]),10);return n+=","+(t&&e?parseFloat(t[3]+i*(e[3]-t[3])):1),n+=")"}function e(e,i,n,r){var s=new fabric.Color(e).getSource(),o=new fabric.Color(i).getSource(),a=r.onComplete,c=r.onChange;return r=r||{},fabric.util.animate(fabric.util.object.extend(r,{duration:n||500,startValue:s,endValue:o,byValue:o,easing:function(e,i,n,s){var o=r.colorEasing?r.colorEasing(e,s):1-Math.cos(e/s*(Math.PI/2));return t(i,n,o)},onComplete:function(e,i,n){return a?a(t(o,o,0),i,n):void 0},onChange:function(e,i,n){if(c){if(Array.isArray(e))return c(t(e,e,0),i,n);c(e,i,n)}}}))}fabric.util.animateColor=e}();!function(){function t(t,e,i,n){return tt?i/2*t*t*t+e:i/2*((t-=2)*t*t+2)+e}function r(t,e,i,n){return i*(t/=n)*t*t*t+e}function s(t,e,i,n){return-i*((t=t/n-1)*t*t*t-1)+e}function o(t,e,i,n){return t/=n/2,1>t?i/2*t*t*t*t+e:-i/2*((t-=2)*t*t*t-2)+e}function a(t,e,i,n){return i*(t/=n)*t*t*t*t+e}function c(t,e,i,n){return i*((t=t/n-1)*t*t*t*t+1)+e}function l(t,e,i,n){return t/=n/2,1>t?i/2*t*t*t*t*t+e:i/2*((t-=2)*t*t*t*t+2)+e}function h(t,e,i,n){return-i*Math.cos(t/n*(Math.PI/2))+i+e}function u(t,e,i,n){return i*Math.sin(t/n*(Math.PI/2))+e}function f(t,e,i,n){return-i/2*(Math.cos(Math.PI*t/n)-1)+e}function d(t,e,i,n){return 0===t?e:i*Math.pow(2,10*(t/n-1))+e}function p(t,e,i,n){return t===n?e+i:i*(-Math.pow(2,-10*t/n)+1)+e}function g(t,e,i,n){return 0===t?e:t===n?e+i:(t/=n/2,1>t?i/2*Math.pow(2,10*(t-1))+e:i/2*(-Math.pow(2,-10*--t)+2)+e)}function m(t,e,i,n){return-i*(Math.sqrt(1-(t/=n)*t)-1)+e}function v(t,e,i,n){return i*Math.sqrt(1-(t=t/n-1)*t)+e}function y(t,e,i,n){return t/=n/2,1>t?-i/2*(Math.sqrt(1-t*t)-1)+e:i/2*(Math.sqrt(1-(t-=2)*t)+1)+e}function b(i,n,r,s){var o=1.70158,a=0,c=r;if(0===i)return n;if(i/=s,1===i)return n+r;a||(a=.3*s);var l=t(c,r,a,o);return-e(l,i,s)+n}function x(e,i,n,r){var s=1.70158,o=0,a=n;if(0===e)return i;if(e/=r,1===e)return i+n;o||(o=.3*r);var c=t(a,n,o,s);return c.a*Math.pow(2,-10*e)*Math.sin(2*(e*r-c.s)*Math.PI/c.p)+c.c+i}function _(i,n,r,s){var o=1.70158,a=0,c=r;if(0===i)return n;if(i/=s/2,2===i)return n+r;a||(a=.3*s*1.5);var l=t(c,r,a,o);return 1>i?-.5*e(l,i,s)+n:l.a*Math.pow(2,-10*(i-=1))*Math.sin(2*(i*s-l.s)*Math.PI/l.p)*.5+l.c+n}function S(t,e,i,n,r){return void 0===r&&(r=1.70158),i*(t/=n)*t*((r+1)*t-r)+e}function C(t,e,i,n,r){return void 0===r&&(r=1.70158),i*((t=t/n-1)*t*((r+1)*t+r)+1)+e}function T(t,e,i,n,r){return void 0===r&&(r=1.70158),t/=n/2,1>t?i/2*t*t*(((r*=1.525)+1)*t-r)+e:i/2*((t-=2)*t*(((r*=1.525)+1)*t+r)+2)+e}function w(t,e,i,n){return i-O(n-t,0,i,n)+e}function O(t,e,i,n){return(t/=n)<1/2.75?7.5625*i*t*t+e:2/2.75>t?i*(7.5625*(t-=1.5/2.75)*t+.75)+e:2.5/2.75>t?i*(7.5625*(t-=2.25/2.75)*t+.9375)+e:i*(7.5625*(t-=2.625/2.75)*t+.984375)+e}function M(t,e,i,n){return n/2>t?.5*w(2*t,0,i,n)+e:.5*O(2*t-n,0,i,n)+.5*i+e}fabric.util.ease={easeInQuad:function(t,e,i,n){return i*(t/=n)*t+e},easeOutQuad:function(t,e,i,n){return-i*(t/=n)*(t-2)+e},easeInOutQuad:function(t,e,i,n){return t/=n/2,1>t?i/2*t*t+e:-i/2*(--t*(t-2)-1)+e},easeInCubic:function(t,e,i,n){return i*(t/=n)*t*t+e},easeOutCubic:i,easeInOutCubic:n,easeInQuart:r,easeOutQuart:s,easeInOutQuart:o,easeInQuint:a,easeOutQuint:c,easeInOutQuint:l,easeInSine:h,easeOutSine:u,easeInOutSine:f,easeInExpo:d,easeOutExpo:p,easeInOutExpo:g,easeInCirc:m,easeOutCirc:v,easeInOutCirc:y,easeInElastic:b,easeOutElastic:x,easeInOutElastic:_,easeInBack:S,easeOutBack:C,easeInOutBack:T,easeInBounce:w,easeOutBounce:O,easeInOutBounce:M}}();!function(t){"use strict";function e(t){return t in E?E[t]:t}function i(t,e,i,n){var r,s=Array.isArray(e);if("fill"!==t&&"stroke"!==t||"none"!==e){if("strokeUniform"===t)return"non-scaling-stroke"===e;if("strokeDashArray"===t)e="none"===e?null:e.replace(/,/g," ").split(/\s+/).map(parseFloat);else if("transformMatrix"===t)e=i&&i.transformMatrix?S(i.transformMatrix,v.parseTransformAttribute(e)):v.parseTransformAttribute(e);else if("visible"===t)e="none"!==e&&"hidden"!==e,i&&i.visible===!1&&(e=!1);else if("opacity"===t)e=parseFloat(e),i&&"undefined"!=typeof i.opacity&&(e*=i.opacity);else if("textAnchor"===t)e="start"===e?"left":"end"===e?"right":"center";else if("charSpacing"===t)r=_(e,n)/n*1e3;else if("paintFirst"===t){var o=e.indexOf("fill"),a=e.indexOf("stroke"),e="fill";o>-1&&a>-1&&o>a?e="stroke":-1===o&&a>-1&&(e="stroke")}else{if("href"===t||"xlink:href"===t||"font"===t)return e;if("imageSmoothing"===t)return"optimizeQuality"===e;r=s?e.map(_):_(e,n)}}else e="";return!s&&isNaN(r)?e:r}function n(t){return new RegExp("^("+t.join("|")+")\\b","i")}function r(t){for(var e in M)if("undefined"!=typeof t[M[e]]&&""!==t[e]){if("undefined"==typeof t[e]){if(!v.Object.prototype[e])continue;t[e]=v.Object.prototype[e]}if(0!==t[e].indexOf("url(")){var i=new v.Color(t[e]);t[e]=i.setAlpha(x(i.getAlpha()*t[M[e]],2)).toRgba()}}return t}function s(t,e){var i,n,r,s,o=[];for(r=0,s=e.length;s>r;r++)i=e[r],n=t.getElementsByTagName(i),o=o.concat(Array.prototype.slice.call(n));return o}function o(t,e){var i,n;t.replace(/;\s*$/,"").split(";").forEach(function(t){var r=t.split(":");i=r[0].trim().toLowerCase(),n=r[1].trim(),e[i]=n})}function a(t,e){var i,n;for(var r in t)"undefined"!=typeof t[r]&&(i=r.toLowerCase(),n=t[r],e[i]=n)}function c(t,e){var i={};for(var n in v.cssRules[e])if(l(t,n.split(" ")))for(var r in v.cssRules[e][n])i[r]=v.cssRules[e][n][r];return i}function l(t,e){var i,n=!0;return i=u(t,e.pop()),i&&e.length&&(n=h(t,e)),i&&n&&0===e.length}function h(t,e){for(var i,n=!0;t.parentNode&&1===t.parentNode.nodeType&&e.length;)n&&(i=e.pop()),t=t.parentNode,n=u(t,i);return 0===e.length}function u(t,e){var i,n,r=t.nodeName,s=t.getAttribute("class"),o=t.getAttribute("id");if(i=new RegExp("^"+r,"i"),e=e.replace(i,""),o&&e.length&&(i=new RegExp("#"+o+"(?![a-zA-Z\\-]+)","i"),e=e.replace(i,"")),s&&e.length)for(s=s.split(" "),n=s.length;n--;)i=new RegExp("\\."+s[n]+"(?![a-zA-Z\\-]+)","i"),e=e.replace(i,"");return 0===e.length}function f(t,e){var i;if(t.getElementById&&(i=t.getElementById(e)),i)return i;var n,r,s,o=t.getElementsByTagName("*");for(r=0,s=o.length;s>r;r++)if(n=o[r],e===n.getAttribute("id"))return n}function d(t){for(var e=s(t,["use","svg:use"]),i=0;e.length&&ic;c++)a=l.item(c),_.setAttributeNS(x,a.nodeName,a.nodeValue);for(;m.firstChild;)_.appendChild(m.firstChild);m=_}for(c=0,l=n.attributes,h=l.length;h>c;c++)a=l.item(c),"x"!==a.nodeName&&"y"!==a.nodeName&&"xlink:href"!==a.nodeName&&"href"!==a.nodeName&&("transform"===a.nodeName?y=a.nodeValue+" "+y:m.setAttribute(a.nodeName,a.nodeValue));m.setAttribute("transform",y),m.setAttribute("instantiated_by_use","1"),m.removeAttribute("id"),o=n.parentNode,o.replaceChild(m,n),e.length===b&&i++}}function p(t){if(!v.svgViewBoxElementsRegEx.test(t.nodeName))return{};var e,i,n,r,s=t.getAttribute("viewBox"),o=1,a=1,c=0,l=0,h=t.getAttribute("width"),u=t.getAttribute("height"),f=t.getAttribute("x")||0,d=t.getAttribute("y")||0,p=t.getAttribute("preserveAspectRatio")||"",g=!s||!(s=s.match(L)),m=!h||!u||"100%"===h||"100%"===u,y=g&&m,b={},x="",S=0,C=0;if(b.width=0,b.height=0,b.toBeParsed=y,g&&(f||d)&&t.parentNode&&"#document"!==t.parentNode.nodeName&&(x=" translate("+_(f)+" "+_(d)+") ",n=(t.getAttribute("transform")||"")+x,t.setAttribute("transform",n),t.removeAttribute("x"),t.removeAttribute("y")),y)return b;if(g)return b.width=_(h),b.height=_(u),b;if(c=-parseFloat(s[1]),l=-parseFloat(s[2]),e=parseFloat(s[3]),i=parseFloat(s[4]),b.minX=c,b.minY=l,b.viewBoxWidth=e,b.viewBoxHeight=i,m?(b.width=e,b.height=i):(b.width=_(h),b.height=_(u),o=b.width/e,a=b.height/i),p=v.util.parsePreserveAspectRatioAttribute(p),"none"!==p.alignX&&("meet"===p.meetOrSlice&&(a=o=o>a?a:o),"slice"===p.meetOrSlice&&(a=o=o>a?o:a),S=b.width-e*o,C=b.height-i*o,"Mid"===p.alignX&&(S/=2),"Mid"===p.alignY&&(C/=2),"Min"===p.alignX&&(S=0),"Min"===p.alignY&&(C=0)),1===o&&1===a&&0===c&&0===l&&0===f&&0===d)return b;if((f||d)&&"#document"!==t.parentNode.nodeName&&(x=" translate("+_(f)+" "+_(d)+") "),n=x+" matrix("+o+" 0 0 "+a+" "+(c*o+S)+" "+(l*a+C)+") ","svg"===t.nodeName){for(r=t.ownerDocument.createElementNS(v.svgNS,"g");t.firstChild;)r.appendChild(t.firstChild);t.appendChild(r)}else r=t,r.removeAttribute("x"),r.removeAttribute("y"),n=r.getAttribute("transform")+n;return r.setAttribute("transform",n),b}function g(t,e){for(;t&&(t=t.parentNode);)if(t.nodeName&&e.test(t.nodeName.replace("svg:",""))&&!t.getAttribute("instantiated_by_use"))return!0;return!1}function m(t,e){var i=["gradientTransform","x1","x2","y1","y2","gradientUnits","cx","cy","r","fx","fy"],n="xlink:href",r=e.getAttribute(n).slice(1),s=f(t,r);if(s&&s.getAttribute(n)&&m(t,s),i.forEach(function(t){s&&!e.hasAttribute(t)&&s.hasAttribute(t)&&e.setAttribute(t,s.getAttribute(t))}),!e.children.length)for(var o=s.cloneNode(!0);o.firstChild;)e.appendChild(o.firstChild);e.removeAttribute(n)}var v=t.fabric||(t.fabric={}),y=v.util.object.extend,b=v.util.object.clone,x=v.util.toFixed,_=v.util.parseUnit,S=v.util.multiplyTransformMatrices,C=["path","circle","polygon","polyline","ellipse","rect","line","image","text"],w=["symbol","image","marker","pattern","view","svg"],T=["pattern","defs","symbol","metadata","clipPath","mask","desc"],O=["symbol","g","a","svg","clipPath","defs"],E={cx:"left",x:"left",r:"radius",cy:"top",y:"top",display:"visible",visibility:"visible",transform:"transformMatrix","fill-opacity":"fillOpacity","fill-rule":"fillRule","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","letter-spacing":"charSpacing","paint-order":"paintFirst","stroke-dasharray":"strokeDashArray","stroke-dashoffset":"strokeDashOffset","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration","text-anchor":"textAnchor",opacity:"opacity","clip-path":"clipPath","clip-rule":"clipRule","vector-effect":"strokeUniform","image-rendering":"imageSmoothing"},M={stroke:"strokeOpacity",fill:"fillOpacity"},k="font-size",A="clip-path";v.svgValidTagNamesRegEx=n(C),v.svgViewBoxElementsRegEx=n(w),v.svgInvalidAncestorsRegEx=n(T),v.svgValidParentsRegEx=n(O),v.cssRules={},v.gradientDefs={},v.clipPaths={},v.parseTransformAttribute=function(){function t(t,e){var i=v.util.cos(e[0]),n=v.util.sin(e[0]),r=0,s=0;3===e.length&&(r=e[1],s=e[2]),t[0]=i,t[1]=n,t[2]=-n,t[3]=i,t[4]=r-(i*r-n*s),t[5]=s-(n*r+i*s)}function e(t,e){var i=e[0],n=2===e.length?e[1]:e[0];t[0]=i,t[3]=n}function i(t,e,i){t[i]=Math.tan(v.util.degreesToRadians(e[0]))}function n(t,e){t[4]=e[0],2===e.length&&(t[5]=e[1])}var r=v.iMatrix,s=v.reNum,o=v.commaWsp,a="(?:(skewX)\\s*\\(\\s*("+s+")\\s*\\))",c="(?:(skewY)\\s*\\(\\s*("+s+")\\s*\\))",l="(?:(rotate)\\s*\\(\\s*("+s+")(?:"+o+"("+s+")"+o+"("+s+"))?\\s*\\))",h="(?:(scale)\\s*\\(\\s*("+s+")(?:"+o+"("+s+"))?\\s*\\))",u="(?:(translate)\\s*\\(\\s*("+s+")(?:"+o+"("+s+"))?\\s*\\))",f="(?:(matrix)\\s*\\(\\s*("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")\\s*\\))",d="(?:"+f+"|"+u+"|"+h+"|"+l+"|"+a+"|"+c+")",p="(?:"+d+"(?:"+o+"*"+d+")*)",g="^\\s*(?:"+p+"?)\\s*$",m=new RegExp(g),y=new RegExp(d,"g");return function(s){var o=r.concat(),a=[];if(!s||s&&!m.test(s))return o;s.replace(y,function(s){var c=new RegExp(d).exec(s).filter(function(t){return!!t}),l=c[1],h=c.slice(2).map(parseFloat);switch(l){case"translate":n(o,h);break;case"rotate":h[0]=v.util.degreesToRadians(h[0]),t(o,h);break;case"scale":e(o,h);break;case"skewX":i(o,h,2);break;case"skewY":i(o,h,1);break;case"matrix":o=h}a.push(o.concat()),o=r.concat()});for(var c=a[0];a.length>1;)a.shift(),c=v.util.multiplyTransformMatrices(c,a[0]);return c}}();var L=new RegExp("^\\s*("+v.reNum+"+)\\s*,?\\s*("+v.reNum+"+)\\s*,?\\s*("+v.reNum+"+)\\s*,?\\s*("+v.reNum+"+)\\s*$");v.parseSVGDocument=function(t,e,i,n){if(t){d(t);var r,s,o=v.Object.__uid++,a=p(t),c=v.util.toArray(t.getElementsByTagName("*"));if(a.crossOrigin=n&&n.crossOrigin,a.svgUid=o,0===c.length&&v.isLikelyNode){c=t.selectNodes('//*[name(.)!="svg"]');var l=[];for(r=0,s=c.length;s>r;r++)l[r]=c[r];c=l}var h=c.filter(function(t){return p(t),v.svgValidTagNamesRegEx.test(t.nodeName.replace("svg:",""))&&!g(t,v.svgInvalidAncestorsRegEx)});if(!h||h&&!h.length)return void(e&&e([],{}));var u={};c.filter(function(t){return"clipPath"===t.nodeName.replace("svg:","")}).forEach(function(t){var e=t.getAttribute("id");u[e]=v.util.toArray(t.getElementsByTagName("*")).filter(function(t){return v.svgValidTagNamesRegEx.test(t.nodeName.replace("svg:",""))})}),v.gradientDefs[o]=v.getGradientDefs(t),v.cssRules[o]=v.getCSSRules(t),v.clipPaths[o]=u,v.parseElements(h,function(t,i){e&&(e(t,a,i,c),delete v.gradientDefs[o],delete v.cssRules[o],delete v.clipPaths[o])},b(a),i,n)}};var I=new RegExp("(normal|italic)?\\s*(normal|small-caps)?\\s*(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*("+v.reNum+"(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|"+v.reNum+"))?\\s+(.*)");y(v,{parseFontDeclaration:function(t,e){var i=t.match(I);if(i){var n=i[1],r=i[3],s=i[4],o=i[5],a=i[6];n&&(e.fontStyle=n),r&&(e.fontWeight=isNaN(parseFloat(r))?r:parseFloat(r)),s&&(e.fontSize=_(s)),a&&(e.fontFamily=a),o&&(e.lineHeight="normal"===o?1:o)}},getGradientDefs:function(t){var e,i=["linearGradient","radialGradient","svg:linearGradient","svg:radialGradient"],n=s(t,i),r=0,o={};for(r=n.length;r--;)e=n[r],e.getAttribute("xlink:href")&&m(t,e),o[e.getAttribute("id")]=e;return o},parseAttributes:function(t,n,s){if(t){var o,a,l,h={};"undefined"==typeof s&&(s=t.getAttribute("svgUid")),t.parentNode&&v.svgValidParentsRegEx.test(t.parentNode.nodeName)&&(h=v.parseAttributes(t.parentNode,n,s));var u=n.reduce(function(e,i){return o=t.getAttribute(i),o&&(e[i]=o),e},{}),f=y(c(t,s),v.parseStyleAttribute(t));u=y(u,f),f[A]&&t.setAttribute(A,f[A]),a=l=h.fontSize||v.Text.DEFAULT_SVG_FONT_SIZE,u[k]&&(u[k]=a=_(u[k],l));var d,p,g={};for(var m in u)d=e(m),p=i(d,u[m],h,a),g[d]=p;g&&g.font&&v.parseFontDeclaration(g.font,g);var b=y(h,g);return v.svgValidParentsRegEx.test(t.nodeName)?b:r(b)}},parseElements:function(t,e,i,n,r){new v.ElementsParser(t,e,i,n,r).parse()},parseStyleAttribute:function(t){var e={},i=t.getAttribute("style");return i?("string"==typeof i?o(i,e):a(i,e),e):e},parsePointsAttribute:function(t){if(!t)return null;t=t.replace(/,/g," ").trim(),t=t.split(/\s+/);var e,i,n=[];for(e=0,i=t.length;i>e;e+=2)n.push({x:parseFloat(t[e]),y:parseFloat(t[e+1])});return n},getCSSRules:function(t){var e,i,n,r=t.getElementsByTagName("style"),s={};for(e=0,i=r.length;i>e;e++){var o=r[e].textContent;o=o.replace(/\/\*[\s\S]*?\*\//g,""),""!==o.trim()&&(n=o.split("}"),n=n.filter(function(t){return t.trim()}),n.forEach(function(t){var n=t.split("{"),r={},o=n[1].trim(),a=o.split(";").filter(function(t){return t.trim()});for(e=0,i=a.length;i>e;e++){var c=a[e].split(":"),l=c[0].trim(),h=c[1].trim();r[l]=h}t=n[0].trim(),t.split(",").forEach(function(t){t=t.replace(/^svg/i,"").trim(),""!==t&&(s[t]?v.util.object.extend(s[t],r):s[t]=v.util.object.clone(r))})}))}return s},loadSVGFromURL:function(t,e,i,n){function r(t){var r=t.responseXML;return r&&r.documentElement?void v.parseSVGDocument(r.documentElement,function(t,i,n,r){e&&e(t,i,n,r)},i,n):(e&&e(null),!1)}t=t.replace(/^\n\s*/,"").trim(),new v.util.request(t,{method:"get",onComplete:r})},loadSVGFromString:function(t,e,i,n){var r=new v.window.DOMParser,s=r.parseFromString(t.trim(),"text/xml");v.parseSVGDocument(s.documentElement,function(t,i,n,r){e(t,i,n,r)},i,n)}})}("undefined"!=typeof exports?exports:this);fabric.ElementsParser=function(t,e,i,n,r,s){this.elements=t,this.callback=e,this.options=i,this.reviver=n,this.svgUid=i&&i.svgUid||0,this.parsingOptions=r,this.regexUrl=/^url\(['"]?#([^'"]+)['"]?\)/g,this.doc=s},function(t){t.parse=function(){this.instances=new Array(this.elements.length),this.numElements=this.elements.length,this.createObjects()},t.createObjects=function(){var t=this;this.elements.forEach(function(e,i){e.setAttribute("svgUid",t.svgUid),t.createObject(e,i)})},t.findTag=function(t){return fabric[fabric.util.string.capitalize(t.tagName.replace("svg:",""))]},t.createObject=function(t,e){var i=this.findTag(t);if(i&&i.fromElement)try{i.fromElement(t,this.createCallback(e,t),this.options)}catch(n){fabric.log(n)}else this.checkIfDone()},t.createCallback=function(t,e){var i=this;return function(n){var r;i.resolveGradient(n,e,"fill"),i.resolveGradient(n,e,"stroke"),n instanceof fabric.Image&&n._originalElement&&(r=n.parsePreserveAspectRatioAttribute(e)),n._removeTransformMatrix(r),i.resolveClipPath(n,e),i.reviver&&i.reviver(e,n),i.instances[t]=n,i.checkIfDone()}},t.extractPropertyDefinition=function(t,e,i){var n=t[e],r=this.regexUrl;if(r.test(n)){r.lastIndex=0;var s=r.exec(n)[1];return r.lastIndex=0,fabric[i][this.svgUid][s]}},t.resolveGradient=function(t,e,i){var n=this.extractPropertyDefinition(t,i,"gradientDefs");if(n){var r=e.getAttribute(i+"-opacity"),s=fabric.Gradient.fromElement(n,t,r,this.options);t.set(i,s)}},t.createClipPathCallback=function(t,e){return function(t){t._removeTransformMatrix(),t.fillRule=t.clipRule,e.push(t)}},t.resolveClipPath=function(t,e){var i,n,r,s,a,o,c=this.extractPropertyDefinition(t,"clipPath","clipPaths");if(c){s=[],r=fabric.util.invertTransform(t.calcTransformMatrix());for(var l=c[0].parentNode,h=e;h.parentNode&&h.getAttribute("clip-path")!==t.clipPath;)h=h.parentNode;h.parentNode.appendChild(l);for(var f=0;ft.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,i){return"undefined"==typeof i&&(i=.5),i=Math.max(Math.min(1,i),0),new e(this.x+(t.x-this.x)*i,this.y+(t.y-this.y)*i)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new e(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new e(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new e(this.x,this.y)}}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t){this.status=t,this.points=[]}var i=t.fabric||(t.fabric={});return i.Intersection?void i.warn("fabric.Intersection is already defined"):(i.Intersection=e,i.Intersection.prototype={constructor:e,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},i.Intersection.intersectLineLine=function(t,n,r,s){var a,o=(s.x-r.x)*(t.y-r.y)-(s.y-r.y)*(t.x-r.x),c=(n.x-t.x)*(t.y-r.y)-(n.y-t.y)*(t.x-r.x),l=(s.y-r.y)*(n.x-t.x)-(s.x-r.x)*(n.y-t.y);if(0!==l){var h=o/l,f=c/l;h>=0&&1>=h&&f>=0&&1>=f?(a=new e("Intersection"),a.appendPoint(new i.Point(t.x+h*(n.x-t.x),t.y+h*(n.y-t.y)))):a=new e}else a=new e(0===o||0===c?"Coincident":"Parallel");return a},i.Intersection.intersectLinePolygon=function(t,i,n){var r,s,a,o,c=new e,l=n.length;for(o=0;l>o;o++)r=n[o],s=n[(o+1)%l],a=e.intersectLineLine(t,i,r,s),c.appendPoints(a.points);return c.points.length>0&&(c.status="Intersection"),c},i.Intersection.intersectPolygonPolygon=function(t,i){var n,r=new e,s=t.length;for(n=0;s>n;n++){var a=t[n],o=t[(n+1)%s],c=e.intersectLinePolygon(a,o,i);r.appendPoints(c.points)}return r.points.length>0&&(r.status="Intersection"),r},void(i.Intersection.intersectPolygonRectangle=function(t,n,r){var s=n.min(r),a=n.max(r),o=new i.Point(a.x,s.y),c=new i.Point(s.x,a.y),l=e.intersectLinePolygon(s,o,t),h=e.intersectLinePolygon(o,a,t),f=e.intersectLinePolygon(a,c,t),u=e.intersectLinePolygon(c,s,t),d=new e;return d.appendPoints(l.points),d.appendPoints(h.points),d.appendPoints(f.points),d.appendPoints(u.points),d.points.length>0&&(d.status="Intersection"),d}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t){t?this._tryParsingColor(t):this.setSource([0,0,0,1])}function i(t,e,i){return 0>i&&(i+=1),i>1&&(i-=1),1/6>i?t+6*(e-t)*i:.5>i?e:2/3>i?t+(e-t)*(2/3-i)*6:t}var n=t.fabric||(t.fabric={});return n.Color?void n.warn("fabric.Color is already defined."):(n.Color=e,n.Color.prototype={_tryParsingColor:function(t){var i;t in e.colorNameMap&&(t=e.colorNameMap[t]),"transparent"===t&&(i=[255,255,255,0]),i||(i=e.sourceFromHex(t)),i||(i=e.sourceFromRgb(t)),i||(i=e.sourceFromHsl(t)),i||(i=[0,0,0,1]),i&&this.setSource(i)},_rgbToHsl:function(t,e,i){t/=255,e/=255,i/=255;var r,s,a,o=n.util.array.max([t,e,i]),l=n.util.array.min([t,e,i]);if(a=(o+l)/2,o===l)r=s=0;else{var c=o-l;switch(s=a>.5?c/(2-o-l):c/(o+l),o){case t:r=(e-i)/c+(i>e?6:0);break;case e:r=(i-t)/c+2;break;case i:r=(t-e)/c+4}r/=6}return[Math.round(360*r),Math.round(100*s),Math.round(100*a)]},getSource:function(){return this._source},setSource:function(t){this._source=t},toRgb:function(){var t=this.getSource();return"rgb("+t[0]+","+t[1]+","+t[2]+")"},toRgba:function(){var t=this.getSource();return"rgba("+t[0]+","+t[1]+","+t[2]+","+t[3]+")"},toHsl:function(){var t=this.getSource(),e=this._rgbToHsl(t[0],t[1],t[2]);return"hsl("+e[0]+","+e[1]+"%,"+e[2]+"%)"},toHsla:function(){var t=this.getSource(),e=this._rgbToHsl(t[0],t[1],t[2]);return"hsla("+e[0]+","+e[1]+"%,"+e[2]+"%,"+t[3]+")"},toHex:function(){var t,e,i,n=this.getSource();return t=n[0].toString(16),t=1===t.length?"0"+t:t,e=n[1].toString(16),e=1===e.length?"0"+e:e,i=n[2].toString(16),i=1===i.length?"0"+i:i,t.toUpperCase()+e.toUpperCase()+i.toUpperCase()},toHexa:function(){var t,e=this.getSource();return t=Math.round(255*e[3]),t=t.toString(16),t=1===t.length?"0"+t:t,this.toHex()+t.toUpperCase()},getAlpha:function(){return this.getSource()[3]},setAlpha:function(t){var e=this.getSource();return e[3]=t,this.setSource(e),this},toGrayscale:function(){var t=this.getSource(),e=parseInt((.3*t[0]+.59*t[1]+.11*t[2]).toFixed(0),10),i=t[3];return this.setSource([e,e,e,i]),this},toBlackWhite:function(t){var e=this.getSource(),i=(.3*e[0]+.59*e[1]+.11*e[2]).toFixed(0),n=e[3];return t=t||127,i=Number(i)i;i++)n.push(Math.round(a[i]*(1-s)+o[i]*s));return n[3]=r,this.setSource(n),this}},n.Color.reRGBa=/^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i,n.Color.reHSLa=/^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i,n.Color.reHex=/^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i,n.Color.colorNameMap={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aqua:"#00FFFF",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blue:"#0000FF",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgrey:"#A9A9A9",darkgreen:"#006400",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",fuchsia:"#FF00FF",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",gray:"#808080",grey:"#808080",green:"#008000",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgray:"#D3D3D3",lightgrey:"#D3D3D3",lightgreen:"#90EE90",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",lime:"#00FF00",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",maroon:"#800000",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",navy:"#000080",oldlace:"#FDF5E6",olive:"#808000",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",purple:"#800080",rebeccapurple:"#663399",red:"#FF0000",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",silver:"#C0C0C0",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",teal:"#008080",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",white:"#FFFFFF",whitesmoke:"#F5F5F5",yellow:"#FFFF00",yellowgreen:"#9ACD32"},n.Color.fromRgb=function(t){return e.fromSource(e.sourceFromRgb(t))},n.Color.sourceFromRgb=function(t){var i=t.match(e.reRGBa);if(i){var n=parseInt(i[1],10)/(/%$/.test(i[1])?100:1)*(/%$/.test(i[1])?255:1),r=parseInt(i[2],10)/(/%$/.test(i[2])?100:1)*(/%$/.test(i[2])?255:1),s=parseInt(i[3],10)/(/%$/.test(i[3])?100:1)*(/%$/.test(i[3])?255:1);return[parseInt(n,10),parseInt(r,10),parseInt(s,10),i[4]?parseFloat(i[4]):1]}},n.Color.fromRgba=e.fromRgb,n.Color.fromHsl=function(t){return e.fromSource(e.sourceFromHsl(t))},n.Color.sourceFromHsl=function(t){var n=t.match(e.reHSLa);if(n){var r,s,a,o=(parseFloat(n[1])%360+360)%360/360,l=parseFloat(n[2])/(/%$/.test(n[2])?100:1),c=parseFloat(n[3])/(/%$/.test(n[3])?100:1);if(0===l)r=s=a=c;else{var h=.5>=c?c*(l+1):c+l-c*l,u=2*c-h;r=i(u,h,o+1/3),s=i(u,h,o),a=i(u,h,o-1/3)}return[Math.round(255*r),Math.round(255*s),Math.round(255*a),n[4]?parseFloat(n[4]):1]}},n.Color.fromHsla=e.fromHsl,n.Color.fromHex=function(t){return e.fromSource(e.sourceFromHex(t))},n.Color.sourceFromHex=function(t){if(t.match(e.reHex)){var i=t.slice(t.indexOf("#")+1),n=3===i.length||4===i.length,r=8===i.length||4===i.length,s=n?i.charAt(0)+i.charAt(0):i.substring(0,2),a=n?i.charAt(1)+i.charAt(1):i.substring(2,4),o=n?i.charAt(2)+i.charAt(2):i.substring(4,6),l=r?n?i.charAt(3)+i.charAt(3):i.substring(6,8):"FF";return[parseInt(s,16),parseInt(a,16),parseInt(o,16),parseFloat((parseInt(l,16)/255).toFixed(2))]}},void(n.Color.fromSource=function(t){var i=new e;return i.setSource(t),i}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t,e){var i=t.angle+R(Math.atan2(e.y,e.x))+360;return Math.round(i%360/45)}function i(t,e){var i=e.transform.target,n=i.canvas,r=E.util.object.clone(e);r.target=i,n&&n.fire("object:"+t,r),i.fire(t,e)}function n(t,e){var i=e.canvas,n=i.uniScaleKey,r=t[n];return i.uniformScaling&&!r||!i.uniformScaling&&r}function r(t){return t.originX===j&&t.originY===j}function s(t,e,i){var n=t.lockScalingX,r=t.lockScalingY;return n&&r?!0:!e&&(n||r)&&i?!0:n&&"x"===e?!0:r&&"y"===e?!0:!1}function a(t,i,r){var a="not-allowed",o=n(t,r),l="";if(0!==i.x&&0===i.y?l="x":0===i.x&&0!==i.y&&(l="y"),s(r,l,o))return a;var c=e(r,i);return D[c]+"-resize"}function o(t,i,n){var r="not-allowed";if(0!==i.x&&n.lockSkewingY)return r;if(0!==i.y&&n.lockSkewingX)return r;var s=e(n,i)%4;return M[s]+"-resize"}function l(t,e,i){return t[i.canvas.altActionKey]?L.skewCursorStyleHandler(t,e,i):L.scaleCursorStyleHandler(t,e,i)}function c(t,e,i){var n=t[i.canvas.altActionKey];return 0===e.x?n?"skewX":"scaleY":0===e.y?n?"skewY":"scaleX":void 0}function h(t,e,i){return i.lockRotation?"not-allowed":e.cursorStyle}function u(t,e,i,n){return{e:t,transform:e,pointer:{x:i,y:n}}}function f(t){return function(e,i,n,r){var s=i.target,a=s.getCenterPoint(),o=s.translateToOriginPoint(a,i.originX,i.originY),l=t(e,i,n,r);return s.setPositionByOrigin(o,i.originX,i.originY),l}}function d(t,e){return function(n,r,s,a){var o=e(n,r,s,a);return o&&i(t,u(n,r,s,a)),o}}function g(t,e,i,n,r){var s=t.target,a=s.controls[t.corner],o=s.canvas.getZoom(),l=s.padding/o,c=s.toLocalPoint(new E.Point(n,r),e,i);return c.x>=l&&(c.x-=l),c.x<=-l&&(c.x+=l),c.y>=l&&(c.y-=l),c.y<=l&&(c.y+=l),c.x-=a.offsetX,c.y-=a.offsetY,c}function p(t){return t.flipX!==t.flipY}function m(t,e,i,n,r){if(0!==t[e]){var s=t._getTransformedDimensions()[n],a=r/s*t[i];t.set(i,a)}}function y(t,e,i,n){var r,s=e.target,a=s._getTransformedDimensions(0,s.skewY),o=g(e,e.originX,e.originY,i,n),l=Math.abs(2*o.x)-a.x,c=s.skewX;2>l?r=0:(r=R(Math.atan2(l/s.scaleX,a.y/s.scaleY)),e.originX===P&&e.originY===H&&(r=-r),e.originX===B&&e.originY===I&&(r=-r),p(s)&&(r=-r));var h=c!==r;if(h){var u=s._getTransformedDimensions().y;s.set("skewX",r),m(s,"skewY","scaleY","y",u)}return h}function v(t,e,i,n){var r,s=e.target,a=s._getTransformedDimensions(s.skewX,0),o=g(e,e.originX,e.originY,i,n),l=Math.abs(2*o.y)-a.y,c=s.skewY;2>l?r=0:(r=R(Math.atan2(l/s.scaleY,a.x/s.scaleX)),e.originX===P&&e.originY===H&&(r=-r),e.originX===B&&e.originY===I&&(r=-r),p(s)&&(r=-r));var h=c!==r;if(h){var u=s._getTransformedDimensions().x;s.set("skewY",r),m(s,"skewX","scaleX","x",u)}return h}function x(t,e,i,n){var r,s=e.target,a=s.skewX,o=e.originY;if(s.lockSkewingX)return!1;if(0===a){var l=g(e,j,j,i,n);r=l.x>0?P:B}else a>0&&(r=o===I?P:B),0>a&&(r=o===I?B:P),p(s)&&(r=r===P?B:P);e.originX=r;var c=d("skewing",f(y));return c(t,e,i,n)}function b(t,e,i,n){var r,s=e.target,a=s.skewY,o=e.originX;if(s.lockSkewingY)return!1;if(0===a){var l=g(e,j,j,i,n);r=l.y>0?I:H}else a>0&&(r=o===P?I:H),0>a&&(r=o===P?H:I),p(s)&&(r=r===I?H:I);e.originY=r;var c=d("skewing",f(v));return c(t,e,i,n)}function _(t,e,i,n){var r=e,s=r.target,a=s.translateToOriginPoint(s.getCenterPoint(),r.originX,r.originY);if(s.lockRotation)return!1;var o=Math.atan2(r.ey-a.y,r.ex-a.x),l=Math.atan2(n-a.y,i-a.x),c=R(l-o+r.theta),h=!0;if(s.snapAngle>0){var u=s.snapAngle,f=s.snapThreshold||u,d=Math.ceil(c/u)*u,g=Math.floor(c/u)*u;Math.abs(c-g)c&&(c=360+c),c%=360,h=s.angle!==c,s.angle=c,h}function S(t,e,i,a,o){o=o||{};var l,c,h,u,f,d,p=e.target,m=p.lockScalingX,y=p.lockScalingY,v=o.by,x=n(t,p),b=s(p,v,x),_=e.gestureScale;if(b)return!1;if(_)c=e.scaleX*_,h=e.scaleY*_;else{if(l=g(e,e.originX,e.originY,i,a),f="y"!==v?N(l.x):1,d="x"!==v?N(l.y):1,e.signX||(e.signX=f),e.signY||(e.signY=d),p.lockScalingFlip&&(e.signX!==f||e.signY!==d))return!1;if(u=p._getTransformedDimensions(),x&&!v){var S=Math.abs(l.x)+Math.abs(l.y),C=e.original,w=Math.abs(u.x*C.scaleX/p.scaleX)+Math.abs(u.y*C.scaleY/p.scaleY),T=S/w;c=C.scaleX*T,h=C.scaleY*T}else c=Math.abs(l.x*p.scaleX/u.x),h=Math.abs(l.y*p.scaleY/u.y);r(e)&&(c*=2,h*=2),e.signX!==f&&"y"!==v&&(e.originX=W[e.originX],c*=-1,e.signX=f),e.signY!==d&&"x"!==v&&(e.originY=W[e.originY],h*=-1,e.signY=d)}var F=p.scaleX,k=p.scaleY;return v?("x"===v&&p.set("scaleX",c),"y"===v&&p.set("scaleY",h)):(!m&&p.set("scaleX",c),!y&&p.set("scaleY",h)),F!==p.scaleX||k!==p.scaleY}function C(t,e,i,n){return S(t,e,i,n)}function w(t,e,i,n){return S(t,e,i,n,{by:"x"})}function T(t,e,i,n){return S(t,e,i,n,{by:"y"})}function F(t,e,i,n){return t[e.target.canvas.altActionKey]?L.skewHandlerX(t,e,i,n):L.scalingY(t,e,i,n)}function k(t,e,i,n){return t[e.target.canvas.altActionKey]?L.skewHandlerY(t,e,i,n):L.scalingX(t,e,i,n)}function O(t,e,i,n){var s=e.target,a=g(e,e.originX,e.originY,i,n),o=s.strokeWidth/(s.strokeUniform?s.scaleX:1),l=r(e)?2:1,c=s.width,h=Math.abs(a.x*l/s.scaleX)-o;return s.set("width",Math.max(h,0)),c!==h}function A(t,e,n,r){var s=e.target,a=n-e.offsetX,o=r-e.offsetY,l=!s.get("lockMovementX")&&s.left!==a,c=!s.get("lockMovementY")&&s.top!==o;return l&&s.set("left",a),c&&s.set("top",o),(l||c)&&i("moving",u(t,e,n,r)),l||c}var E=t.fabric||(t.fabric={}),D=["e","se","s","sw","w","nw","n","ne","e"],M=["ns","nesw","ew","nwse"],L={},P="left",I="top",B="right",H="bottom",j="center",W={top:H,bottom:I,left:B,right:P,center:j},R=E.util.radiansToDegrees,N=Math.sign||function(t){return(t>0)-(0>t)||+t};L.scaleCursorStyleHandler=a,L.skewCursorStyleHandler=o,L.scaleSkewCursorStyleHandler=l,L.rotationWithSnapping=d("rotating",f(_)),L.scalingEqually=d("scaling",f(C)),L.scalingX=d("scaling",f(w)),L.scalingY=d("scaling",f(T)),L.scalingYOrSkewingX=F,L.scalingXOrSkewingY=k,L.changeWidth=d("resizing",f(O)),L.skewHandlerX=x,L.skewHandlerY=b,L.dragHandler=A,L.scaleOrSkewActionName=c,L.rotationStyleHandler=h,L.fireEvent=i,L.wrapWithFixedAnchor=f,L.wrapWithFireEvent=d,L.getLocalPoint=g,E.controlsUtils=L}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t,e,i,n,r){n=n||{};var s,a=this.sizeX||n.cornerSize||r.cornerSize,o=this.sizeY||n.cornerSize||r.cornerSize,c="undefined"!=typeof n.transparentCorners?n.transparentCorners:r.transparentCorners,l=c?"stroke":"fill",h=!c&&(n.cornerStrokeColor||r.cornerStrokeColor),u=e,f=i;t.save(),t.fillStyle=n.cornerColor||r.cornerColor,t.strokeStyle=n.cornerStrokeColor||r.cornerStrokeColor,a>o?(s=a,t.scale(1,o/a),f=i*a/o):o>a?(s=o,t.scale(a/o,1),u=e*o/a):s=a,t.lineWidth=1,t.beginPath(),t.arc(u,f,s/2,0,2*Math.PI,!1),t[l](),h&&t.stroke(),t.restore()}function i(t,e,i,n,s){n=n||{};var a=this.sizeX||n.cornerSize||s.cornerSize,o=this.sizeY||n.cornerSize||s.cornerSize,c="undefined"!=typeof n.transparentCorners?n.transparentCorners:s.transparentCorners,l=c?"stroke":"fill",h=!c&&(n.cornerStrokeColor||s.cornerStrokeColor),u=a/2,f=o/2;t.save(),t.fillStyle=n.cornerColor||s.cornerColor,t.strokeStyle=n.cornerStrokeColor||s.cornerStrokeColor,t.lineWidth=1,t.translate(e,i),t.rotate(r(s.angle)),t[l+"Rect"](-u,-f,a,o),h&&t.strokeRect(-u,-f,a,o),t.restore()}var n=t.fabric||(t.fabric={}),r=n.util.degreesToRadians,s=n.controlsUtils;s.renderCircleControl=e,s.renderSquareControl=i}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t){for(var e in t)this[e]=t[e]}var i=t.fabric||(t.fabric={});i.Control=e,i.Control.prototype={visible:!0,actionName:"scale",angle:0,x:0,y:0,offsetX:0,offsetY:0,sizeX:null,sizeY:null,touchSizeX:null,touchSizeY:null,cursorStyle:"crosshair",withConnection:!1,actionHandler:function(){},mouseDownHandler:function(){},mouseUpHandler:function(){},getActionHandler:function(){return this.actionHandler},getMouseDownHandler:function(){return this.mouseDownHandler},getMouseUpHandler:function(){return this.mouseUpHandler},cursorStyleHandler:function(t,e){return e.cursorStyle},getActionName:function(t,e){return e.actionName},getVisibility:function(t,e){var i=t._controlsVisibility;return i&&"undefined"!=typeof i[e]?i[e]:this.visible},setVisibility:function(t){this.visible=t},positionHandler:function(t,e){var n=i.util.transformPoint({x:this.x*t.x+this.offsetX,y:this.y*t.y+this.offsetY},e);return n},calcCornerCoords:function(t,e,n,r,s){var a,o,c,l,h=s?this.touchSizeX:this.sizeX,u=s?this.touchSizeY:this.sizeY;if(h&&u&&h!==u){var f=Math.atan2(u,h),d=Math.sqrt(h*h+u*u)/2,g=f-i.util.degreesToRadians(t),p=Math.PI/2-f-i.util.degreesToRadians(t);a=d*i.util.cos(g),o=d*i.util.sin(g),c=d*i.util.cos(p),l=d*i.util.sin(p)}else{var m=h&&u?h:e;d=.7071067812*m;var g=i.util.degreesToRadians(45-t);a=c=d*i.util.cos(g),o=l=d*i.util.sin(g)}return{tl:{x:n-l,y:r-c},tr:{x:n+a,y:r-o},bl:{x:n-a,y:r+o},br:{x:n+l,y:r+c}}},render:function(t,e,n,r,s){switch(r=r||{},r.cornerStyle||s.cornerStyle){case"circle":i.controlsUtils.renderCircleControl.call(this,t,e,n,r,s);break;default:i.controlsUtils.renderSquareControl.call(this,t,e,n,r,s)}}}}("undefined"!=typeof exports?exports:this);!function(){function t(t,e){var i,r,n,s,o=t.getAttribute("style"),a=t.getAttribute("offset")||0;if(a=parseFloat(a)/(/%$/.test(a)?100:1),a=0>a?0:a>1?1:a,o){var l=o.split(/\s*;\s*/);for(""===l[l.length-1]&&l.pop(),s=l.length;s--;){var c=l[s].split(/\s*:\s*/),h=c[0].trim(),u=c[1].trim();"stop-color"===h?i=u:"stop-opacity"===h&&(n=u)}}return i||(i=t.getAttribute("stop-color")||"rgb(0,0,0)"),n||(n=t.getAttribute("stop-opacity")),i=new fabric.Color(i),r=i.getAlpha(),n=isNaN(parseFloat(n))?1:parseFloat(n),n*=r*e,{offset:a,color:i.toRgb(),opacity:n}}function e(t){return{x1:t.getAttribute("x1")||0,y1:t.getAttribute("y1")||0,x2:t.getAttribute("x2")||"100%",y2:t.getAttribute("y2")||0}}function i(t){return{x1:t.getAttribute("fx")||t.getAttribute("cx")||"50%",y1:t.getAttribute("fy")||t.getAttribute("cy")||"50%",r1:0,x2:t.getAttribute("cx")||"50%",y2:t.getAttribute("cy")||"50%",r2:t.getAttribute("r")||"50%"}}function r(t,e,i,r){var n,s;Object.keys(e).forEach(function(t){n=e[t],"Infinity"===n?s=1:"-Infinity"===n?s=0:(s=parseFloat(e[t],10),"string"==typeof n&&/^(\d+\.\d+)%|(\d+)%$/.test(n)&&(s*=.01,"pixels"===r&&(("x1"===t||"x2"===t||"r2"===t)&&(s*=i.viewBoxWidth||i.width),("y1"===t||"y2"===t)&&(s*=i.viewBoxHeight||i.height)))),e[t]=s})}var n=fabric.util.object.clone;fabric.Gradient=fabric.util.createClass({offsetX:0,offsetY:0,gradientTransform:null,gradientUnits:"pixels",type:"linear",initialize:function(t){t||(t={}),t.coords||(t.coords={});var e,i=this;Object.keys(t).forEach(function(e){i[e]=t[e]}),this.id?this.id+="_"+fabric.Object.__uid++:this.id=fabric.Object.__uid++,e={x1:t.coords.x1||0,y1:t.coords.y1||0,x2:t.coords.x2||0,y2:t.coords.y2||0},"radial"===this.type&&(e.r1=t.coords.r1||0,e.r2=t.coords.r2||0),this.coords=e,this.colorStops=t.colorStops.slice()},addColorStop:function(t){for(var e in t){var i=new fabric.Color(t[e]);this.colorStops.push({offset:parseFloat(e),color:i.toRgb(),opacity:i.getAlpha()})}return this},toObject:function(t){var e={type:this.type,coords:this.coords,colorStops:this.colorStops,offsetX:this.offsetX,offsetY:this.offsetY,gradientUnits:this.gradientUnits,gradientTransform:this.gradientTransform?this.gradientTransform.concat():this.gradientTransform};return fabric.util.populateWithProperties(this,e,t),e},toSVG:function(t,e){var i,r,s,o,a=n(this.coords,!0),e=e||{},l=n(this.colorStops,!0),c=a.r1>a.r2,h=this.gradientTransform?this.gradientTransform.concat():fabric.iMatrix.concat(),u=-this.offsetX,f=-this.offsetY,d=!!e.additionalTransform,g="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(l.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"===g?(u/=t.width,f/=t.height):(u+=t.width/2,f+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(u-=t.pathOffset.x,f-=t.pathOffset.y),h[4]-=u,h[5]-=f,o='id="SVGID_'+this.id+'" gradientUnits="'+g+'"',o+=' gradientTransform="'+(d?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(h)+'" ',"linear"===this.type?s=["\n']:"radial"===this.type&&(s=["\n']),"radial"===this.type){if(c)for(l=l.concat(),l.reverse(),i=0,r=l.length;r>i;i++)l[i].offset=1-l[i].offset;var p=Math.min(a.r1,a.r2);if(p>0){var m=Math.max(a.r1,a.r2),y=p/m;for(i=0,r=l.length;r>i;i++)l[i].offset+=y*(1-l[i].offset)}}for(i=0,r=l.length;r>i;i++){var v=l[i];s.push("\n')}return s.push("linear"===this.type?"\n":"\n"),s.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;r>i;i++){var s=this.colorStops[i].color,o=this.colorStops[i].opacity,a=this.colorStops[i].offset;"undefined"!=typeof o&&(s=new fabric.Color(s).setAlpha(o).toRgba()),e.addColorStop(a,s)}return e}}}),fabric.util.object.extend(fabric.Gradient,{fromElement:function(n,s,o,a){var l=parseFloat(o)/(/%$/.test(o)?100:1);l=0>l?0:l>1?1:l,isNaN(l)&&(l=1);var c,h,u,f,d=n.getElementsByTagName("stop"),g="userSpaceOnUse"===n.getAttribute("gradientUnits")?"pixels":"percentage",p=n.getAttribute("gradientTransform")||"",m=[],y=0,v=0;for("linearGradient"===n.nodeName||"LINEARGRADIENT"===n.nodeName?(c="linear",h=e(n)):(c="radial",h=i(n)),u=d.length;u--;)m.push(t(d[u],l));f=fabric.parseTransformAttribute(p),r(s,h,a,g),"pixels"===g&&(y=-s.left,v=-s.top);var x=new fabric.Gradient({id:n.getAttribute("id"),type:c,coords:h,colorStops:m,gradientUnits:g,gradientTransform:f,offsetX:y,offsetY:v});return x}})}();!function(){"use strict";var t=fabric.util.toFixed;fabric.Pattern=fabric.util.createClass({repeat:"repeat",offsetX:0,offsetY:0,crossOrigin:"",patternTransform:null,initialize:function(t,e){if(t||(t={}),this.id=fabric.Object.__uid++,this.setOptions(t),!t.source||t.source&&"string"!=typeof t.source)return void(e&&e(this));var i=this;this.source=fabric.util.createImage(),fabric.util.loadImage(t.source,function(t,r){i.source=t,e&&e(i,r)},null,this.crossOrigin)},toObject:function(e){var i,r,n=fabric.Object.NUM_FRACTION_DIGITS;return"string"==typeof this.source.src?i=this.source.src:"object"==typeof this.source&&this.source.toDataURL&&(i=this.source.toDataURL()),r={type:"pattern",source:i,repeat:this.repeat,crossOrigin:this.crossOrigin,offsetX:t(this.offsetX,n),offsetY:t(this.offsetY,n),patternTransform:this.patternTransform?this.patternTransform.concat():null},fabric.util.populateWithProperties(this,r,e),r},toSVG:function(t){var e="function"==typeof this.source?this.source():this.source,i=e.width/t.width,r=e.height/t.height,n=this.offsetX/t.width,s=this.offsetY/t.height,o="";return("repeat-x"===this.repeat||"no-repeat"===this.repeat)&&(r=1,s&&(r+=Math.abs(s))),("repeat-y"===this.repeat||"no-repeat"===this.repeat)&&(i=1,n&&(i+=Math.abs(n))),e.src?o=e.src:e.toDataURL&&(o=e.toDataURL()),'\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if("undefined"!=typeof e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}();!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.toFixed;return e.Shadow?void e.warn("fabric.Shadow is already defined."):(e.Shadow=e.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){"string"==typeof t&&(t=this._parseShadow(t));for(var i in t)this[i]=t[i];this.id=e.Object.__uid++},_parseShadow:function(t){var i=t.trim(),r=e.Shadow.reOffsetsAndBlur.exec(i)||[],n=i.replace(e.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)";return{color:n.trim(),offsetX:parseFloat(r[1],10)||0,offsetY:parseFloat(r[2],10)||0,blur:parseFloat(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var r=40,n=40,s=e.Object.NUM_FRACTION_DIGITS,o=e.util.rotateVector({x:this.offsetX,y:this.offsetY},e.util.degreesToRadians(-t.angle)),a=20,l=new e.Color(this.color);return t.width&&t.height&&(r=100*i((Math.abs(o.x)+this.blur)/t.width,s)+a,n=100*i((Math.abs(o.y)+this.blur)/t.height,s)+a),t.flipX&&(o.x*=-1),t.flipY&&(o.y*=-1),'\n \n \n \n \n \n \n \n \n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var t={},i=e.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(e){this[e]!==i[e]&&(t[e]=this[e])},this),t}}),void(e.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/))}("undefined"!=typeof exports?exports:this);!function(){"use strict";if(fabric.StaticCanvas)return void fabric.warn("fabric.StaticCanvas is already defined.");var t=fabric.util.object.extend,e=fabric.util.getElementOffset,i=fabric.util.removeFromArray,r=fabric.util.toFixed,n=fabric.util.transformPoint,s=fabric.util.invertTransform,o=fabric.util.getNodeCanvas,a=fabric.util.createCanvasElement,c=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return fabric.devicePixelRatio>1&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?Math.max(1,fabric.devicePixelRatio):1},_initRetinaScaling:function(){if(this._isRetinaScaling()){var t=fabric.devicePixelRatio;this.__initRetinaScaling(t,this.lowerCanvasEl,this.contextContainer),this.upperCanvasEl&&this.__initRetinaScaling(t,this.upperCanvasEl,this.contextTop)}},__initRetinaScaling:function(t,e,i){e.setAttribute("width",this.width*t),e.setAttribute("height",this.height*t),i.scale(t,t)},calcOffset:function(){return this._offset=e(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},__setBgOverlayImage:function(t,e,i,r){return"string"==typeof e?fabric.util.loadImage(e,function(e,n){if(e){var s=new fabric.Image(e,r);this[t]=s,s.canvas=this}i&&i(e,n)},this,r&&r.crossOrigin):(r&&e.setOptions(r),this[t]=e,e&&(e.canvas=this),i&&i(e,!1)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(){var t=a();if(!t)throw c;if(t.style||(t.style={}),"undefined"==typeof t.getContext)throw c;return t},_initOptions:function(t){var e=this.lowerCanvasEl;this._setOptions(t),this.width=this.width||parseInt(e.width,10)||0,this.height=this.height||parseInt(e.height,10)||0,this.lowerCanvasEl.style&&(e.width=this.width,e.height=this.height,e.style.width=this.width+"px",e.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){this.lowerCanvasEl=t&&t.getContext?t:fabric.util.getById(t)||this._createCanvasElement(),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this._originalCanvasStyle=this.lowerCanvasEl.style,this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;e=e||{};for(var r in t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px",this.hasLostContext=!0),e.backstoreOnly||this._setCssDimension(r,i);return this._isCurrentlyDrawing&&this.freeDrawingBrush&&this.freeDrawingBrush._setBrushStyles(this.contextTop),this._initRetinaScaling(),this.calcOffset(),e.cssOnly||this.requestRenderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i,r,n=this._activeObject,s=this.backgroundImage,o=this.overlayImage;for(this.viewportTransform=t,i=0,r=this._objects.length;r>i;i++)e=this._objects[i],e.group||e.setCoords(!0);return n&&n.setCoords(),s&&s.setCoords(!0),o&&o.setCoords(!0),this.calcViewportBoundaries(),this.renderOnAddRemove&&this.requestRenderAll(),this},zoomToPoint:function(t,e){var i=t,r=this.viewportTransform.slice(0);t=n(t,s(this.viewportTransform)),r[0]=e,r[3]=e;var o=n(t,r);return r[4]+=i.x-o.x,r[5]+=i.y-o.y,this.setViewportTransform(r)},setZoom:function(t){return this.zoomToPoint(new fabric.Point(0,0),t),this},absolutePan:function(t){var e=this.viewportTransform.slice(0);return e[4]=-t.x,e[5]=-t.y,this.setViewportTransform(e)},relativePan:function(t){return this.absolutePan(new fabric.Point(-t.x-this.viewportTransform[4],-t.y-this.viewportTransform[5]))},getElement:function(){return this.lowerCanvasEl},_onObjectAdded:function(t){this.stateful&&t.setupState(),t._set("canvas",this),t.setCoords(),this.fire("object:added",{target:t}),t.fire("added")},_onObjectRemoved:function(t){this.fire("object:removed",{target:t}),t.fire("removed"),delete t.canvas},clearContext:function(t){return t.clearRect(0,0,this.width,this.height),this},getContext:function(){return this.contextContainer},clear:function(){return this.remove.apply(this,this.getObjects()),this.backgroundImage=null,this.overlayImage=null,this.backgroundColor="",this.overlayColor="",this._hasITextHandlers&&(this.off("mouse:up",this._mouseUpITextHandler),this._iTextInstances=null,this._hasITextHandlers=!1),this.clearContext(this.contextContainer),this.fire("canvas:cleared"),this.renderOnAddRemove&&this.requestRenderAll(),this},renderAll:function(){var t=this.contextContainer;return this.renderCanvas(t,this._objects),this},renderAndReset:function(){this.isRendering=0,this.renderAll()},requestRenderAll:function(){return this.isRendering||(this.isRendering=fabric.util.requestAnimFrame(this.renderAndResetBound)),this},calcViewportBoundaries:function(){var t={},e=this.width,i=this.height,r=s(this.viewportTransform);return t.tl=n({x:0,y:0},r),t.br=n({x:e,y:i},r),t.tr=new fabric.Point(t.br.x,t.tl.y),t.bl=new fabric.Point(t.tl.x,t.br.y),this.vptCoords=t,t},cancelRequestedRender:function(){this.isRendering&&(fabric.util.cancelAnimFrame(this.isRendering),this.isRendering=0)},renderCanvas:function(t,e){var i=this.viewportTransform,r=this.clipPath;this.cancelRequestedRender(),this.calcViewportBoundaries(),this.clearContext(t),fabric.util.setImageSmoothing(t,this.imageSmoothingEnabled),this.fire("before:render",{ctx:t}),this._renderBackground(t),t.save(),t.transform(i[0],i[1],i[2],i[3],i[4],i[5]),this._renderObjects(t,e),t.restore(),!this.controlsAboveOverlay&&this.interactive&&this.drawControls(t),r&&(r.canvas=this,r.shouldCache(),r._transformDone=!0,r.renderCache({forClipping:!0}),this.drawClipPathOnCanvas(t)),this._renderOverlay(t),this.controlsAboveOverlay&&this.interactive&&this.drawControls(t),this.fire("after:render",{ctx:t})},drawClipPathOnCanvas:function(t){var e=this.viewportTransform,i=this.clipPath;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5]),t.globalCompositeOperation="destination-in",i.transform(t),t.scale(1/i.zoomX,1/i.zoomY),t.drawImage(i._cacheCanvas,-i.cacheTranslationX,-i.cacheTranslationY),t.restore()},_renderObjects:function(t,e){var i,r;for(i=0,r=e.length;r>i;++i)e[i]&&e[i].render(t)},_renderBackgroundOrOverlay:function(t,e){var i=this[e+"Color"],r=this[e+"Image"],n=this.viewportTransform,s=this[e+"Vpt"];if(i||r){if(i){t.save(),t.beginPath(),t.moveTo(0,0),t.lineTo(this.width,0),t.lineTo(this.width,this.height),t.lineTo(0,this.height),t.closePath(),t.fillStyle=i.toLive?i.toLive(t,this):i,s&&t.transform(n[0],n[1],n[2],n[3],n[4],n[5]),t.transform(1,0,0,1,i.offsetX||0,i.offsetY||0);var o=i.gradientTransform||i.patternTransform;o&&t.transform(o[0],o[1],o[2],o[3],o[4],o[5]),t.fill(),t.restore()}r&&(t.save(),s&&t.transform(n[0],n[1],n[2],n[3],n[4],n[5]),r.render(t),t.restore())}},_renderBackground:function(t){this._renderBackgroundOrOverlay(t,"background")},_renderOverlay:function(t){this._renderBackgroundOrOverlay(t,"overlay")},getCenter:function(){return{top:this.height/2,left:this.width/2}},getCenterPoint:function(){return new fabric.Point(this.width/2,this.height/2)},centerObjectH:function(t){return this._centerObject(t,new fabric.Point(this.getCenterPoint().x,t.getCenterPoint().y))},centerObjectV:function(t){return this._centerObject(t,new fabric.Point(t.getCenterPoint().x,this.getCenterPoint().y))},centerObject:function(t){var e=this.getCenterPoint();return this._centerObject(t,e)},viewportCenterObject:function(t){var e=this.getVpCenter();return this._centerObject(t,e)},viewportCenterObjectH:function(t){var e=this.getVpCenter();return this._centerObject(t,new fabric.Point(e.x,t.getCenterPoint().y)),this},viewportCenterObjectV:function(t){var e=this.getVpCenter();return this._centerObject(t,new fabric.Point(t.getCenterPoint().x,e.y))},getVpCenter:function(){var t=this.getCenterPoint(),e=s(this.viewportTransform);return n(t,e)},_centerObject:function(t,e){return t.setPositionByOrigin(e,"center","center"),t.setCoords(),this.renderOnAddRemove&&this.requestRenderAll(),this},toDatalessJSON:function(t){return this.toDatalessObject(t)},toObject:function(t){return this._toObjectMethod("toObject",t)},toDatalessObject:function(t){return this._toObjectMethod("toDatalessObject",t)},_toObjectMethod:function(e,i){var r=this.clipPath,n={version:fabric.version,objects:this._toObjects(e,i)};return r&&!r.excludeFromExport&&(n.clipPath=this._toObject(this.clipPath,e,i)),t(n,this.__serializeBgOverlay(e,i)),fabric.util.populateWithProperties(this,n,i),n},_toObjects:function(t,e){return this._objects.filter(function(t){return!t.excludeFromExport}).map(function(i){return this._toObject(i,t,e)},this)},_toObject:function(t,e,i){var r;this.includeDefaultValues||(r=t.includeDefaultValues,t.includeDefaultValues=!1);var n=t[e](i);return this.includeDefaultValues||(t.includeDefaultValues=r),n},__serializeBgOverlay:function(t,e){var i={},r=this.backgroundImage,n=this.overlayImage,s=this.backgroundColor,o=this.overlayColor;return s&&s.toObject?s.excludeFromExport||(i.background=s.toObject(e)):s&&(i.background=s),o&&o.toObject?o.excludeFromExport||(i.overlay=o.toObject(e)):o&&(i.overlay=o),r&&!r.excludeFromExport&&(i.backgroundImage=this._toObject(r,t,e)),n&&!n.excludeFromExport&&(i.overlayImage=this._toObject(n,t,e)),i},svgViewportTransformation:!0,toSVG:function(t,e){t||(t={}),t.reviver=e;var i=[];return this._setSVGPreamble(i,t),this._setSVGHeader(i,t),this.clipPath&&i.push('\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,n=e.width||this.width,s=e.height||this.height,o='viewBox="0 0 '+this.width+" "+this.height+'" ',a=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?o='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,o='viewBox="'+r(-i[4]/i[0],a)+" "+r(-i[5]/i[3],a)+" "+r(this.width/i[0],a)+" "+r(this.height/i[3],a)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+fabric.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var t=this,e=["background","overlay"].map(function(e){var i=t[e+"Color"];if(i&&i.toLive){var r=t[e+"Vpt"],n=t.viewportTransform,s={width:t.width/(r?n[0]:1),height:t.height/(r?n[3]:1)};return i.toSVG(s,{additionalTransform:r?fabric.util.matrixToSVG(n):""})}});return e.join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,s,o,a,c,l="",h={},u=fabric.fontPaths,f=[];for(this._objects.forEach(function g(t){f.push(t),t._objects&&t._objects.forEach(g)}),a=0,c=f.length;c>a;a++)if(t=f[a],e=t.fontFamily,-1!==t.type.indexOf("text")&&!h[e]&&u[e]&&(h[e]=!0,t.styles)){i=t.styles;for(n in i){r=i[n];for(o in r)s=r[o],e=s.fontFamily,!h[e]&&u[e]&&(h[e]=!0)}}for(var d in h)l+=[" @font-face {\n"," font-family: '",d,"';\n"," src: url('",u[d],"');\n"," }\n"].join("");return l&&(l=[' \n"].join("")),l},_setSVGObjects:function(t,e){var i,r,n,s=this._objects;for(r=0,n=s.length;n>r;r++)i=s[r],i.excludeFromExport||this._setSVGObject(t,i,e)},_setSVGObject:function(t,e,i){t.push(e.toSVG(i))},_setSVGBgOverlayImage:function(t,e,i){this[e]&&!this[e].excludeFromExport&&this[e].toSVG&&t.push(this[e].toSVG(i))},_setSVGBgOverlayColor:function(t,e){var i=this[e+"Color"],r=this.viewportTransform,n=this.width,s=this.height;if(i)if(i.toLive){var o=i.repeat,a=fabric.util.invertTransform(r),c=this[e+"Vpt"],l=c?fabric.util.matrixToSVG(a):"";t.push('\n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,r,n,s=this._activeObject;if(t===s&&"activeSelection"===t.type)for(n=s._objects,e=n.length;e--;)r=n[e],i(this._objects,r),this._objects.unshift(r);else i(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,r,n,s=this._activeObject;if(t===s&&"activeSelection"===t.type)for(n=s._objects,e=0;e0+l&&(o=s-1,i(this._objects,n),this._objects.splice(o,0,n)),l++;else s=this._objects.indexOf(t),0!==s&&(o=this._findNewLowerIndex(t,s,e),i(this._objects,t),this._objects.splice(o,0,t));return this.renderOnAddRemove&&this.requestRenderAll(),this},_findNewLowerIndex:function(t,e,i){var r,n;if(i)for(r=e,n=e-1;n>=0;--n){var s=t.intersectsWithObject(this._objects[n])||t.isContainedWithinObject(this._objects[n])||this._objects[n].isContainedWithinObject(t);if(s){r=n;break}}else r=e-1;return r},bringForward:function(t,e){if(!t)return this;var r,n,s,o,a,c=this._activeObject,l=0;if(t===c&&"activeSelection"===t.type)for(a=c._objects,r=a.length;r--;)n=a[r],s=this._objects.indexOf(n),sn;++n){var o=t.intersectsWithObject(this._objects[n])||t.isContainedWithinObject(this._objects[n])||this._objects[n].isContainedWithinObject(t);if(o){r=n;break}}else r=e+1;return r},moveTo:function(t,e){return i(this._objects,t),this._objects.splice(e,0,t),this.renderOnAddRemove&&this.requestRenderAll()},dispose:function(){return this.isRendering&&(fabric.util.cancelAnimFrame(this.isRendering),this.isRendering=0),this.forEachObject(function(t){t.dispose&&t.dispose()}),this._objects=[],this.backgroundImage&&this.backgroundImage.dispose&&this.backgroundImage.dispose(),this.backgroundImage=null,this.overlayImage&&this.overlayImage.dispose&&this.overlayImage.dispose(),this.overlayImage=null,this._iTextInstances=null,this.contextContainer=null,this.lowerCanvasEl.classList.remove("lower-canvas"),fabric.util.setStyle(this.lowerCanvasEl,this._originalCanvasStyle),delete this._originalCanvasStyle,this.lowerCanvasEl.setAttribute("width",this.width),this.lowerCanvasEl.setAttribute("height",this.height),fabric.util.cleanUpJsdomNode(this.lowerCanvasEl),this.lowerCanvasEl=void 0,this},toString:function(){return"#"}}),t(fabric.StaticCanvas.prototype,fabric.Observable),t(fabric.StaticCanvas.prototype,fabric.Collection),t(fabric.StaticCanvas.prototype,fabric.DataURLExporter),t(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=a();if(!e||!e.getContext)return null;var i=e.getContext("2d");if(!i)return null;switch(t){case"setLineDash":return"undefined"!=typeof i.setLineDash;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject,fabric.isLikelyNode&&(fabric.StaticCanvas.prototype.createPNGStream=function(){var t=o(this.lowerCanvasEl);return t&&t.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){var e=o(this.lowerCanvasEl);return e&&e.createJPEGStream(t)})}();fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(t){t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){if(this.shadow){var t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom();t&&t._isRetinaScaling()&&(r*=fabric.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r}},needsFullRender:function(){var t=new fabric.Color(this.color);return t.getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}});!function(){fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{decimate:.4,drawStraightLine:!1,straightLineKey:"shiftKey",initialize:function(t){this.canvas=t,this._points=[]},needsFullRender:function(){return this.callSuper("needsFullRender")||this._hasStraightLine},_drawSegment:function(t,e,i){var r=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,r.x,r.y),r},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){if(this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],(this.limitedToCanvasSize!==!0||!this._isOutSideCanvas(t))&&this._captureDrawingPath(t)&&this._points.length>1))if(this.needsFullRender())this.canvas.clearContext(this.canvas.contextTop),this._render();else{var i=this._points,r=i.length,n=this.canvas.contextTop;this._saveAndTransform(n),this.oldEnd&&(n.beginPath(),n.moveTo(this.oldEnd.x,this.oldEnd.y)),this.oldEnd=this._drawSegment(n,i[r-2],i[r-1],!0),n.stroke(),n.restore()}},onMouseUp:function(t){return this.canvas._isMainEvent(t.e)?(this.drawStraightLine=!1,this.oldEnd=void 0,this._finalizeAndAddPath(),!1):!0},_prepareForDrawing:function(t){var e=new fabric.Point(t.x,t.y);this._reset(),this._addPoint(e),this.canvas.contextTop.moveTo(e.x,e.y)},_addPoint:function(t){return this._points.length>1&&t.eq(this._points[this._points.length-1])?!1:(this.drawStraightLine&&this._points.length>1&&(this._hasStraightLine=!0,this._points.pop()),this._points.push(t),!0)},_reset:function(){this._points=[],this._setBrushStyles(this.canvas.contextTop),this._setShadow(),this._hasStraightLine=!1},_captureDrawingPath:function(t){var e=new fabric.Point(t.x,t.y);return this._addPoint(e)},_render:function(t){var e,i,r=this._points[0],n=this._points[1];if(t=t||this.canvas.contextTop,this._saveAndTransform(t),t.beginPath(),2===this._points.length&&r.x===n.x&&r.y===n.y){var s=this.width/1e3;r=new fabric.Point(r.x,r.y),n=new fabric.Point(n.x,n.y),r.x-=s,n.x+=s}for(t.moveTo(r.x,r.y),e=1,i=this._points.length;i>e;e++)this._drawSegment(t,r,n),r=this._points[e],n=this._points[e+1];t.lineTo(r.x,r.y),t.stroke(),t.restore()},convertPointsToSVGPath:function(t){var e=this.width/1e3;return fabric.util.getSmoothPathFromPoints(t,e)},_isEmptySVGPath:function(t){var e=fabric.util.joinPath(t);return"M 0 0 Q 0 0 0 0 L 0 0"===e},createPath:function(t){var e=new fabric.Path(t,{fill:null,stroke:this.color,strokeWidth:this.width,strokeLineCap:this.strokeLineCap,strokeMiterLimit:this.strokeMiterLimit,strokeLineJoin:this.strokeLineJoin,strokeDashArray:this.strokeDashArray});return this.shadow&&(this.shadow.affectStroke=!0,e.shadow=new fabric.Shadow(this.shadow)),e},decimatePoints:function(t,e){if(t.length<=2)return t;var i,r,n=this.canvas.getZoom(),s=Math.pow(e/n,2),o=t.length-1,a=t[0],c=[a];for(i=1;o-1>i;i++)r=Math.pow(a.x-t[i].x,2)+Math.pow(a.y-t[i].y,2),r>=s&&(a=t[i],c.push(a));return c.push(t[o]),c},_finalizeAndAddPath:function(){var t=this.canvas.contextTop;t.closePath(),this.decimate&&(this._points=this.decimatePoints(this._points,this.decimate));var e=this.convertPointsToSVGPath(this._points);if(this._isEmptySVGPath(e))return void this.canvas.requestRenderAll();var i=this.createPath(e);this.canvas.clearContext(this.canvas.contextTop),this.canvas.fire("before:path:created",{path:i}),this.canvas.add(i),this.canvas.requestRenderAll(),i.setCoords(),this._resetShadow(),this.canvas.fire("path:created",{path:i})}})}();fabric.CircleBrush=fabric.util.createClass(fabric.BaseBrush,{width:10,initialize:function(t){this.canvas=t,this.points=[]},drawDot:function(t){var e=this.addPoint(t),i=this.canvas.contextTop;this._saveAndTransform(i),this.dot(i,e),i.restore()},dot:function(t,e){t.fillStyle=e.fill,t.beginPath(),t.arc(e.x,e.y,e.radius,0,2*Math.PI,!1),t.closePath(),t.fill()},onMouseDown:function(t){this.points.length=0,this.canvas.clearContext(this.canvas.contextTop),this._setShadow(),this.drawDot(t)},_render:function(){var t,e,i=this.canvas.contextTop,r=this.points;for(this._saveAndTransform(i),t=0,e=r.length;e>t;t++)this.dot(i,r[t]);i.restore()},onMouseMove:function(t){this.limitedToCanvasSize===!0&&this._isOutSideCanvas(t)||(this.needsFullRender()?(this.canvas.clearContext(this.canvas.contextTop),this.addPoint(t),this._render()):this.drawDot(t))},onMouseUp:function(){var t,e,i=this.canvas.renderOnAddRemove;this.canvas.renderOnAddRemove=!1;var r=[];for(t=0,e=this.points.length;e>t;t++){var n=this.points[t],s=new fabric.Circle({radius:n.radius,left:n.x,top:n.y,originX:"center",originY:"center",fill:n.fill});this.shadow&&(s.shadow=new fabric.Shadow(this.shadow)),r.push(s)}var o=new fabric.Group(r);o.canvas=this.canvas,this.canvas.fire("before:path:created",{path:o}),this.canvas.add(o),this.canvas.fire("path:created",{path:o}),this.canvas.clearContext(this.canvas.contextTop),this._resetShadow(),this.canvas.renderOnAddRemove=i,this.canvas.requestRenderAll()},addPoint:function(t){var e=new fabric.Point(t.x,t.y),i=fabric.util.getRandomInt(Math.max(0,this.width-20),this.width+20)/2,r=new fabric.Color(this.color).setAlpha(fabric.util.getRandomInt(0,100)/100).toRgba();return e.radius=i,e.fill=r,this.points.push(e),e}});fabric.SprayBrush=fabric.util.createClass(fabric.BaseBrush,{width:10,density:20,dotWidth:1,dotWidthVariance:1,randomOpacity:!1,optimizeOverlapping:!0,initialize:function(t){this.canvas=t,this.sprayChunks=[]},onMouseDown:function(t){this.sprayChunks.length=0,this.canvas.clearContext(this.canvas.contextTop),this._setShadow(),this.addSprayChunk(t),this.render(this.sprayChunkPoints)},onMouseMove:function(t){this.limitedToCanvasSize===!0&&this._isOutSideCanvas(t)||(this.addSprayChunk(t),this.render(this.sprayChunkPoints))},onMouseUp:function(){var t=this.canvas.renderOnAddRemove;this.canvas.renderOnAddRemove=!1;for(var e=[],i=0,r=this.sprayChunks.length;r>i;i++)for(var n=this.sprayChunks[i],o=0,s=n.length;s>o;o++){var a=new fabric.Rect({width:n[o].width,height:n[o].width,left:n[o].x+1,top:n[o].y+1,originX:"center",originY:"center",fill:this.color});e.push(a)}this.optimizeOverlapping&&(e=this._getOptimizedRects(e));var c=new fabric.Group(e);this.shadow&&c.set("shadow",new fabric.Shadow(this.shadow)),this.canvas.fire("before:path:created",{path:c}),this.canvas.add(c),this.canvas.fire("path:created",{path:c}),this.canvas.clearContext(this.canvas.contextTop),this._resetShadow(),this.canvas.renderOnAddRemove=t,this.canvas.requestRenderAll()},_getOptimizedRects:function(t){var e,i,r,n={};for(i=0,r=t.length;r>i;i++)e=t[i].left+""+t[i].top,n[e]||(n[e]=t[i]);var o=[];for(e in n)o.push(n[e]);return o},render:function(t){var e,i,r=this.canvas.contextTop;for(r.fillStyle=this.color,this._saveAndTransform(r),e=0,i=t.length;i>e;e++){var n=t[e];"undefined"!=typeof n.opacity&&(r.globalAlpha=n.opacity),r.fillRect(n.x,n.y,n.width,n.width)}r.restore()},_render:function(){var t,e,i=this.canvas.contextTop;for(i.fillStyle=this.color,this._saveAndTransform(i),t=0,e=this.sprayChunks.length;e>t;t++)this.render(this.sprayChunks[t]);i.restore()},addSprayChunk:function(t){this.sprayChunkPoints=[];var e,i,r,n,o=this.width/2;for(n=0;n0&&!this.preserveObjectStacking){e=[],i=[];for(var n=0,o=this._objects.length;o>n;n++)t=this._objects[n],-1===r.indexOf(t)?e.push(t):i.push(t);r.length>1&&(this._activeObject._objects=i),e.push.apply(e,i)}else e=this._objects;return e},renderAll:function(){!this.contextTopDirty||this._groupSelector||this.isDrawingMode||(this.clearContext(this.contextTop),this.contextTopDirty=!1),this.hasLostContext&&(this.renderTopLayer(this.contextTop),this.hasLostContext=!1);var t=this.contextContainer;return this.renderCanvas(t,this._chooseObjectsToRender()),this},renderTopLayer:function(t){t.save(),this.isDrawingMode&&this._isCurrentlyDrawing&&(this.freeDrawingBrush&&this.freeDrawingBrush._render(),this.contextTopDirty=!0),this.selection&&this._groupSelector&&(this._drawSelection(t),this.contextTopDirty=!0),t.restore()},renderTop:function(){var t=this.contextTop;return this.clearContext(t),this.renderTopLayer(t),this.fire("after:render"),this},_normalizePointer:function(t,e){var i=t.calcTransformMatrix(),r=fabric.util.invertTransform(i),n=this.restorePointerVpt(e);return fabric.util.transformPoint(n,r)},isTargetTransparent:function(t,e,i){if(t.shouldCache()&&t._cacheCanvas&&t!==this._activeObject){var r=this._normalizePointer(t,{x:e,y:i}),n=Math.max(t.cacheTranslationX+r.x*t.zoomX,0),o=Math.max(t.cacheTranslationY+r.y*t.zoomY,0),s=fabric.util.isTransparent(t._cacheContext,Math.round(n),Math.round(o),this.targetFindTolerance);return s}var a=this.contextCache,c=t.selectionBackgroundColor,l=this.viewportTransform;t.selectionBackgroundColor="",this.clearContext(a),a.save(),a.transform(l[0],l[1],l[2],l[3],l[4],l[5]),t.render(a),a.restore(),t.selectionBackgroundColor=c;var s=fabric.util.isTransparent(a,e,i,this.targetFindTolerance);return s},_isSelectionKeyPressed:function(t){var e=!1;return e=Array.isArray(this.selectionKey)?!!this.selectionKey.find(function(e){return t[e]===!0}):t[this.selectionKey]},_shouldClearSelection:function(t,e){var i=this.getActiveObjects(),r=this._activeObject;return!e||e&&r&&i.length>1&&-1===i.indexOf(e)&&r!==e&&!this._isSelectionKeyPressed(t)||e&&!e.evented||e&&!e.selectable&&r&&r!==e},_shouldCenterTransform:function(t,e,i){if(t){var r;return"scale"===e||"scaleX"===e||"scaleY"===e||"resizing"===e?r=this.centeredScaling||t.centeredScaling:"rotate"===e&&(r=this.centeredRotation||t.centeredRotation),r?!i:i}},_getOriginFromCorner:function(t,e){var i={x:t.originX,y:t.originY};return"ml"===e||"tl"===e||"bl"===e?i.x="right":("mr"===e||"tr"===e||"br"===e)&&(i.x="left"),"tl"===e||"mt"===e||"tr"===e?i.y="bottom":("bl"===e||"mb"===e||"br"===e)&&(i.y="top"),i},_getActionFromCorner:function(t,e,i,r){if(!e||!t)return"drag";var n=r.controls[e];return n.getActionName(i,n,r)},_setupCurrentTransform:function(t,i,r){if(i){var n=this.getPointer(t),o=i.__corner,s=i.controls[o],a=r&&o?s.getActionHandler(t,i,s):fabric.controlsUtils.dragHandler,c=this._getActionFromCorner(r,o,t,i),l=this._getOriginFromCorner(i,o),h=t[this.centeredKey],u={target:i,action:c,actionHandler:a,corner:o,scaleX:i.scaleX,scaleY:i.scaleY,skewX:i.skewX,skewY:i.skewY,offsetX:n.x-i.left,offsetY:n.y-i.top,originX:l.x,originY:l.y,ex:n.x,ey:n.y,lastX:n.x,lastY:n.y,theta:e(i.angle),width:i.width*i.scaleX,shiftKey:t.shiftKey,altKey:h,original:fabric.util.saveObjectTransform(i)};this._shouldCenterTransform(i,c,h)&&(u.originX="center",u.originY="center"),u.original.originX=l.x,u.original.originY=l.y,this._currentTransform=u,this._beforeTransform(t)}},setCursor:function(t){this.upperCanvasEl.style.cursor=t},_drawSelection:function(t){var e=this._groupSelector,i=new fabric.Point(e.ex,e.ey),r=fabric.util.transformPoint(i,this.viewportTransform),n=new fabric.Point(e.ex+e.left,e.ey+e.top),o=fabric.util.transformPoint(n,this.viewportTransform),s=Math.min(r.x,o.x),a=Math.min(r.y,o.y),c=Math.max(r.x,o.x),l=Math.max(r.y,o.y),h=this.selectionLineWidth/2;this.selectionColor&&(t.fillStyle=this.selectionColor,t.fillRect(s,a,c-s,l-a)),this.selectionLineWidth&&this.selectionBorderColor&&(t.lineWidth=this.selectionLineWidth,t.strokeStyle=this.selectionBorderColor,s+=h,a+=h,c-=h,l-=h,fabric.Object.prototype._setLineDash.call(this,t,this.selectionDashArray),t.strokeRect(s,a,c-s,l-a))},findTarget:function(t,e){if(!this.skipTargetFind){var r,n,o=!0,s=this.getPointer(t,o),a=this._activeObject,c=this.getActiveObjects(),l=i(t),h=c.length>1&&!e||1===c.length;if(this.targets=[],h&&a._findTargetCorner(s,l))return a;if(c.length>1&&!e&&a===this._searchPossibleTargets([a],s))return a;if(1===c.length&&a===this._searchPossibleTargets([a],s)){if(!this.preserveObjectStacking)return a;r=a,n=this.targets,this.targets=[]}var u=this._searchPossibleTargets(this._objects,s);return t[this.altSelectionKey]&&u&&r&&u!==r&&(u=r,this.targets=n),u}},_checkTarget:function(t,e,i){if(e&&e.visible&&e.evented&&e.containsPoint(t)){if(!this.perPixelTargetFind&&!e.perPixelTargetFind||e.isEditing)return!0;var r=this.isTargetTransparent(e,i.x,i.y);if(!r)return!0}},_searchPossibleTargets:function(t,e){for(var i,r,n=t.length;n--;){var o=t[n],s=o.group?this._normalizePointer(o.group,e):e;if(this._checkTarget(s,o,e)){i=t[n],i.subTargetCheck&&i instanceof fabric.Group&&(r=this._searchPossibleTargets(i._objects,e),r&&this.targets.push(r));break}}return i},restorePointerVpt:function(t){return fabric.util.transformPoint(t,fabric.util.invertTransform(this.viewportTransform))},getPointer:function(e,i){if(this._absolutePointer&&!i)return this._absolutePointer;if(this._pointer&&i)return this._pointer;var r,n=t(e),o=this.upperCanvasEl,s=o.getBoundingClientRect(),a=s.width||0,c=s.height||0;a&&c||("top"in s&&"bottom"in s&&(c=Math.abs(s.top-s.bottom)),"right"in s&&"left"in s&&(a=Math.abs(s.right-s.left))),this.calcOffset(),n.x=n.x-this._offset.left,n.y=n.y-this._offset.top,i||(n=this.restorePointerVpt(n));var l=this.getRetinaScaling();return 1!==l&&(n.x/=l,n.y/=l),r=0===a||0===c?{width:1,height:1}:{width:o.width/a,height:o.height/c},{x:n.x*r.width,y:n.y*r.height}},_createUpperCanvas:function(){var t=this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/,""),e=this.lowerCanvasEl,i=this.upperCanvasEl;i?i.className="":(i=this._createCanvasElement(),this.upperCanvasEl=i),fabric.util.addClass(i,"upper-canvas "+t),this.wrapperEl.appendChild(i),this._copyCanvasStyle(e,i),this._applyCanvasStyle(i),this.contextTop=i.getContext("2d")},getTopContext:function(){return this.contextTop},_createCacheCanvas:function(){this.cacheCanvasEl=this._createCanvasElement(),this.cacheCanvasEl.setAttribute("width",this.width),this.cacheCanvasEl.setAttribute("height",this.height),this.contextCache=this.cacheCanvasEl.getContext("2d")},_initWrapperElement:function(){this.wrapperEl=fabric.util.wrapElement(this.lowerCanvasEl,"div",{"class":this.containerClass}),fabric.util.setStyle(this.wrapperEl,{width:this.width+"px",height:this.height+"px",position:"relative"}),fabric.util.makeElementUnselectable(this.wrapperEl)},_applyCanvasStyle:function(t){var e=this.width||t.width,i=this.height||t.height;fabric.util.setStyle(t,{position:"absolute",width:e+"px",height:i+"px",left:0,top:0,"touch-action":this.allowTouchScrolling?"manipulation":"none","-ms-touch-action":this.allowTouchScrolling?"manipulation":"none"}),t.width=e,t.height=i,fabric.util.makeElementUnselectable(t)},_copyCanvasStyle:function(t,e){e.style.cssText=t.style.cssText},getSelectionContext:function(){return this.contextTop},getSelectionElement:function(){return this.upperCanvasEl},getActiveObject:function(){return this._activeObject},getActiveObjects:function(){var t=this._activeObject;return t?"activeSelection"===t.type&&t._objects?t._objects.slice(0):[t]:[]},_onObjectRemoved:function(t){t===this._activeObject&&(this.fire("before:selection:cleared",{target:t}),this._discardActiveObject(),this.fire("selection:cleared",{target:t}),t.fire("deselected")),t===this._hoveredTarget&&(this._hoveredTarget=null,this._hoveredTargets=[]),this.callSuper("_onObjectRemoved",t)},_fireSelectionEvents:function(t,e){var i=!1,r=this.getActiveObjects(),n=[],o=[];t.forEach(function(t){-1===r.indexOf(t)&&(i=!0,t.fire("deselected",{e:e,target:t}),o.push(t))}),r.forEach(function(r){-1===t.indexOf(r)&&(i=!0,r.fire("selected",{e:e,target:r}),n.push(r))}),t.length>0&&r.length>0?i&&this.fire("selection:updated",{e:e,selected:n,deselected:o}):r.length>0?this.fire("selection:created",{e:e,selected:n}):t.length>0&&this.fire("selection:cleared",{e:e,deselected:o})},setActiveObject:function(t,e){var i=this.getActiveObjects();return this._setActiveObject(t,e),this._fireSelectionEvents(i,e),this},_setActiveObject:function(t,e){return this._activeObject===t?!1:this._discardActiveObject(e,t)?t.onSelect({e:e})?!1:(this._activeObject=t,!0):!1},_discardActiveObject:function(t,e){var i=this._activeObject;if(i){if(i.onDeselect({e:t,object:e}))return!1;this._activeObject=null}return!0},discardActiveObject:function(t){var e=this.getActiveObjects(),i=this.getActiveObject();return e.length&&this.fire("before:selection:cleared",{target:i,e:t}),this._discardActiveObject(t),this._fireSelectionEvents(e,t),this},dispose:function(){var t=this.wrapperEl;return this.removeListeners(),t.removeChild(this.upperCanvasEl),t.removeChild(this.lowerCanvasEl),this.contextCache=null,this.contextTop=null,["upperCanvasEl","cacheCanvasEl"].forEach(function(t){fabric.util.cleanUpJsdomNode(this[t]),this[t]=void 0}.bind(this)),t.parentNode&&t.parentNode.replaceChild(this.lowerCanvasEl,this.wrapperEl),delete this.wrapperEl,fabric.StaticCanvas.prototype.dispose.call(this),this},clear:function(){return this.discardActiveObject(),this.clearContext(this.contextTop),this.callSuper("clear")},drawControls:function(t){var e=this._activeObject;e&&e._renderControls(t)},_toObject:function(t,e,i){var r=this._realizeGroupTransformOnObject(t),n=this.callSuper("_toObject",t,e,i);return this._unwindGroupTransformOnObject(t,r),n},_realizeGroupTransformOnObject:function(t){if(t.group&&"activeSelection"===t.group.type&&this._activeObject===t.group){var e=["angle","flipX","flipY","left","scaleX","scaleY","skewX","skewY","top"],i={};return e.forEach(function(e){i[e]=t[e]}),fabric.util.addTransformToObject(t,this._activeObject.calcOwnMatrix()),i}return null},_unwindGroupTransformOnObject:function(t,e){e&&t.set(e)},_setSVGObject:function(t,e,i){var r=this._realizeGroupTransformOnObject(e);this.callSuper("_setSVGObject",t,e,i),this._unwindGroupTransformOnObject(e,r)},setViewportTransform:function(t){this.renderOnAddRemove&&this._activeObject&&this._activeObject.isEditing&&this._activeObject.clearContextTop(),fabric.StaticCanvas.prototype.setViewportTransform.call(this,t)}});for(var r in fabric.StaticCanvas)"prototype"!==r&&(fabric.Canvas[r]=fabric.StaticCanvas[r])}();!function(){function t(t,e){return t.button&&t.button===e-1}var e=fabric.util.addListener,i=fabric.util.removeListener,r=3,n=2,o=1,s={passive:!1};fabric.util.object.extend(fabric.Canvas.prototype,{mainTouchId:null,_initEventListeners:function(){this.removeListeners(),this._bindEvents(),this.addOrRemove(e,"add")},_getEventPrefix:function(){return this.enablePointerEvents?"pointer":"mouse"},addOrRemove:function(t,e){var i=this.upperCanvasEl,r=this._getEventPrefix();t(fabric.window,"resize",this._onResize),t(i,r+"down",this._onMouseDown),t(i,r+"move",this._onMouseMove,s),t(i,r+"out",this._onMouseOut),t(i,r+"enter",this._onMouseEnter),t(i,"wheel",this._onMouseWheel),t(i,"contextmenu",this._onContextMenu),t(i,"dblclick",this._onDoubleClick),t(i,"dragover",this._onDragOver),t(i,"dragenter",this._onDragEnter),t(i,"dragleave",this._onDragLeave),t(i,"drop",this._onDrop),this.enablePointerEvents||t(i,"touchstart",this._onTouchStart,s),"undefined"!=typeof eventjs&&e in eventjs&&(eventjs[e](i,"gesture",this._onGesture),eventjs[e](i,"drag",this._onDrag),eventjs[e](i,"orientation",this._onOrientationChange),eventjs[e](i,"shake",this._onShake),eventjs[e](i,"longpress",this._onLongPress))},removeListeners:function(){this.addOrRemove(i,"remove");var t=this._getEventPrefix();i(fabric.document,t+"up",this._onMouseUp),i(fabric.document,"touchend",this._onTouchEnd,s),i(fabric.document,t+"move",this._onMouseMove,s),i(fabric.document,"touchmove",this._onMouseMove,s)},_bindEvents:function(){this.eventsBound||(this._onMouseDown=this._onMouseDown.bind(this),this._onTouchStart=this._onTouchStart.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseUp=this._onMouseUp.bind(this),this._onTouchEnd=this._onTouchEnd.bind(this),this._onResize=this._onResize.bind(this),this._onGesture=this._onGesture.bind(this),this._onDrag=this._onDrag.bind(this),this._onShake=this._onShake.bind(this),this._onLongPress=this._onLongPress.bind(this),this._onOrientationChange=this._onOrientationChange.bind(this),this._onMouseWheel=this._onMouseWheel.bind(this),this._onMouseOut=this._onMouseOut.bind(this),this._onMouseEnter=this._onMouseEnter.bind(this),this._onContextMenu=this._onContextMenu.bind(this),this._onDoubleClick=this._onDoubleClick.bind(this),this._onDragOver=this._onDragOver.bind(this),this._onDragEnter=this._simpleEventHandler.bind(this,"dragenter"),this._onDragLeave=this._simpleEventHandler.bind(this,"dragleave"),this._onDrop=this._onDrop.bind(this),this.eventsBound=!0)},_onGesture:function(t,e){this.__onTransformGesture&&this.__onTransformGesture(t,e)},_onDrag:function(t,e){this.__onDrag&&this.__onDrag(t,e)},_onMouseWheel:function(t){this.__onMouseWheel(t)},_onMouseOut:function(t){var e=this._hoveredTarget;this.fire("mouse:out",{target:e,e:t}),this._hoveredTarget=null,e&&e.fire("mouseout",{e:t});var i=this;this._hoveredTargets.forEach(function(r){i.fire("mouse:out",{target:e,e:t}),r&&e.fire("mouseout",{e:t})}),this._hoveredTargets=[],this._iTextInstances&&this._iTextInstances.forEach(function(t){t.isEditing&&t.hiddenTextarea.focus()})},_onMouseEnter:function(t){this._currentTransform||this.findTarget(t)||(this.fire("mouse:over",{target:null,e:t}),this._hoveredTarget=null,this._hoveredTargets=[])},_onOrientationChange:function(t,e){this.__onOrientationChange&&this.__onOrientationChange(t,e)},_onShake:function(t,e){this.__onShake&&this.__onShake(t,e)},_onLongPress:function(t,e){this.__onLongPress&&this.__onLongPress(t,e)},_onDragOver:function(t){t.preventDefault();var e=this._simpleEventHandler("dragover",t);this._fireEnterLeaveEvents(e,t)},_onDrop:function(t){return this._simpleEventHandler("drop:before",t),this._simpleEventHandler("drop",t)},_onContextMenu:function(t){return this.stopContextMenu&&(t.stopPropagation(),t.preventDefault()),!1},_onDoubleClick:function(t){this._cacheTransformEventData(t),this._handleEvent(t,"dblclick"),this._resetTransformEventData(t)},getPointerId:function(t){var e=t.changedTouches;return e?e[0]&&e[0].identifier:this.enablePointerEvents?t.pointerId:-1},_isMainEvent:function(t){return t.isPrimary===!0?!0:t.isPrimary===!1?!1:"touchend"===t.type&&0===t.touches.length?!0:t.changedTouches?t.changedTouches[0].identifier===this.mainTouchId:!0},_onTouchStart:function(t){t.preventDefault(),null===this.mainTouchId&&(this.mainTouchId=this.getPointerId(t)),this.__onMouseDown(t),this._resetTransformEventData();var r=this.upperCanvasEl,n=this._getEventPrefix();e(fabric.document,"touchend",this._onTouchEnd,s),e(fabric.document,"touchmove",this._onMouseMove,s),i(r,n+"down",this._onMouseDown)},_onMouseDown:function(t){this.__onMouseDown(t),this._resetTransformEventData();var r=this.upperCanvasEl,n=this._getEventPrefix();i(r,n+"move",this._onMouseMove,s),e(fabric.document,n+"up",this._onMouseUp),e(fabric.document,n+"move",this._onMouseMove,s)},_onTouchEnd:function(t){if(!(t.touches.length>0)){this.__onMouseUp(t),this._resetTransformEventData(),this.mainTouchId=null;var r=this._getEventPrefix();i(fabric.document,"touchend",this._onTouchEnd,s),i(fabric.document,"touchmove",this._onMouseMove,s);var n=this;this._willAddMouseDown&&clearTimeout(this._willAddMouseDown),this._willAddMouseDown=setTimeout(function(){e(n.upperCanvasEl,r+"down",n._onMouseDown),n._willAddMouseDown=0},400)}},_onMouseUp:function(t){this.__onMouseUp(t),this._resetTransformEventData();var r=this.upperCanvasEl,n=this._getEventPrefix();this._isMainEvent(t)&&(i(fabric.document,n+"up",this._onMouseUp),i(fabric.document,n+"move",this._onMouseMove,s),e(r,n+"move",this._onMouseMove,s))},_onMouseMove:function(t){!this.allowTouchScrolling&&t.preventDefault&&t.preventDefault(),this.__onMouseMove(t)},_onResize:function(){this.calcOffset()},_shouldRender:function(t){var e=this._activeObject;return!!e!=!!t||e&&t&&e!==t?!0:e&&e.isEditing?!1:!1},__onMouseUp:function(e){var i,s=this._currentTransform,a=this._groupSelector,c=!1,h=!a||0===a.left&&0===a.top;if(this._cacheTransformEventData(e),i=this._target,this._handleEvent(e,"up:before"),t(e,r))return void(this.fireRightClick&&this._handleEvent(e,"up",r,h));if(t(e,n))return this.fireMiddleClick&&this._handleEvent(e,"up",n,h),void this._resetTransformEventData();if(this.isDrawingMode&&this._isCurrentlyDrawing)return void this._onMouseUpInDrawingMode(e);if(this._isMainEvent(e)){if(s&&(this._finalizeCurrentTransform(e),c=s.actionPerformed),!h){var l=i===this._activeObject;this._maybeGroupObjects(e),c||(c=this._shouldRender(i)||!l&&i===this._activeObject)}var u,f;if(i){if(u=i._findTargetCorner(this.getPointer(e,!0),fabric.util.isTouchEvent(e)),i.selectable&&i!==this._activeObject&&"up"===i.activeOn)this.setActiveObject(i,e),c=!0;else{var d=i.controls[u],g=d&&d.getMouseUpHandler(e,i,d);g&&(f=this.getPointer(e),g(e,s,f.x,f.y))}i.isMoving=!1}if(s&&(s.target!==i||s.corner!==u)){var p=s.target&&s.target.controls[s.corner],v=p&&p.getMouseUpHandler(e,i,d);f=f||this.getPointer(e),v&&v(e,s,f.x,f.y)}this._setCursorFromEvent(e,i),this._handleEvent(e,"up",o,h),this._groupSelector=null,this._currentTransform=null,i&&(i.__corner=0),c?this.requestRenderAll():h||this.renderTop()}},_simpleEventHandler:function(t,e){var i=this.findTarget(e),r=this.targets,n={e:e,target:i,subTargets:r};if(this.fire(t,n),i&&i.fire(t,n),!r)return i;for(var o=0;os;s++)this.fireSyntheticInOutEvents(n[s],e,{oldTarget:r[s],evtOut:"mouseout",evtIn:"mouseover"});this._hoveredTarget=t,this._hoveredTargets=this.targets.concat()},_fireEnterLeaveEvents:function(t,e){var i=this._draggedoverTarget,r=this._hoveredTargets,n=this.targets,o=Math.max(r.length,n.length);this.fireSyntheticInOutEvents(t,e,{oldTarget:i,evtOut:"dragleave",evtIn:"dragenter"});for(var s=0;o>s;s++)this.fireSyntheticInOutEvents(n[s],e,{oldTarget:r[s],evtOut:"dragleave",evtIn:"dragenter"});this._draggedoverTarget=t},fireSyntheticInOutEvents:function(t,e,i){var r,n,o,s,a=i.oldTarget,c=a!==t,h=i.canvasEvtIn,l=i.canvasEvtOut;c&&(r={e:e,target:t,previousTarget:a},n={e:e,target:a,nextTarget:t}),s=t&&c,o=a&&c,o&&(l&&this.fire(l,n),a.fire(i.evtOut,n)),s&&(h&&this.fire(h,r),t.fire(i.evtIn,r))},__onMouseWheel:function(t){this._cacheTransformEventData(t),this._handleEvent(t,"wheel"),this._resetTransformEventData()},_transformObject:function(t){var e=this.getPointer(t),i=this._currentTransform;i.reset=!1,i.shiftKey=t.shiftKey,i.altKey=t[this.centeredKey],this._performTransformAction(t,i,e),i.actionPerformed&&this.requestRenderAll()},_performTransformAction:function(t,e,i){var r=i.x,n=i.y,o=e.action,s=!1,a=e.actionHandler;a&&(s=a(t,e,r,n)),"drag"===o&&s&&(e.target.isMoving=!0,this.setCursor(e.target.moveCursor||this.moveCursor)),e.actionPerformed=e.actionPerformed||s},_fire:fabric.controlsUtils.fireEvent,_setCursorFromEvent:function(t,e){if(!e)return this.setCursor(this.defaultCursor),!1;var i=e.hoverCursor||this.hoverCursor,r=this._activeObject&&"activeSelection"===this._activeObject.type?this._activeObject:null,n=(!r||!r.contains(e))&&e._findTargetCorner(this.getPointer(t,!0));n?this.setCursor(this.getCornerCursor(n,e,t)):(e.subTargetCheck&&this.targets.concat().reverse().map(function(t){i=t.hoverCursor||i}),this.setCursor(i))},getCornerCursor:function(t,e,i){var r=e.controls[t];return r.cursorStyleHandler(i,r,e)}})}();!function(){var t=Math.min,e=Math.max;fabric.util.object.extend(fabric.Canvas.prototype,{_shouldGroup:function(t,e){var i=this._activeObject;return i&&this._isSelectionKeyPressed(t)&&e&&e.selectable&&this.selection&&(i!==e||"activeSelection"===i.type)&&!e.onSelect({e:t})},_handleGrouping:function(t,e){var i=this._activeObject;i.__corner||(e!==i||(e=this.findTarget(t,!0),e&&e.selectable))&&(i&&"activeSelection"===i.type?this._updateActiveSelection(e,t):this._createActiveSelection(e,t))},_updateActiveSelection:function(t,e){var i=this._activeObject,r=i._objects.slice(0);i.contains(t)?(i.removeWithUpdate(t),this._hoveredTarget=t,this._hoveredTargets=this.targets.concat(),1===i.size()&&this._setActiveObject(i.item(0),e)):(i.addWithUpdate(t),this._hoveredTarget=i,this._hoveredTargets=this.targets.concat()),this._fireSelectionEvents(r,e)},_createActiveSelection:function(t,e){var i=this.getActiveObjects(),r=this._createGroup(t);this._hoveredTarget=r,this._setActiveObject(r,e),this._fireSelectionEvents(i,e)},_createGroup:function(t){var e=this._objects,i=e.indexOf(this._activeObject)1&&(e=new fabric.ActiveSelection(i.reverse(),{canvas:this}),this.setActiveObject(e,t))},_collectObjects:function(i){for(var r,n=[],o=this._groupSelector.ex,s=this._groupSelector.ey,a=o+this._groupSelector.left,c=s+this._groupSelector.top,h=new fabric.Point(t(o,a),t(s,c)),l=new fabric.Point(e(o,a),e(s,c)),u=!this.selectionFullyContained,f=o===a&&s===c,d=this._objects.length;d--&&(r=this._objects[d],!(r&&r.selectable&&r.visible&&(u&&r.intersectsWithRect(h,l,!0)||r.isContainedWithinRect(h,l,!0)||u&&r.containsPoint(h,null,!0)||u&&r.containsPoint(l,null,!0))&&(n.push(r),f))););return n.length>1&&(n=n.filter(function(t){return!t.onSelect({e:i})})),n},_maybeGroupObjects:function(t){this.selection&&this._groupSelector&&this._groupSelectedObjects(t),this.setCursor(this.defaultCursor),this._groupSelector=null}})}();!function(){fabric.util.object.extend(fabric.StaticCanvas.prototype,{toDataURL:function(t){t||(t={});var e=t.format||"png",i=t.quality||1,r=(t.multiplier||1)*(t.enableRetinaScaling?this.getRetinaScaling():1),n=this.toCanvasElement(r,t);return fabric.util.toDataURL(n,e,i)},toCanvasElement:function(t,e){t=t||1,e=e||{};var i=(e.width||this.width)*t,r=(e.height||this.height)*t,n=this.getZoom(),o=this.width,s=this.height,a=n*t,c=this.viewportTransform,h=(c[4]-(e.left||0))*t,l=(c[5]-(e.top||0))*t,u=this.interactive,f=[a,0,0,a,h,l],d=this.enableRetinaScaling,g=fabric.util.createCanvasElement(),p=this.contextTop;return g.width=i,g.height=r,this.contextTop=null,this.enableRetinaScaling=!1,this.interactive=!1,this.viewportTransform=f,this.width=i,this.height=r,this.calcViewportBoundaries(),this.renderCanvas(g.getContext("2d"),this._objects),this.viewportTransform=c,this.width=o,this.height=s,this.calcViewportBoundaries(),this.interactive=u,this.enableRetinaScaling=d,this.contextTop=p,g}})}();fabric.util.object.extend(fabric.StaticCanvas.prototype,{loadFromJSON:function(t,e,i){if(t){var r="string"==typeof t?JSON.parse(t):fabric.util.object.clone(t),n=this,o=r.clipPath,s=this.renderOnAddRemove;return this.renderOnAddRemove=!1,delete r.clipPath,this._enlivenObjects(r.objects,function(t){n.clear(),n._setBgOverlay(r,function(){o?n._enlivenObjects([o],function(i){n.clipPath=i[0],n.__setupCanvas.call(n,r,t,s,e)}):n.__setupCanvas.call(n,r,t,s,e)})},i),this}},__setupCanvas:function(t,e,i,r){var n=this;e.forEach(function(t,e){n.insertAt(t,e)}),this.renderOnAddRemove=i,delete t.objects,delete t.backgroundImage,delete t.overlayImage,delete t.background,delete t.overlay,this._setOptions(t),this.renderAll(),r&&r()},_setBgOverlay:function(t,e){var i={backgroundColor:!1,overlayColor:!1,backgroundImage:!1,overlayImage:!1};if(!(t.backgroundImage||t.overlayImage||t.background||t.overlay))return void(e&&e());var r=function(){i.backgroundImage&&i.overlayImage&&i.backgroundColor&&i.overlayColor&&e&&e()};this.__setBgOverlay("backgroundImage",t.backgroundImage,i,r),this.__setBgOverlay("overlayImage",t.overlayImage,i,r),this.__setBgOverlay("backgroundColor",t.background,i,r),this.__setBgOverlay("overlayColor",t.overlay,i,r)},__setBgOverlay:function(t,e,i,r){var n=this;return e?void("backgroundImage"===t||"overlayImage"===t?fabric.util.enlivenObjects([e],function(e){n[t]=e[0],i[t]=!0,r&&r()}):this["set"+fabric.util.string.capitalize(t,!0)](e,function(){i[t]=!0,r&&r()})):(i[t]=!0,void(r&&r()))},_enlivenObjects:function(t,e,i){return t&&0!==t.length?void fabric.util.enlivenObjects(t,function(t){e&&e(t)},null,i):void(e&&e([]))},_toDataURL:function(t,e){this.clone(function(i){e(i.toDataURL(t))})},_toDataURLWithMultiplier:function(t,e,i){this.clone(function(r){i(r.toDataURLWithMultiplier(t,e))})},clone:function(t,e){var i=JSON.stringify(this.toJSON(e));this.cloneWithoutData(function(e){e.loadFromJSON(i,function(){t&&t(e)})})},cloneWithoutData:function(t){var e=fabric.util.createCanvasElement();e.width=this.width,e.height=this.height;var i=new fabric.Canvas(e);this.backgroundImage?(i.setBackgroundImage(this.backgroundImage.src,function(){i.renderAll(),t&&t(i)}),i.backgroundImageOpacity=this.backgroundImageOpacity,i.backgroundImageStretch=this.backgroundImageStretch):t&&t(i)}});!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.object.clone,n=e.util.toFixed,s=e.util.string.capitalize,o=e.util.degreesToRadians,a=!e.isLikelyNode,c=2;e.Object||(e.Object=e.util.createClass(e.CommonMethods,{type:"object",originX:"left",originY:"top",top:0,left:0,width:0,height:0,scaleX:1,scaleY:1,flipX:!1,flipY:!1,opacity:1,angle:0,skewX:0,skewY:0,cornerSize:13,touchCornerSize:24,transparentCorners:!0,hoverCursor:null,moveCursor:null,padding:0,borderColor:"rgb(178,204,255)",borderDashArray:null,cornerColor:"rgb(178,204,255)",cornerStrokeColor:null,cornerStyle:"rect",cornerDashArray:null,centeredScaling:!1,centeredRotation:!0,fill:"rgb(0,0,0)",fillRule:"nonzero",globalCompositeOperation:"source-over",backgroundColor:"",selectionBackgroundColor:"",stroke:null,strokeWidth:1,strokeDashArray:null,strokeDashOffset:0,strokeLineCap:"butt",strokeLineJoin:"miter",strokeMiterLimit:4,shadow:null,borderOpacityWhenMoving:.4,borderScaleFactor:1,minScaleLimit:0,selectable:!0,evented:!0,visible:!0,hasControls:!0,hasBorders:!0,perPixelTargetFind:!1,includeDefaultValues:!0,lockMovementX:!1,lockMovementY:!1,lockRotation:!1,lockScalingX:!1,lockScalingY:!1,lockSkewingX:!1,lockSkewingY:!1,lockScalingFlip:!1,excludeFromExport:!1,objectCaching:a,statefullCache:!1,noScaleCache:!0,strokeUniform:!1,dirty:!0,__corner:0,paintFirst:"fill",activeOn:"down",stateProperties:"top left width height scaleX scaleY flipX flipY originX originY transformMatrix stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit angle opacity fill globalCompositeOperation shadow visible backgroundColor skewX skewY fillRule paintFirst clipPath strokeUniform".split(" "),cacheProperties:"fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath".split(" "),colorProperties:"fill stroke backgroundColor".split(" "),clipPath:void 0,inverted:!1,absolutePositioned:!1,initialize:function(t){t&&this.setOptions(t)},_createCacheCanvas:function(){this._cacheProperties={},this._cacheCanvas=e.util.createCanvasElement(),this._cacheContext=this._cacheCanvas.getContext("2d"),this._updateCacheCanvas(),this.dirty=!0},_limitCacheSize:function(t){var i=e.perfLimitSizeTotal,r=t.width,n=t.height,s=e.maxCacheSideLimit,o=e.minCacheSideLimit;if(s>=r&&s>=n&&i>=r*n)return o>r&&(t.width=o),o>n&&(t.height=o),t;var a=r/n,c=e.util.limitDimsByArea(a,i),h=e.util.capValue,l=h(o,c.x,s),u=h(o,c.y,s);return r>l&&(t.zoomX/=r/l,t.width=l,t.capped=!0),n>u&&(t.zoomY/=n/u,t.height=u,t.capped=!0),t},_getCacheCanvasDimensions:function(){var t=this.getTotalObjectScaling(),e=this._getTransformedDimensions(0,0),i=e.x*t.scaleX/this.scaleX,r=e.y*t.scaleY/this.scaleY;return{width:i+c,height:r+c,zoomX:t.scaleX,zoomY:t.scaleY,x:i,y:r}},_updateCacheCanvas:function(){var t=this.canvas;if(this.noScaleCache&&t&&t._currentTransform){var i=t._currentTransform.target,r=t._currentTransform.action;if(this===i&&r.slice&&"scale"===r.slice(0,5))return!1}var n,s,o=this._cacheCanvas,a=this._limitCacheSize(this._getCacheCanvasDimensions()),c=e.minCacheSideLimit,h=a.width,l=a.height,u=a.zoomX,f=a.zoomY,d=h!==this.cacheWidth||l!==this.cacheHeight,g=this.zoomX!==u||this.zoomY!==f,p=d||g,v=0,m=0,b=!1;if(d){var y=this._cacheCanvas.width,_=this._cacheCanvas.height,x=h>y||l>_,C=(.9*y>h||.9*_>l)&&y>c&&_>c;b=x||C,x&&!a.capped&&(h>c||l>c)&&(v=.1*h,m=.1*l)}return this instanceof e.Text&&this.path&&(p=!0,b=!0,v+=this.getHeightOfLine(0)*this.zoomX,m+=this.getHeightOfLine(0)*this.zoomY),p?(b?(o.width=Math.ceil(h+v),o.height=Math.ceil(l+m)):(this._cacheContext.setTransform(1,0,0,1,0,0),this._cacheContext.clearRect(0,0,o.width,o.height)),n=a.x/2,s=a.y/2,this.cacheTranslationX=Math.round(o.width/2-n)+n,this.cacheTranslationY=Math.round(o.height/2-s)+s,this.cacheWidth=h,this.cacheHeight=l,this._cacheContext.translate(this.cacheTranslationX,this.cacheTranslationY),this._cacheContext.scale(u,f),this.zoomX=u,this.zoomY=f,!0):!1},setOptions:function(t){this._setOptions(t),this._initGradient(t.fill,"fill"),this._initGradient(t.stroke,"stroke"),this._initPattern(t.fill,"fill"),this._initPattern(t.stroke,"stroke")},transform:function(t){var e=this.group&&!this.group._transformDone||this.group&&this.canvas&&t===this.canvas.contextTop,i=this.calcTransformMatrix(!e);t.transform(i[0],i[1],i[2],i[3],i[4],i[5])},toObject:function(t){var i=e.Object.NUM_FRACTION_DIGITS,r={type:this.type,version:e.version,originX:this.originX,originY:this.originY,left:n(this.left,i),top:n(this.top,i),width:n(this.width,i),height:n(this.height,i),fill:this.fill&&this.fill.toObject?this.fill.toObject():this.fill,stroke:this.stroke&&this.stroke.toObject?this.stroke.toObject():this.stroke,strokeWidth:n(this.strokeWidth,i),strokeDashArray:this.strokeDashArray?this.strokeDashArray.concat():this.strokeDashArray,strokeLineCap:this.strokeLineCap,strokeDashOffset:this.strokeDashOffset,strokeLineJoin:this.strokeLineJoin,strokeUniform:this.strokeUniform,strokeMiterLimit:n(this.strokeMiterLimit,i),scaleX:n(this.scaleX,i),scaleY:n(this.scaleY,i),angle:n(this.angle,i),flipX:this.flipX,flipY:this.flipY,opacity:n(this.opacity,i),shadow:this.shadow&&this.shadow.toObject?this.shadow.toObject():this.shadow,visible:this.visible,backgroundColor:this.backgroundColor,fillRule:this.fillRule,paintFirst:this.paintFirst,globalCompositeOperation:this.globalCompositeOperation,skewX:n(this.skewX,i),skewY:n(this.skewY,i)};return this.clipPath&&!this.clipPath.excludeFromExport&&(r.clipPath=this.clipPath.toObject(t),r.clipPath.inverted=this.clipPath.inverted,r.clipPath.absolutePositioned=this.clipPath.absolutePositioned),e.util.populateWithProperties(this,r,t),this.includeDefaultValues||(r=this._removeDefaultValues(r)),r},toDatalessObject:function(t){return this.toObject(t)},_removeDefaultValues:function(t){var i=e.util.getKlass(t.type).prototype,r=i.stateProperties;return r.forEach(function(e){"left"!==e&&"top"!==e&&(t[e]===i[e]&&delete t[e],Array.isArray(t[e])&&Array.isArray(i[e])&&0===t[e].length&&0===i[e].length&&delete t[e])}),t},toString:function(){return"#"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=e.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t=this.getObjectScaling(),e=t.scaleX,i=t.scaleY;if(this.canvas){var r=this.canvas.getZoom(),n=this.canvas.getRetinaScaling();e*=r*n,i*=r*n}return{scaleX:e,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,i){var r="scaleX"===t||"scaleY"===t,n=this[t]!==i,s=!1;return r&&(i=this._constrainScale(i)),"scaleX"===t&&0>i?(this.flipX=!this.flipX,i*=-1):"scaleY"===t&&0>i?(this.flipY=!this.flipY,i*=-1):"shadow"!==t||!i||i instanceof e.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",i):i=new e.Shadow(i),this[t]=i,n&&(s=this.group&&this.group.isOnACache(),this.cacheProperties.indexOf(t)>-1?(this.dirty=!0,s&&this.group.set("dirty",!0)):s&&this.stateProperties.indexOf(t)>-1&&this.group.set("dirty",!0)),this},setOnGroup:function(){},getViewportTransform:function(){return this.canvas&&this.canvas.viewportTransform?this.canvas.viewportTransform:e.iMatrix.concat()},isNotVisible:function(){return 0===this.opacity||!this.width&&!this.height&&0===this.strokeWidth||!this.visible},render:function(t){this.isNotVisible()||(!this.canvas||!this.canvas.skipOffscreen||this.group||this.isOnScreen())&&(t.save(),this._setupCompositeOperation(t),this.drawSelectionBackground(t),this.transform(t),this._setOpacity(t),this._setShadow(t,this),this.shouldCache()?(this.renderCache(),this.drawCacheOnCanvas(t)):(this._removeCacheCanvas(),this.dirty=!1,this.drawObject(t),this.objectCaching&&this.statefullCache&&this.saveState({propertySet:"cacheProperties"})),t.restore())},renderCache:function(t){t=t||{},this._cacheCanvas&&this._cacheContext||this._createCacheCanvas(),this.isCacheDirty()&&(this.statefullCache&&this.saveState({propertySet:"cacheProperties"}),this.drawObject(this._cacheContext,t.forClipping),this.dirty=!1)},_removeCacheCanvas:function(){this._cacheCanvas=null,this._cacheContext=null,this.cacheWidth=0,this.cacheHeight=0},hasStroke:function(){return this.stroke&&"transparent"!==this.stroke&&0!==this.strokeWidth},hasFill:function(){return this.fill&&"transparent"!==this.fill},needsItsOwnCache:function(){return"stroke"===this.paintFirst&&this.hasFill()&&this.hasStroke()&&"object"==typeof this.shadow?!0:this.clipPath?!0:!1},shouldCache:function(){return this.ownCaching=this.needsItsOwnCache()||this.objectCaching&&(!this.group||!this.group.isOnACache()),this.ownCaching},willDrawShadow:function(){return!!this.shadow&&(0!==this.shadow.offsetX||0!==this.shadow.offsetY)},drawClipPathOnCache:function(t,i){if(t.save(),t.globalCompositeOperation=i.inverted?"destination-out":"destination-in",i.absolutePositioned){var r=e.util.invertTransform(this.calcTransformMatrix());t.transform(r[0],r[1],r[2],r[3],r[4],r[5])}i.transform(t),t.scale(1/i.zoomX,1/i.zoomY),t.drawImage(i._cacheCanvas,-i.cacheTranslationX,-i.cacheTranslationY),t.restore()},drawObject:function(t,e){var i=this.fill,r=this.stroke;e?(this.fill="black",this.stroke="",this._setClippingProperties(t)):this._renderBackground(t),this._render(t),this._drawClipPath(t,this.clipPath),this.fill=i,this.stroke=r},_drawClipPath:function(t,e){e&&(e.canvas=this.canvas,e.shouldCache(),e._transformDone=!0,e.renderCache({forClipping:!0}),this.drawClipPathOnCache(t,e))},drawCacheOnCanvas:function(t){t.scale(1/this.zoomX,1/this.zoomY),t.drawImage(this._cacheCanvas,-this.cacheTranslationX,-this.cacheTranslationY)},isCacheDirty:function(t){if(this.isNotVisible())return!1;if(this._cacheCanvas&&this._cacheContext&&!t&&this._updateCacheCanvas())return!0;if(this.dirty||this.clipPath&&this.clipPath.absolutePositioned||this.statefullCache&&this.hasStateChanged("cacheProperties")){if(this._cacheCanvas&&this._cacheContext&&!t){var e=this.cacheWidth/this.zoomX,i=this.cacheHeight/this.zoomY;this._cacheContext.clearRect(-e/2,-i/2,e,i)}return!0}return!1},_renderBackground:function(t){if(this.backgroundColor){var e=this._getNonTransformedDimensions();t.fillStyle=this.backgroundColor,t.fillRect(-e.x/2,-e.y/2,e.x,e.y),this._removeShadow(t)}},_setOpacity:function(t){this.group&&!this.group._transformDone?t.globalAlpha=this.getObjectOpacity():t.globalAlpha*=this.opacity},_setStrokeStyles:function(t,e){var i=e.stroke;i&&(t.lineWidth=e.strokeWidth,t.lineCap=e.strokeLineCap,t.lineDashOffset=e.strokeDashOffset,t.lineJoin=e.strokeLineJoin,t.miterLimit=e.strokeMiterLimit,i.toLive?"percentage"===i.gradientUnits||i.gradientTransform||i.patternTransform?this._applyPatternForTransformedGradient(t,i):(t.strokeStyle=i.toLive(t,this),this._applyPatternGradientTransform(t,i)):t.strokeStyle=e.stroke)},_setFillStyles:function(t,e){var i=e.fill;i&&(i.toLive?(t.fillStyle=i.toLive(t,this),this._applyPatternGradientTransform(t,e.fill)):t.fillStyle=i)},_setClippingProperties:function(t){t.globalAlpha=1,t.strokeStyle="transparent",t.fillStyle="#000000"},_setLineDash:function(t,e){e&&0!==e.length&&(1&e.length&&e.push.apply(e,e),t.setLineDash(e))},_renderControls:function(t,i){var r,n,s,a=this.getViewportTransform(),c=this.calcTransformMatrix();i=i||{},n="undefined"!=typeof i.hasBorders?i.hasBorders:this.hasBorders,s="undefined"!=typeof i.hasControls?i.hasControls:this.hasControls,c=e.util.multiplyTransformMatrices(a,c),r=e.util.qrDecompose(c),t.save(),t.translate(r.translateX,r.translateY),t.lineWidth=1*this.borderScaleFactor,this.group||(t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1),this.flipX&&(r.angle-=180),t.rotate(o(this.group?r.angle:this.angle)),i.forActiveSelection||this.group?n&&this.drawBordersInGroup(t,r,i):n&&this.drawBorders(t,i),s&&this.drawControls(t,i),t.restore()},_setShadow:function(t){if(this.shadow){var i,r=this.shadow,n=this.canvas,s=n&&n.viewportTransform[0]||1,o=n&&n.viewportTransform[3]||1;i=r.nonScaling?{scaleX:1,scaleY:1}:this.getObjectScaling(),n&&n._isRetinaScaling()&&(s*=e.devicePixelRatio,o*=e.devicePixelRatio),t.shadowColor=r.color,t.shadowBlur=r.blur*e.browserShadowBlurConstant*(s+o)*(i.scaleX+i.scaleY)/4,t.shadowOffsetX=r.offsetX*s*i.scaleX,t.shadowOffsetY=r.offsetY*o*i.scaleY}},_removeShadow:function(t){this.shadow&&(t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0)},_applyPatternGradientTransform:function(t,e){if(!e||!e.toLive)return{offsetX:0,offsetY:0};var i=e.gradientTransform||e.patternTransform,r=-this.width/2+e.offsetX||0,n=-this.height/2+e.offsetY||0;return"percentage"===e.gradientUnits?t.transform(this.width,0,0,this.height,r,n):t.transform(1,0,0,1,r,n),i&&t.transform(i[0],i[1],i[2],i[3],i[4],i[5]),{offsetX:r,offsetY:n}},_renderPaintInOrder:function(t){"stroke"===this.paintFirst?(this._renderStroke(t),this._renderFill(t)):(this._renderFill(t),this._renderStroke(t))},_render:function(){},_renderFill:function(t){this.fill&&(t.save(),this._setFillStyles(t,this),"evenodd"===this.fillRule?t.fill("evenodd"):t.fill(),t.restore())},_renderStroke:function(t){if(this.stroke&&0!==this.strokeWidth){if(this.shadow&&!this.shadow.affectStroke&&this._removeShadow(t),t.save(),this.strokeUniform&&this.group){var e=this.getObjectScaling();t.scale(1/e.scaleX,1/e.scaleY)}else this.strokeUniform&&t.scale(1/this.scaleX,1/this.scaleY);this._setLineDash(t,this.strokeDashArray),this._setStrokeStyles(t,this),t.stroke(),t.restore()}},_applyPatternForTransformedGradient:function(t,i){var r,n=this._limitCacheSize(this._getCacheCanvasDimensions()),s=e.util.createCanvasElement(),o=this.canvas.getRetinaScaling(),a=n.x/this.scaleX/o,c=n.y/this.scaleY/o;s.width=a,s.height=c,r=s.getContext("2d"),r.beginPath(),r.moveTo(0,0),r.lineTo(a,0),r.lineTo(a,c),r.lineTo(0,c),r.closePath(),r.translate(a/2,c/2),r.scale(n.zoomX/this.scaleX/o,n.zoomY/this.scaleY/o),this._applyPatternGradientTransform(r,i),r.fillStyle=i.toLive(t),r.fill(),t.translate(-this.width/2-this.strokeWidth/2,-this.height/2-this.strokeWidth/2),t.scale(o*this.scaleX/n.zoomX,o*this.scaleY/n.zoomY),t.strokeStyle=r.createPattern(s,"no-repeat")},_findCenterFromElement:function(){return{x:this.left+this.width/2,y:this.top+this.height/2}},_assignTransformMatrixProps:function(){if(this.transformMatrix){var t=e.util.qrDecompose(this.transformMatrix);this.flipX=!1,this.flipY=!1,this.set("scaleX",t.scaleX),this.set("scaleY",t.scaleY),this.angle=t.angle,this.skewX=t.skewX,this.skewY=0}},_removeTransformMatrix:function(t){var i=this._findCenterFromElement();this.transformMatrix&&(this._assignTransformMatrixProps(),i=e.util.transformPoint(i,this.transformMatrix)),this.transformMatrix=null,t&&(this.scaleX*=t.scaleX,this.scaleY*=t.scaleY,this.cropX=t.cropX,this.cropY=t.cropY,i.x+=t.offsetLeft,i.y+=t.offsetTop,this.width=t.width,this.height=t.height),this.setPositionByOrigin(i,"center","center")},clone:function(t,i){var r=this.toObject(i);this.constructor.fromObject?this.constructor.fromObject(r,t):e.Object._fromObject("Object",r,t)},cloneAsImage:function(t,i){var r=this.toCanvasElement(i);return t&&t(new e.Image(r)),this},toCanvasElement:function(t){t||(t={});var i=e.util,r=i.saveObjectTransform(this),n=this.group,s=this.shadow,o=Math.abs,a=(t.multiplier||1)*(t.enableRetinaScaling?e.devicePixelRatio:1);delete this.group,t.withoutTransform&&i.resetObjectTransform(this),t.withoutShadow&&(this.shadow=null);var c,h,l,u,f=e.util.createCanvasElement(),d=this.getBoundingRect(!0,!0),g=this.shadow,p={x:0,y:0};g&&(h=g.blur,c=g.nonScaling?{scaleX:1,scaleY:1}:this.getObjectScaling(),p.x=2*Math.round(o(g.offsetX)+h)*o(c.scaleX),p.y=2*Math.round(o(g.offsetY)+h)*o(c.scaleY)),l=d.width+p.x,u=d.height+p.y,f.width=Math.ceil(l),f.height=Math.ceil(u);var v=new e.StaticCanvas(f,{enableRetinaScaling:!1,renderOnAddRemove:!1,skipOffscreen:!1});"jpeg"===t.format&&(v.backgroundColor="#fff"),this.setPositionByOrigin(new e.Point(v.width/2,v.height/2),"center","center");var m=this.canvas;v.add(this);var b=v.toCanvasElement(a||1,t);return this.shadow=s,this.set("canvas",m),n&&(this.group=n),this.set(r).setCoords(),v._objects=[],v.dispose(),v=null,b},toDataURL:function(t){return t||(t={}),e.util.toDataURL(this.toCanvasElement(t),t.format||"png",t.quality||1)},isType:function(t){return arguments.length>1?Array.from(arguments).includes(this.type):this.type===t},complexity:function(){return 1},toJSON:function(t){return this.toObject(t)},rotate:function(t){var e=("center"!==this.originX||"center"!==this.originY)&&this.centeredRotation;return e&&this._setOriginToCenter(),this.set("angle",t),e&&this._resetOrigin(),this},centerH:function(){return this.canvas&&this.canvas.centerObjectH(this),this},viewportCenterH:function(){return this.canvas&&this.canvas.viewportCenterObjectH(this),this},centerV:function(){return this.canvas&&this.canvas.centerObjectV(this),this},viewportCenterV:function(){return this.canvas&&this.canvas.viewportCenterObjectV(this),this},center:function(){return this.canvas&&this.canvas.centerObject(this),this},viewportCenter:function(){return this.canvas&&this.canvas.viewportCenterObject(this),this},getLocalPointer:function(t,i){i=i||this.canvas.getPointer(t);var r=new e.Point(i.x,i.y),n=this._getLeftTopCoords();return this.angle&&(r=e.util.rotatePoint(r,n,o(-this.angle))),{x:r.x-n.x,y:r.y-n.y}},_setupCompositeOperation:function(t){this.globalCompositeOperation&&(t.globalCompositeOperation=this.globalCompositeOperation)},dispose:function(){e.runningAnimations&&e.runningAnimations.cancelByTarget(this)}}),e.util.createAccessors&&e.util.createAccessors(e.Object),i(e.Object.prototype,e.Observable),e.Object.NUM_FRACTION_DIGITS=2,e.Object.ENLIVEN_PROPS=["clipPath"],e.Object._fromObject=function(t,i,n,s){var o=e[t];i=r(i,!0),e.util.enlivenPatterns([i.fill,i.stroke],function(t){"undefined"!=typeof t[0]&&(i.fill=t[0]),"undefined"!=typeof t[1]&&(i.stroke=t[1]),e.util.enlivenObjectEnlivables(i,i,function(){var t=s?new o(i[s],i):new o(i);n&&n(t)})})},e.Object.__uid=0)}("undefined"!=typeof exports?exports:this);!function(){var t=fabric.util.degreesToRadians,e={left:-.5,center:0,right:.5},i={top:-.5,center:0,bottom:.5};fabric.util.object.extend(fabric.Object.prototype,{translateToGivenOrigin:function(t,r,n,s,o){var a,c,h,l=t.x,u=t.y;return"string"==typeof r?r=e[r]:r-=.5,"string"==typeof s?s=e[s]:s-=.5,a=s-r,"string"==typeof n?n=i[n]:n-=.5,"string"==typeof o?o=i[o]:o-=.5,c=o-n,(a||c)&&(h=this._getTransformedDimensions(),l=t.x+a*h.x,u=t.y+c*h.y),new fabric.Point(l,u)},translateToCenterPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,i,r,"center","center");return this.angle?fabric.util.rotatePoint(n,e,t(this.angle)):n},translateToOriginPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,"center","center",i,r);return this.angle?fabric.util.rotatePoint(n,e,t(this.angle)):n},getCenterPoint:function(){var t=new fabric.Point(this.left,this.top);return this.translateToCenterPoint(t,this.originX,this.originY)},getPointByOrigin:function(t,e){var i=this.getCenterPoint();return this.translateToOriginPoint(i,t,e)},toLocalPoint:function(e,i,r){var n,s,o=this.getCenterPoint();return n="undefined"!=typeof i&&"undefined"!=typeof r?this.translateToGivenOrigin(o,"center","center",i,r):new fabric.Point(this.left,this.top),s=new fabric.Point(e.x,e.y),this.angle&&(s=fabric.util.rotatePoint(s,o,-t(this.angle))),s.subtractEquals(n)},setPositionByOrigin:function(t,e,i){var r=this.translateToCenterPoint(t,e,i),n=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",n.x),this.set("top",n.y)},adjustPosition:function(i){var r,n,s=t(this.angle),o=this.getScaledWidth(),a=fabric.util.cos(s)*o,c=fabric.util.sin(s)*o;r="string"==typeof this.originX?e[this.originX]:this.originX-.5,n="string"==typeof i?e[i]:i-.5,this.left+=a*(n-r),this.top+=c*(n-r),this.setCoords(),this.originX=i},_setOriginToCenter:function(){this._originalOriginX=this.originX,this._originalOriginY=this.originY;var t=this.getCenterPoint();this.originX="center",this.originY="center",this.left=t.x,this.top=t.y},_resetOrigin:function(){var t=this.translateToOriginPoint(this.getCenterPoint(),this._originalOriginX,this._originalOriginY);this.originX=this._originalOriginX,this.originY=this._originalOriginY,this.left=t.x,this.top=t.y,this._originalOriginX=null,this._originalOriginY=null},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","top")}})}();!function(){function t(t){return[new fabric.Point(t.tl.x,t.tl.y),new fabric.Point(t.tr.x,t.tr.y),new fabric.Point(t.br.x,t.br.y),new fabric.Point(t.bl.x,t.bl.y)]}var e=fabric.util,i=e.degreesToRadians,r=e.multiplyTransformMatrices,n=e.transformPoint;e.object.extend(fabric.Object.prototype,{oCoords:null,aCoords:null,lineCoords:null,ownMatrixCache:null,matrixCache:null,controls:{},_getCoords:function(t,e){return e?t?this.calcACoords():this.calcLineCoords():(this.aCoords&&this.lineCoords||this.setCoords(!0),t?this.aCoords:this.lineCoords)},getCoords:function(e,i){return t(this._getCoords(e,i))},intersectsWithRect:function(t,e,i,r){var n=this.getCoords(i,r),s=fabric.Intersection.intersectPolygonRectangle(n,t,e);return"Intersection"===s.status},intersectsWithObject:function(t,e,i){var r=fabric.Intersection.intersectPolygonPolygon(this.getCoords(e,i),t.getCoords(e,i));return"Intersection"===r.status||t.isContainedWithinObject(this,e,i)||this.isContainedWithinObject(t,e,i)},isContainedWithinObject:function(t,e,i){for(var r=this.getCoords(e,i),n=e?t.aCoords:t.lineCoords,s=0,o=t._getImageLines(n);4>s;s++)if(!t.containsPoint(r[s],o))return!1;return!0},isContainedWithinRect:function(t,e,i,r){var n=this.getBoundingRect(i,r);return n.left>=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var n=this._getCoords(i,r),e=e||this._getImageLines(n),s=this._findCrossPoints(t,e);return 0!==s&&s%2===1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br,r=this.getCoords(!0,t);return r.some(function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y})?!0:this.intersectsWithRect(e,i,!0,t)?!0:this._containsCenterOfCanvas(e,i,t)},_containsCenterOfCanvas:function(t,e,i){var r={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return this.containsPoint(r,null,!0,i)?!0:!1},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;if(this.intersectsWithRect(e,i,!0,t))return!0;var r=this.getCoords(!0,t).every(function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)});return r&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){var e={topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}};return e},_findCrossPoints:function(t,e){var i,r,n,s,o,a,c=0;for(var h in e)if(a=e[h],!(a.o.y=t.y&&a.d.y>=t.y||(a.o.x===a.d.x&&a.o.x>=t.x?o=a.o.x:(i=0,r=(a.d.y-a.o.y)/(a.d.x-a.o.x),n=t.y-i*t.x,s=a.o.y-r*a.o.x,o=-(n-s)/(i-r)),o>=t.x&&(c+=1),2!==c)))break;return c},getBoundingRect:function(t,i){var r=this.getCoords(t,i);return e.makeBoundingBoxFromPoints(r)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)t?-this.minScaleLimit:this.minScaleLimit:0===t?1e-4:t},scale:function(t){return this._set("scaleX",t),this._set("scaleY",t),this.setCoords()},scaleToWidth:function(t,e){var i=this.getBoundingRect(e).width/this.getScaledWidth();return this.scale(t/this.width/i)},scaleToHeight:function(t,e){var i=this.getBoundingRect(e).height/this.getScaledHeight();return this.scale(t/this.height/i)},calcLineCoords:function(){var t=this.getViewportTransform(),r=this.padding,s=i(this.angle),o=e.cos(s),a=e.sin(s),c=o*r,h=a*r,l=c+h,u=c-h,f=this.calcACoords(),d={tl:n(f.tl,t),tr:n(f.tr,t),bl:n(f.bl,t),br:n(f.br,t)};return r&&(d.tl.x-=u,d.tl.y-=l,d.tr.x+=l,d.tr.y-=u,d.bl.x-=l,d.bl.y+=u,d.br.x+=u,d.br.y+=l),d},calcOCoords:function(){var t=this._calcRotateMatrix(),e=this._calcTranslateMatrix(),i=this.getViewportTransform(),n=r(i,e),s=r(n,t),s=r(s,[1/i[0],0,0,1/i[3],0,0]),o=this._calculateCurrentDimensions(),a={};return this.forEachControl(function(t,e,i){a[e]=t.positionHandler(o,s,i)}),a},calcACoords:function(){var t=this._calcRotateMatrix(),e=this._calcTranslateMatrix(),i=r(e,t),s=this._getTransformedDimensions(),o=s.x/2,a=s.y/2;return{tl:n({x:-o,y:-a},i),tr:n({x:o,y:-a},i),bl:n({x:-o,y:a},i),br:n({x:o,y:a},i)}},setCoords:function(t){return this.aCoords=this.calcACoords(),this.lineCoords=this.group?this.aCoords:this.calcLineCoords(),t?this:(this.oCoords=this.calcOCoords(),this._setCornerCoords&&this._setCornerCoords(),this)},_calcRotateMatrix:function(){return e.calcRotateMatrix(this)},_calcTranslateMatrix:function(){var t=this.getCenterPoint();return[1,0,0,1,t.x,t.y]},transformMatrixKey:function(t){var e="_",i="";return!t&&this.group&&(i=this.group.transformMatrixKey(t)+e),i+this.top+e+this.left+e+this.scaleX+e+this.scaleY+e+this.skewX+e+this.skewY+e+this.angle+e+this.originX+e+this.originY+e+this.width+e+this.height+e+this.strokeWidth+this.flipX+this.flipY},calcTransformMatrix:function(t){var e=this.calcOwnMatrix();if(t||!this.group)return e;var i=this.transformMatrixKey(t),n=this.matrixCache||(this.matrixCache={});return n.key===i?n.value:(this.group&&(e=r(this.group.calcTransformMatrix(!1),e)),n.key=i,n.value=e,e)},calcOwnMatrix:function(){var t=this.transformMatrixKey(!0),i=this.ownMatrixCache||(this.ownMatrixCache={});if(i.key===t)return i.value;var r=this._calcTranslateMatrix(),n={angle:this.angle,translateX:r[4],translateY:r[5],scaleX:this.scaleX,scaleY:this.scaleY,skewX:this.skewX,skewY:this.skewY,flipX:this.flipX,flipY:this.flipY};return i.key=t,i.value=e.composeMatrix(n),i.value},_getNonTransformedDimensions:function(){var t=this.strokeWidth,e=this.width+t,i=this.height+t;return{x:e,y:i}},_getTransformedDimensions:function(t,i){"undefined"==typeof t&&(t=this.skewX),"undefined"==typeof i&&(i=this.skewY);var r,n,s,o=0===t&&0===i;if(this.strokeUniform?(n=this.width,s=this.height):(r=this._getNonTransformedDimensions(),n=r.x,s=r.y),o)return this._finalizeDimensions(n*this.scaleX,s*this.scaleY);var a=e.sizeAfterTransform(n,s,{scaleX:this.scaleX,scaleY:this.scaleY,skewX:t,skewY:i});return this._finalizeDimensions(a.x,a.y)},_finalizeDimensions:function(t,e){return this.strokeUniform?{x:t+this.strokeWidth,y:e+this.strokeWidth}:{x:t,y:e}},_calculateCurrentDimensions:function(){var t=this.getViewportTransform(),e=this._getTransformedDimensions(),i=n(e,t,!0);return i.scalarAdd(2*this.padding)}})}();fabric.util.object.extend(fabric.Object.prototype,{sendToBack:function(){return this.group?fabric.StaticCanvas.prototype.sendToBack.call(this.group,this):this.canvas&&this.canvas.sendToBack(this),this},bringToFront:function(){return this.group?fabric.StaticCanvas.prototype.bringToFront.call(this.group,this):this.canvas&&this.canvas.bringToFront(this),this},sendBackwards:function(t){return this.group?fabric.StaticCanvas.prototype.sendBackwards.call(this.group,this,t):this.canvas&&this.canvas.sendBackwards(this,t),this},bringForward:function(t){return this.group?fabric.StaticCanvas.prototype.bringForward.call(this.group,this,t):this.canvas&&this.canvas.bringForward(this,t),this},moveTo:function(t){return this.group&&"activeSelection"!==this.group.type?fabric.StaticCanvas.prototype.moveTo.call(this.group,this,t):this.canvas&&this.canvas.moveTo(this,t),this}});!function(){function t(t,e){if(e){if(e.toLive)return t+": url(#SVGID_"+e.id+"); ";var i=new fabric.Color(e),r=t+": "+i.toRgb()+"; ",n=i.getAlpha();return 1!==n&&(r+=t+"-opacity: "+n.toString()+"; "),r}return t+": none; "}var e=fabric.util.toFixed;fabric.util.object.extend(fabric.Object.prototype,{getSvgStyles:function(e){var i=this.fillRule?this.fillRule:"nonzero",r=this.strokeWidth?this.strokeWidth:"0",n=this.strokeDashArray?this.strokeDashArray.join(" "):"none",s=this.strokeDashOffset?this.strokeDashOffset:"0",o=this.strokeLineCap?this.strokeLineCap:"butt",a=this.strokeLineJoin?this.strokeLineJoin:"miter",c=this.strokeMiterLimit?this.strokeMiterLimit:"4",h="undefined"!=typeof this.opacity?this.opacity:"1",l=this.visible?"":" visibility: hidden;",u=e?"":this.getSvgFilter(),f=t("fill",this.fill),d=t("stroke",this.stroke);return[d,"stroke-width: ",r,"; ","stroke-dasharray: ",n,"; ","stroke-linecap: ",o,"; ","stroke-dashoffset: ",s,"; ","stroke-linejoin: ",a,"; ","stroke-miterlimit: ",c,"; ",f,"fill-rule: ",i,"; ","opacity: ",h,";",u,l].join("")},getSvgSpanStyles:function(e,i){var r="; ",n=e.fontFamily?"font-family: "+(-1===e.fontFamily.indexOf("'")&&-1===e.fontFamily.indexOf('"')?"'"+e.fontFamily+"'":e.fontFamily)+r:"",s=e.strokeWidth?"stroke-width: "+e.strokeWidth+r:"",n=n,o=e.fontSize?"font-size: "+e.fontSize+"px"+r:"",a=e.fontStyle?"font-style: "+e.fontStyle+r:"",c=e.fontWeight?"font-weight: "+e.fontWeight+r:"",h=e.fill?t("fill",e.fill):"",l=e.stroke?t("stroke",e.stroke):"",u=this.getSvgTextDecoration(e),f=e.deltaY?"baseline-shift: "+-e.deltaY+"; ":"";return u&&(u="text-decoration: "+u+r),[l,s,n,o,a,c,u,h,f,i?"white-space: pre; ":""].join("")},getSvgTextDecoration:function(t){return["overline","underline","line-through"].filter(function(e){return t[e.replace("-","")]}).join(" ")},getSvgFilter:function(){return this.shadow?"filter: url(#SVGID_"+this.shadow.id+");":""},getSvgCommons:function(){return[this.id?'id="'+this.id+'" ':"",this.clipPath?'clip-path="url(#'+this.clipPath.clipPathId+')" ':""].join("")},getSvgTransform:function(t,e){var i=t?this.calcTransformMatrix():this.calcOwnMatrix(),r='transform="'+fabric.util.matrixToSVG(i);return r+(e||"")+'" '},_setSVGBg:function(t){if(this.backgroundColor){var i=fabric.Object.NUM_FRACTION_DIGITS;t.push(" \n')}},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return" "+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){e=e||{};var i=e.reviver,r=e.additionalTransform||"",n=[this.getSvgTransform(!0,r),this.getSvgCommons()].join(""),s=t.indexOf("COMMON_PARTS");return t[s]=n,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){e=e||{};var i,r,n=e.noStyle,s=e.reviver,o=n?"":'style="'+this.getSvgStyles()+'" ',a=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",c=this.clipPath,h=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",l=c&&c.absolutePositioned,u=this.stroke,f=this.fill,d=this.shadow,g=[],p=t.indexOf("COMMON_PARTS"),v=e.additionalTransform;return c&&(c.clipPathId="CLIPPATH_"+fabric.Object.__uid++,r='\n'+c.toClipPathSVG(s)+"\n"),l&&g.push("\n"),g.push("\n"),i=[o,h,n?"":this.addPaintOrder()," ",v?'transform="'+v+'" ':""].join(""),t[p]=i,f&&f.toLive&&g.push(f.toSVG(this)),u&&u.toLive&&g.push(u.toSVG(this)),d&&g.push(d.toSVG(this)),c&&g.push(r),g.push(t.join("")),g.push("\n"),l&&g.push("\n"),s?s(g.join("")):g.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}();!function(){function t(t,e,r){var n={},s=!0;r.forEach(function(e){n[e]=t[e]}),i(t[e],n,s)}function e(t,i,r){if(t===i)return!0;if(Array.isArray(t)){if(!Array.isArray(i)||t.length!==i.length)return!1;for(var n=0,s=t.length;s>n;n++)if(!e(t[n],i[n]))return!1;return!0}if(t&&"object"==typeof t){var o,a=Object.keys(t);if(!i||"object"!=typeof i||!r&&a.length!==Object.keys(i).length)return!1;for(var n=0,s=a.length;s>n;n++)if(o=a[n],"canvas"!==o&&"group"!==o&&!e(t[o],i[o]))return!1;return!0}}var i=fabric.util.object.extend,r="stateProperties";fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(t){t=t||r;var i="_"+t;return Object.keys(this[i]).length=0;c--)if(n=a[c],this.isControlVisible(n)&&(r=this._getImageLines(e?this.oCoords[n].touchCorner:this.oCoords[n].corner),i=this._findCrossPoints({x:s,y:o},r),0!==i&&i%2===1))return this.__corner=n,n;return!1},forEachControl:function(t){for(var e in this.controls)t(this.controls[e],e,this)},_setCornerCoords:function(){var t=this.oCoords;for(var e in t){var i=this.controls[e];t[e].corner=i.calcCornerCoords(this.angle,this.cornerSize,t[e].x,t[e].y,!1),t[e].touchCorner=i.calcCornerCoords(this.angle,this.touchCornerSize,t[e].x,t[e].y,!0)}},drawSelectionBackground:function(e){if(!this.selectionBackgroundColor||this.canvas&&!this.canvas.interactive||this.canvas&&this.canvas._activeObject!==this)return this;e.save();var i=this.getCenterPoint(),r=this._calculateCurrentDimensions(),n=this.canvas.viewportTransform;return e.translate(i.x,i.y),e.scale(1/n[0],1/n[3]),e.rotate(t(this.angle)),e.fillStyle=this.selectionBackgroundColor,e.fillRect(-r.x/2,-r.y/2,r.x,r.y),e.restore(),this},drawBorders:function(t,e){e=e||{};var i=this._calculateCurrentDimensions(),r=this.borderScaleFactor,n=i.x+r,s=i.y+r,o="undefined"!=typeof e.hasControls?e.hasControls:this.hasControls,a=!1;return t.save(),t.strokeStyle=e.borderColor||this.borderColor,this._setLineDash(t,e.borderDashArray||this.borderDashArray),t.strokeRect(-n/2,-s/2,n,s),o&&(t.beginPath(),this.forEachControl(function(e,i,r){e.withConnection&&e.getVisibility(r,i)&&(a=!0,t.moveTo(e.x*n,e.y*s),t.lineTo(e.x*n+e.offsetX,e.y*s+e.offsetY))}),a&&t.stroke()),t.restore(),this},drawBordersInGroup:function(t,e,i){i=i||{};var r=fabric.util.sizeAfterTransform(this.width,this.height,e),n=this.strokeWidth,s=this.strokeUniform,o=this.borderScaleFactor,a=r.x+n*(s?this.canvas.getZoom():e.scaleX)+o,c=r.y+n*(s?this.canvas.getZoom():e.scaleY)+o;return t.save(),this._setLineDash(t,i.borderDashArray||this.borderDashArray),t.strokeStyle=i.borderColor||this.borderColor,t.strokeRect(-a/2,-c/2,a,c),t.restore(),this},drawControls:function(t,e){e=e||{},t.save();var i,r,n=this.canvas.getRetinaScaling();return t.setTransform(n,0,0,n,0,0),t.strokeStyle=t.fillStyle=e.cornerColor||this.cornerColor,this.transparentCorners||(t.strokeStyle=e.cornerStrokeColor||this.cornerStrokeColor),this._setLineDash(t,e.cornerDashArray||this.cornerDashArray),this.setCoords(),this.group&&(i=this.group.calcTransformMatrix()),this.forEachControl(function(n,s,o){r=o.oCoords[s],n.getVisibility(o,s)&&(i&&(r=fabric.util.transformPoint(r,i)),n.render(t,r.x,r.y,e,o))}),t.restore(),this},isControlVisible:function(t){return this.controls[t]&&this.controls[t].getVisibility(this,t)},setControlVisible:function(t,e){return this._controlsVisibility||(this._controlsVisibility={}),this._controlsVisibility[t]=e,this},setControlsVisibility:function(t){t||(t={});for(var e in t)this.setControlVisible(e,t[e]);return this},onDeselect:function(){},onSelect:function(){}})}();fabric.util.object.extend(fabric.StaticCanvas.prototype,{FX_DURATION:500,fxCenterObjectH:function(t,e){e=e||{};var i=function(){},r=e.onComplete||i,n=e.onChange||i,s=this;return fabric.util.animate({target:this,startValue:t.left,endValue:this.getCenterPoint().x,duration:this.FX_DURATION,onChange:function(e){t.set("left",e),s.requestRenderAll(),n()},onComplete:function(){t.setCoords(),r()}})},fxCenterObjectV:function(t,e){e=e||{};var i=function(){},r=e.onComplete||i,n=e.onChange||i,s=this;return fabric.util.animate({target:this,startValue:t.top,endValue:this.getCenterPoint().y,duration:this.FX_DURATION,onChange:function(e){t.set("top",e),s.requestRenderAll(),n()},onComplete:function(){t.setCoords(),r()}})},fxRemove:function(t,e){e=e||{};var i=function(){},r=e.onComplete||i,n=e.onChange||i,s=this;return fabric.util.animate({target:this,startValue:t.opacity,endValue:0,duration:this.FX_DURATION,onChange:function(e){t.set("opacity",e),s.requestRenderAll(),n()},onComplete:function(){s.remove(t),r()}})}}),fabric.util.object.extend(fabric.Object.prototype,{animate:function(){if(arguments[0]&&"object"==typeof arguments[0]){var t,e,i=[],r=[];for(t in arguments[0])i.push(t);for(var n=0,s=i.length;s>n;n++)t=i[n],e=n!==s-1,r.push(this._animate(t,arguments[0][t],arguments[1],e));return r}return this._animate.apply(this,arguments)},_animate:function(t,e,i,r){var n,s=this;e=e.toString(),i=i?fabric.util.object.clone(i):{},~t.indexOf(".")&&(n=t.split("."));var o=s.colorProperties.indexOf(t)>-1||n&&s.colorProperties.indexOf(n[1])>-1,a=n?this.get(n[0])[n[1]]:this.get(t);"from"in i||(i.from=a),o||(e=~e.indexOf("=")?a+parseFloat(e.replace("=","")):parseFloat(e));var c={target:this,startValue:i.from,endValue:e,byValue:i.by,easing:i.easing,duration:i.duration,abort:i.abort&&function(t,e,r){return i.abort.call(s,t,e,r)},onChange:function(e,o,a){n?s[n[0]][n[1]]=e:s.set(t,e),r||i.onChange&&i.onChange(e,o,a)},onComplete:function(t,e,n){r||(s.setCoords(),i.onComplete&&i.onComplete(t,e,n))}};return o?fabric.util.animateColor(c.startValue,c.endValue,c.duration,c):fabric.util.animate(c)}});!function(t){"use strict";function e(t,e){var i=t.origin,r=t.axis1,n=t.axis2,s=t.dimension,o=e.nearest,a=e.center,c=e.farthest;return function(){switch(this.get(i)){case o:return Math.min(this.get(r),this.get(n));case a:return Math.min(this.get(r),this.get(n))+.5*this.get(s);case c:return Math.max(this.get(r),this.get(n))}}}var i=t.fabric||(t.fabric={}),r=i.util.object.extend,n=i.util.object.clone,s={x1:1,x2:1,y1:1,y2:1};return i.Line?void i.warn("fabric.Line is already defined"):(i.Line=i.util.createClass(i.Object,{type:"line",x1:0,y1:0,x2:0,y2:0,cacheProperties:i.Object.prototype.cacheProperties.concat("x1","x2","y1","y2"),initialize:function(t,e){t||(t=[0,0,0,0]),this.callSuper("initialize",e),this.set("x1",t[0]),this.set("y1",t[1]),this.set("x2",t[2]),this.set("y2",t[3]),this._setWidthHeight(e)},_setWidthHeight:function(t){t||(t={}),this.width=Math.abs(this.x2-this.x1),this.height=Math.abs(this.y2-this.y1),this.left="left"in t?t.left:this._getLeftToOriginX(),this.top="top"in t?t.top:this._getTopToOriginY()},_set:function(t,e){return this.callSuper("_set",t,e),"undefined"!=typeof s[t]&&this._setWidthHeight(),this},_getLeftToOriginX:e({origin:"originX",axis1:"x1",axis2:"x2",dimension:"width"},{nearest:"left",center:"center",farthest:"right"}),_getTopToOriginY:e({origin:"originY",axis1:"y1",axis2:"y2",dimension:"height"},{nearest:"top",center:"center",farthest:"bottom"}),_render:function(t){t.beginPath();var e=this.calcLinePoints();t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.lineWidth=this.strokeWidth;var i=t.strokeStyle;t.strokeStyle=this.stroke||t.fillStyle,this.stroke&&this._renderStroke(t),t.strokeStyle=i},_findCenterFromElement:function(){return{x:(this.x1+this.x2)/2,y:(this.y1+this.y2)/2}},toObject:function(t){return r(this.callSuper("toObject",t),this.calcLinePoints())},_getNonTransformedDimensions:function(){var t=this.callSuper("_getNonTransformedDimensions");return"butt"===this.strokeLineCap&&(0===this.width&&(t.y-=this.strokeWidth),0===this.height&&(t.x-=this.strokeWidth)),t},calcLinePoints:function(){var t=this.x1<=this.x2?-1:1,e=this.y1<=this.y2?-1:1,i=t*this.width*.5,r=e*this.height*.5,n=t*this.width*-.5,s=e*this.height*-.5;return{x1:i,x2:n,y1:r,y2:s}},_toSVG:function(){var t=this.calcLinePoints();return["\n']}}),i.Line.ATTRIBUTE_NAMES=i.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),i.Line.fromElement=function(t,e,n){n=n||{};var s=i.parseAttributes(t,i.Line.ATTRIBUTE_NAMES),o=[s.x1||0,s.y1||0,s.x2||0,s.y2||0];e(new i.Line(o,r(s,n)))},void(i.Line.fromObject=function(t,e){function r(t){delete t.points,e&&e(t)}var s=n(t,!0);s.points=[t.x1,t.y1,t.x2,t.y2],i.Object._fromObject("Line",s,r,"points")}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";function e(t){return"radius"in t&&t.radius>=0}var i=t.fabric||(t.fabric={}),r=i.util.degreesToRadians;return i.Circle?void i.warn("fabric.Circle is already defined."):(i.Circle=i.util.createClass(i.Object,{type:"circle",radius:0,startAngle:0,endAngle:360,cacheProperties:i.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,e=0,n=0,s=(this.endAngle-this.startAngle)%360;if(0===s)t=["\n'];else{var o=r(this.startAngle),a=r(this.endAngle),c=this.radius,h=i.util.cos(o)*c,l=i.util.sin(o)*c,u=i.util.cos(a)*c,f=i.util.sin(a)*c,d=s>180?"1":"0";t=['\n"]}return t},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,r(this.startAngle),r(this.endAngle),!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),i.Circle.ATTRIBUTE_NAMES=i.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),i.Circle.fromElement=function(t,r){var n=i.parseAttributes(t,i.Circle.ATTRIBUTE_NAMES);if(!e(n))throw new Error("value of `r` attribute is required and can not be negative");n.left=(n.left||0)-n.radius,n.top=(n.top||0)-n.radius,r(new i.Circle(n))},void(i.Circle.fromObject=function(t,e){i.Object._fromObject("Circle",t,e)}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={});return e.Triangle?void e.warn("fabric.Triangle is already defined"):(e.Triangle=e.util.createClass(e.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2,i=[-t+" "+e,"0 "+-e,t+" "+e].join(",");return["']}}),void(e.Triangle.fromObject=function(t,i){return e.Object._fromObject("Triangle",t,i)}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=2*Math.PI;return e.Ellipse?void e.warn("fabric.Ellipse is already defined."):(e.Ellipse=e.util.createClass(e.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:e.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,i,!1),t.restore(),this._renderPaintInOrder(t)}}),e.Ellipse.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),e.Ellipse.fromElement=function(t,i){var r=e.parseAttributes(t,e.Ellipse.ATTRIBUTE_NAMES);r.left=(r.left||0)-r.rx,r.top=(r.top||0)-r.ry,i(new e.Ellipse(r))},void(e.Ellipse.fromObject=function(t,i){e.Object._fromObject("Ellipse",t,i)}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend;return e.Rect?void e.warn("fabric.Rect is already defined"):(e.Rect=e.util.createClass(e.Object,{stateProperties:e.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:e.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,s=-this.width/2,o=-this.height/2,a=0!==e||0!==i,c=.4477152502;t.beginPath(),t.moveTo(s+e,o),t.lineTo(s+r-e,o),a&&t.bezierCurveTo(s+r-c*e,o,s+r,o+c*i,s+r,o+i),t.lineTo(s+r,o+n-i),a&&t.bezierCurveTo(s+r,o+n-c*i,s+r-c*e,o+n,s+r-e,o+n),t.lineTo(s+e,o+n),a&&t.bezierCurveTo(s+c*e,o+n,s,o+n-c*i,s,o+n-i),t.lineTo(s,o+i),a&&t.bezierCurveTo(s,o+c*i,s+c*e,o,s+e,o),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){var t=-this.width/2,e=-this.height/2;return["\n']}}),e.Rect.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),e.Rect.fromElement=function(t,r,n){if(!t)return r(null);n=n||{};var s=e.parseAttributes(t,e.Rect.ATTRIBUTE_NAMES);s.left=s.left||0,s.top=s.top||0,s.height=s.height||0,s.width=s.width||0;var o=new e.Rect(i(n?e.util.object.clone(n):{},s));o.visible=o.visible&&o.width>0&&o.height>0,r(o)},void(e.Rect.fromObject=function(t,i){return e.Object._fromObject("Rect",t,i)}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.array.min,n=e.util.array.max,s=e.util.toFixed,o=e.util.projectStrokeOnPoints;return e.Polyline?void e.warn("fabric.Polyline is already defined"):(e.Polyline=e.util.createClass(e.Object,{type:"polyline",points:null,exactBoundingBox:!1,cacheProperties:e.Object.prototype.cacheProperties.concat("points"),initialize:function(t,e){e=e||{},this.points=t||[],this.callSuper("initialize",e),this._setPositionDimensions(e)},_projectStrokeOnPoints:function(){return o(this.points,this,!0)},_setPositionDimensions:function(t){var e,i=this._calcDimensions(t),r=this.exactBoundingBox?this.strokeWidth:0;this.width=i.width-r,this.height=i.height-r,t.fromSVG||(e=this.translateToGivenOrigin({x:i.left-this.strokeWidth/2+r/2,y:i.top-this.strokeWidth/2+r/2},"left","top",this.originX,this.originY)),"undefined"==typeof t.left&&(this.left=t.fromSVG?i.left:e.x),"undefined"==typeof t.top&&(this.top=t.fromSVG?i.top:e.y),this.pathOffset={x:i.left+this.width/2+r/2,y:i.top+this.height/2+r/2}},_calcDimensions:function(){var t=this.exactBoundingBox?this._projectStrokeOnPoints():this.points,e=r(t,"x")||0,i=r(t,"y")||0,s=n(t,"x")||0,o=n(t,"y")||0,a=s-e,c=o-i;return{left:e,top:i,width:a,height:c}},toObject:function(t){return i(this.callSuper("toObject",t),{points:this.points.concat()})},_toSVG:function(){for(var t=[],i=this.pathOffset.x,r=this.pathOffset.y,n=e.Object.NUM_FRACTION_DIGITS,o=0,a=this.points.length;a>o;o++)t.push(s(this.points[o].x-i,n),",",s(this.points[o].y-r,n)," ");return["<"+this.type+" ","COMMON_PARTS",'points="',t.join(""),'" />\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var s=0;i>s;s++)e=this.points[s],t.lineTo(e.x-r,e.y-n);return!0},_render:function(t){this.commonRender(t)&&this._renderPaintInOrder(t)},complexity:function(){return this.get("points").length}}),e.Polyline.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat(),e.Polyline.fromElementGenerator=function(t){return function(r,n,s){if(!r)return n(null);s||(s={});var o=e.parsePointsAttribute(r.getAttribute("points")),a=e.parseAttributes(r,e[t].ATTRIBUTE_NAMES);a.fromSVG=!0,n(new e[t](o,i(a,s)))}},e.Polyline.fromElement=e.Polyline.fromElementGenerator("Polyline"),void(e.Polyline.fromObject=function(t,i){return e.Object._fromObject("Polyline",t,i,"points")}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.projectStrokeOnPoints;return e.Polygon?void e.warn("fabric.Polygon is already defined"):(e.Polygon=e.util.createClass(e.Polyline,{type:"polygon",_projectStrokeOnPoints:function(){return i(this.points,this)},_render:function(t){this.commonRender(t)&&(t.closePath(),this._renderPaintInOrder(t))}}),e.Polygon.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat(),e.Polygon.fromElement=e.Polyline.fromElementGenerator("Polygon"),void(e.Polygon.fromObject=function(t,i){e.Object._fromObject("Polygon",t,i,"points")}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.array.min,r=e.util.array.max,n=e.util.object.extend,s=e.util.object.clone,o=e.util.toFixed;return e.Path?void e.warn("fabric.Path is already defined"):(e.Path=e.util.createClass(e.Object,{type:"path",path:null,cacheProperties:e.Object.prototype.cacheProperties.concat("path","fillRule"),stateProperties:e.Object.prototype.stateProperties.concat("path"),initialize:function(t,e){e=s(e||{}),delete e.path,this.callSuper("initialize",e),this._setPath(t||[],e)},_setPath:function(t,i){this.path=e.util.makePathSimpler(Array.isArray(t)?t:e.util.parsePath(t)),e.Polyline.prototype._setPositionDimensions.call(this,i||{})},_renderPathCommands:function(t){var e,i=0,r=0,n=0,s=0,o=0,a=0,c=-this.pathOffset.x,h=-this.pathOffset.y;t.beginPath();for(var l=0,u=this.path.length;u>l;++l)switch(e=this.path[l],e[0]){case"L":n=e[1],s=e[2],t.lineTo(n+c,s+h);break;case"M":n=e[1],s=e[2],i=n,r=s,t.moveTo(n+c,s+h);break;case"C":n=e[5],s=e[6],o=e[3],a=e[4],t.bezierCurveTo(e[1]+c,e[2]+h,o+c,a+h,n+c,s+h);break;case"Q":t.quadraticCurveTo(e[1]+c,e[2]+h,e[3]+c,e[4]+h),n=e[3],s=e[4],o=e[1],a=e[2];break;case"z":case"Z":n=i,s=r,t.closePath()}},_render:function(t){this._renderPathCommands(t),this._renderPaintInOrder(t)},toString:function(){return"#"},toObject:function(t){return n(this.callSuper("toObject",t),{path:this.path.map(function(t){return t.slice()})})},toDatalessObject:function(t){var e=this.toObject(["sourcePath"].concat(t));return e.sourcePath&&delete e.path,e},_toSVG:function(){var t=e.util.joinPath(this.path);return["\n"]},_getOffsetTransform:function(){var t=e.Object.NUM_FRACTION_DIGITS;return" translate("+o(-this.pathOffset.x,t)+", "+o(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return" "+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,n,s=[],o=[],a=0,c=0,h=0,l=0,u=0,f=this.path.length;f>u;++u){switch(t=this.path[u],t[0]){case"L":h=t[1],l=t[2],n=[];break;case"M":h=t[1],l=t[2],a=h,c=l,n=[];break;case"C":n=e.util.getBoundsOfCurve(h,l,t[1],t[2],t[3],t[4],t[5],t[6]),h=t[5],l=t[6];break;case"Q":n=e.util.getBoundsOfCurve(h,l,t[1],t[2],t[1],t[2],t[3],t[4]),h=t[3],l=t[4];break;case"z":case"Z":h=a,l=c}n.forEach(function(t){s.push(t.x),o.push(t.y)}),s.push(h),o.push(l)}var d=i(s)||0,g=i(o)||0,p=r(s)||0,v=r(o)||0,m=p-d,b=v-g;return{left:d,top:g,width:m,height:b}}}),e.Path.fromObject=function(t,i){if("string"==typeof t.sourcePath){var r=t.sourcePath;e.loadSVGFromURL(r,function(e){var r=e[0];r.setOptions(t),i&&i(r)})}else e.Object._fromObject("Path",t,i,"path")},e.Path.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat(["d"]),void(e.Path.fromElement=function(t,i,r){var s=e.parseAttributes(t,e.Path.ATTRIBUTE_NAMES);s.fromSVG=!0,i(new e.Path(s.d,n(s,r)))}))}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.array.min,r=e.util.array.max;e.Group||(e.Group=e.util.createClass(e.Object,e.Collection,{type:"group",strokeWidth:0,subTargetCheck:!1,cacheProperties:[],useSetOnGroup:!1,initialize:function(t,e,i){e=e||{},this._objects=[],i&&this.callSuper("initialize",e),this._objects=t||[];for(var r=this._objects.length;r--;)this._objects[r].group=this;if(i)this._updateObjectsACoords();else{var n=e&&e.centerPoint;void 0!==e.originX&&(this.originX=e.originX),void 0!==e.originY&&(this.originY=e.originY),n||this._calcBounds(),this._updateObjectsCoords(n),delete e.centerPoint,this.callSuper("initialize",e)}this.setCoords()},_updateObjectsACoords:function(){for(var t=!0,e=this._objects.length;e--;)this._objects[e].setCoords(t)},_updateObjectsCoords:function(t){for(var t=t||this.getCenterPoint(),e=this._objects.length;e--;)this._updateObjectCoords(this._objects[e],t)},_updateObjectCoords:function(t,e){var i=t.left,r=t.top,n=!0;t.set({left:i-e.x,top:r-e.y}),t.group=this,t.setCoords(n)},toString:function(){return"#"},addWithUpdate:function(t){var i=!!this.group;return this._restoreObjectsState(),e.util.resetObjectTransform(this),t&&(i&&e.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,i?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),e.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,i){var r=this._objects.length;if(this.useSetOnGroup)for(;r--;)this._objects[r].setOnGroup(t,i);if("canvas"===t)for(;r--;)this._objects[r]._set(t,i);e.Object.prototype._set.call(this,t,i)},toObject:function(t){var i=this.includeDefaultValues,r=this._objects.filter(function(t){return!t.excludeFromExport}).map(function(e){var r=e.includeDefaultValues;e.includeDefaultValues=i;var n=e.toObject(t);return e.includeDefaultValues=r,n}),n=e.Object.prototype.toObject.call(this,t);return n.objects=r,n},toDatalessObject:function(t){var i,r=this.sourcePath;if(r)i=r;else{var n=this.includeDefaultValues;i=this._objects.map(function(e){var i=e.includeDefaultValues;e.includeDefaultValues=n;var r=e.toDatalessObject(t);return e.includeDefaultValues=i,r})}var s=e.Object.prototype.toDatalessObject.call(this,t);return s.objects=i,s},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=e.Object.prototype.shouldCache.call(this);if(t)for(var i=0,r=this._objects.length;r>i;i++)if(this._objects[i].willDrawShadow())return this.ownCaching=!1,!1;return t},willDrawShadow:function(){if(e.Object.prototype.willDrawShadow.call(this))return!0;for(var t=0,i=this._objects.length;i>t;t++)if(this._objects[t].willDrawShadow())return!0;return!1},isOnACache:function(){return this.ownCaching||this.group&&this.group.isOnACache()},drawObject:function(t){for(var e=0,i=this._objects.length;i>e;e++)this._objects[e].render(t);this._drawClipPath(t,this.clipPath)},isCacheDirty:function(t){if(this.callSuper("isCacheDirty",t))return!0;if(!this.statefullCache)return!1;for(var e=0,i=this._objects.length;i>e;e++)if(this._objects[e].isCacheDirty(!0)){if(this._cacheCanvas){var r=this.cacheWidth/this.zoomX,n=this.cacheHeight/this.zoomY;this._cacheContext.clearRect(-r/2,-n/2,r,n)}return!0}return!1},_restoreObjectsState:function(){var t=this.calcOwnMatrix();return this._objects.forEach(function(i){e.util.addTransformToObject(i,t),delete i.group,i.setCoords()}),this},destroy:function(){return this._objects.forEach(function(t){t.set("dirty",!0)}),this._restoreObjectsState()},dispose:function(){this.callSuper("dispose"),this.forEachObject(function(t){t.dispose&&t.dispose()}),this._objects=[]},toActiveSelection:function(){if(this.canvas){var t=this._objects,i=this.canvas;this._objects=[];var r=this.toObject();delete r.objects;var n=new e.ActiveSelection([]);return n.set(r),n.type="activeSelection",i.remove(this),t.forEach(function(t){t.group=n,t.dirty=!0,i.add(t)}),n.canvas=i,n._objects=t,i._activeObject=n,n.setCoords(),n}},ungroupOnCanvas:function(){return this._restoreObjectsState()},setObjectsCoords:function(){var t=!0;return this.forEachObject(function(e){e.setCoords(t)}),this},_calcBounds:function(t){for(var e,i,r,n,s=[],o=[],a=["tr","br","bl","tl"],c=0,h=this._objects.length,l=a.length;h>c;++c){for(e=this._objects[c],r=e.calcACoords(),n=0;l>n;n++)i=a[n],s.push(r[i].x),o.push(r[i].y);e.aCoords=r}this._getBounds(s,o,t)},_getBounds:function(t,n,s){var o=new e.Point(i(t),i(n)),a=new e.Point(r(t),r(n)),c=o.y||0,h=o.x||0,l=a.x-o.x||0,u=a.y-o.y||0;this.width=l,this.height=u,s||this.setPositionByOrigin({x:h,y:c},"left","top")},_toSVG:function(t){for(var e=["\n"],i=0,r=this._objects.length;r>i;i++)e.push(" ",this._objects[i].toSVG(t));return e.push("\n"),e},getSvgStyles:function(){var t="undefined"!=typeof this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;r>i;i++)e.push(" ",this._objects[i].toClipPathSVG(t));return this._createBaseClipPathSVGMarkup(e,{reviver:t})}}),e.Group.fromObject=function(t,i){var r=t.objects,n=e.util.object.clone(t,!0);return delete n.objects,"string"==typeof r?void e.loadSVGFromURL(r,function(s){var o=e.util.groupSVGElements(s,t,r);o.set(n),i&&i(o)}):void e.util.enlivenObjects(r,function(r){var n=e.util.object.clone(t,!0);delete n.objects,e.util.enlivenObjectEnlivables(t,n,function(){i&&i(new e.Group(r,n,!0))})})})}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={});e.ActiveSelection||(e.ActiveSelection=e.util.createClass(e.Group,{type:"activeSelection",initialize:function(t,i){i=i||{},this._objects=t||[];for(var r=this._objects.length;r--;)this._objects[r].group=this;i.originX&&(this.originX=i.originX),i.originY&&(this.originY=i.originY),this._calcBounds(),this._updateObjectsCoords(),e.Object.prototype.initialize.call(this,i),this.setCoords()},toGroup:function(){var t=this._objects.concat();this._objects=[];var i=e.Object.prototype.toObject.call(this),r=new e.Group([]);if(delete i.type,r.set(i),t.forEach(function(t){t.canvas.remove(t),t.group=r}),r._objects=t,!this.canvas)return r;var n=this.canvas;return n.add(r),n._activeObject=r,r.setCoords(),r},onDeselect:function(){return this.destroy(),!1},toString:function(){return"#"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),i=i||{},"undefined"==typeof i.hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;n>r;r++)this._objects[r]._renderControls(t,i);t.restore()}}),e.ActiveSelection.fromObject=function(t,i){e.util.enlivenObjects(t.objects,function(r){delete t.objects,i&&i(new e.ActiveSelection(r,t,!0))})})}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=fabric.util.object.extend;return t.fabric||(t.fabric={}),t.fabric.Image?void fabric.warn("fabric.Image is already defined."):(fabric.Image=fabric.util.createClass(fabric.Object,{type:"image",strokeWidth:0,srcFromAttribute:!1,_lastScaleX:1,_lastScaleY:1,_filterScalingX:1,_filterScalingY:1,minimumScaleTrigger:.5,stateProperties:fabric.Object.prototype.stateProperties.concat("cropX","cropY"),cacheProperties:fabric.Object.prototype.cacheProperties.concat("cropX","cropY"),cacheKey:"",cropX:0,cropY:0,imageSmoothing:!0,initialize:function(t,e){e||(e={}),this.filters=[],this.cacheKey="texture"+fabric.Object.__uid++,this.callSuper("initialize",e),this._initElement(t,e)},getElement:function(){return this._element||{}},setElement:function(t,e){return this.removeTexture(this.cacheKey),this.removeTexture(this.cacheKey+"_filtered"),this._element=t,this._originalElement=t,this._initConfig(e),0!==this.filters.length&&this.applyFilters(),this.resizeFilter&&this.applyResizeFilters(),this},removeTexture:function(t){var e=fabric.filterBackend;e&&e.evictCachesForKey&&e.evictCachesForKey(t)},dispose:function(){this.callSuper("dispose"),this.removeTexture(this.cacheKey),this.removeTexture(this.cacheKey+"_filtered"),this._cacheContext=void 0,["_originalElement","_element","_filteredEl","_cacheCanvas"].forEach(function(t){fabric.util.cleanUpJsdomNode(this[t]),this[t]=void 0}.bind(this))},getCrossOrigin:function(){return this._originalElement&&(this._originalElement.crossOrigin||null)},getOriginalSize:function(){var t=this.getElement();return{width:t.naturalWidth||t.width,height:t.naturalHeight||t.height}},_stroke:function(t){if(this.stroke&&0!==this.strokeWidth){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,-i),t.lineTo(e,-i),t.lineTo(e,i),t.lineTo(-e,i),t.lineTo(-e,-i),t.closePath()}},toObject:function(t){var i=[];this.filters.forEach(function(t){t&&i.push(t.toObject())});var r=e(this.callSuper("toObject",["cropX","cropY"].concat(t)),{src:this.getSrc(),crossOrigin:this.getCrossOrigin(),filters:i});return this.resizeFilter&&(r.resizeFilter=this.resizeFilter.toObject()),r},hasCrop:function(){return this.cropX||this.cropY||this.width\n',' \n',"\n"),o=' clip-path="url(#imageCrop_'+c+')" '}if(this.imageSmoothing||(a='" image-rendering="optimizeSpeed'),i.push(" \n"),this.stroke||this.strokeDashArray){var h=this.fill;this.fill=null,t=[" \n'],this.fill=h}return e="fill"!==this.paintFirst?e.concat(t,i):e.concat(i,t)},getSrc:function(t){var e=t?this._element:this._originalElement;return e?e.toDataURL?e.toDataURL():this.srcFromAttribute?e.getAttribute("src"):e.src:this.src||""},setSrc:function(t,e,i){return fabric.util.loadImage(t,function(t,r){this.setElement(t,i),this._setWidthHeight(),e&&e(this,r)},this,i&&i.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,n=i.scaleY,s=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||r>e&&n>e)return this._element=s,this._filterScalingX=1,this._filterScalingY=1,this._lastScaleX=r,void(this._lastScaleY=n);fabric.filterBackend||(fabric.filterBackend=fabric.initFilterBackend());var o=fabric.util.createCanvasElement(),a=this._filteredEl?this.cacheKey+"_filtered":this.cacheKey,c=s.width,h=s.height;o.width=c,o.height=h,this._element=o,this._lastScaleX=t.scaleX=r,this._lastScaleY=t.scaleY=n,fabric.filterBackend.applyFilters([t],s,c,h,this._element,a),this._filterScalingX=o.width/this._originalElement.width,this._filterScalingY=o.height/this._originalElement.height},applyFilters:function(t){if(t=t||this.filters||[],t=t.filter(function(t){return t&&!t.isNeutralState()}),this.set("dirty",!0),this.removeTexture(this.cacheKey+"_filtered"),0===t.length)return this._element=this._originalElement,this._filteredEl=null,this._filterScalingX=1,this._filterScalingY=1,this;var e=this._originalElement,i=e.naturalWidth||e.width,r=e.naturalHeight||e.height;if(this._element===this._originalElement){var n=fabric.util.createCanvasElement();n.width=i,n.height=r,this._element=n,this._filteredEl=n}else this._element=this._filteredEl,this._filteredEl.getContext("2d").clearRect(0,0,i,r),this._lastScaleX=1,this._lastScaleY=1;return fabric.filterBackend||(fabric.filterBackend=fabric.initFilterBackend()),fabric.filterBackend.applyFilters(t,this._originalElement,i,r,this._element,this.cacheKey),(this._originalElement.width!==this._element.width||this._originalElement.height!==this._element.height)&&(this._filterScalingX=this._element.width/this._originalElement.width,this._filterScalingY=this._element.height/this._originalElement.height),this},_render:function(t){fabric.util.setImageSmoothing(t,this.imageSmoothing),this.isMoving!==!0&&this.resizeFilter&&this._needsResize()&&this.applyResizeFilters(),this._stroke(t),this._renderPaintInOrder(t)},drawCacheOnCanvas:function(t){fabric.util.setImageSmoothing(t,this.imageSmoothing),fabric.Object.prototype.drawCacheOnCanvas.call(this,t)},shouldCache:function(){return this.needsItsOwnCache()},_renderFill:function(t){var e=this._element;if(e){var i=this._filterScalingX,r=this._filterScalingY,n=this.width,s=this.height,o=Math.min,a=Math.max,c=a(this.cropX,0),h=a(this.cropY,0),l=e.naturalWidth||e.width,u=e.naturalHeight||e.height,f=c*i,d=h*r,g=o(n*i,l-f),p=o(s*r,u-d),v=-n/2,m=-s/2,b=o(n,l/i-c),y=o(s,u/r-h);e&&t.drawImage(e,f,d,g,p,v,m,b,y)}},_needsResize:function(){var t=this.getTotalObjectScaling();return t.scaleX!==this._lastScaleX||t.scaleY!==this._lastScaleY},_resetWidthHeight:function(){this.set(this.getOriginalSize())},_initElement:function(t,e){this.setElement(fabric.util.getById(t),e),fabric.util.addClass(this.getElement(),fabric.Image.CSS_CANVAS)},_initConfig:function(t){t||(t={}),this.setOptions(t),this._setWidthHeight(t)},_initFilters:function(t,e){t&&t.length?fabric.util.enlivenObjects(t,function(t){e&&e(t)},"fabric.Image.filters"):e&&e()},_setWidthHeight:function(t){t||(t={});var e=this.getElement();this.width=t.width||e.naturalWidth||e.width||0,this.height=t.height||e.naturalHeight||e.height||0},parsePreserveAspectRatioAttribute:function(){var t,e=fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio||""),i=this._element.width,r=this._element.height,n=1,s=1,o=0,a=0,c=0,h=0,l=this.width,u=this.height,f={width:l,height:u};return!e||"none"===e.alignX&&"none"===e.alignY?(n=l/i,s=u/r):("meet"===e.meetOrSlice&&(n=s=fabric.util.findScaleToFit(this._element,f),t=(l-i*n)/2,"Min"===e.alignX&&(o=-t),"Max"===e.alignX&&(o=t),t=(u-r*s)/2,"Min"===e.alignY&&(a=-t),"Max"===e.alignY&&(a=t)),"slice"===e.meetOrSlice&&(n=s=fabric.util.findScaleToCover(this._element,f),t=i-l/n,"Mid"===e.alignX&&(c=t/2),"Max"===e.alignX&&(c=t),t=r-u/s,"Mid"===e.alignY&&(h=t/2),"Max"===e.alignY&&(h=t),i=l/n,r=u/s)),{width:i,height:r,scaleX:n,scaleY:s,offsetLeft:o,offsetTop:a,cropX:c,cropY:h}}}),fabric.Image.CSS_CANVAS="canvas-img",fabric.Image.prototype.getSvgSrc=fabric.Image.prototype.getSrc,fabric.Image.fromObject=function(t,e){var i=fabric.util.object.clone(t);fabric.util.loadImage(i.src,function(t,r){return r?void(e&&e(null,!0)):void fabric.Image.prototype._initFilters.call(i,i.filters,function(r){i.filters=r||[],fabric.Image.prototype._initFilters.call(i,[i.resizeFilter],function(r){i.resizeFilter=r[0],fabric.util.enlivenObjectEnlivables(i,i,function(){var r=new fabric.Image(t,i);e(r,!1)})})})},null,i.crossOrigin)},fabric.Image.fromURL=function(t,e,i){fabric.util.loadImage(t,function(t,r){e&&e(new fabric.Image(t,i),r)},null,i&&i.crossOrigin)},fabric.Image.ATTRIBUTE_NAMES=fabric.SHARED_ATTRIBUTES.concat("x y width height preserveAspectRatio xlink:href crossOrigin image-rendering".split(" ")),void(fabric.Image.fromElement=function(t,i,r){var n=fabric.parseAttributes(t,fabric.Image.ATTRIBUTE_NAMES);fabric.Image.fromURL(n["xlink:href"],i,e(r?fabric.util.object.clone(r):{},n))}))}("undefined"!=typeof exports?exports:this);fabric.util.object.extend(fabric.Object.prototype,{_getAngleValueForStraighten:function(){var t=this.angle%360;return t>0?90*Math.round((t-1)/90):90*Math.round(t/90)},straighten:function(){return this.rotate(this._getAngleValueForStraighten())},fxStraighten:function(t){t=t||{};var e=function(){},i=t.onComplete||e,r=t.onChange||e,n=this;return fabric.util.animate({target:this,startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(t){n.rotate(t),r()},onComplete:function(){n.setCoords(),i()}})}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(t){return t.straighten(),this.requestRenderAll(),this},fxStraightenObject:function(t){return t.fxStraighten({onChange:this.requestRenderAllBound})}});function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,o=t.destinationHeight;(i!==n||r!==o)&&(e.width=n,e.height=o)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var o=i.height-r.height;n.drawImage(i,0,o,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas,r=i.getContext("2d"),n=e.destinationWidth,o=e.destinationHeight,s=n*o*4,a=new Uint8Array(this.imageBuffer,0,s),c=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,n,o,t.RGBA,t.UNSIGNED_BYTE,a);var h=new ImageData(c,n,o);r.putImageData(h,0,0)}!function(){"use strict";function t(t,e){var i="precision "+e+" float;\nvoid main(){}",r=t.createShader(t.FRAGMENT_SHADER);return t.shaderSource(r,i),t.compileShader(r),t.getShaderParameter(r,t.COMPILE_STATUS)?!0:!1}function e(t){t&&t.tileSize&&(this.tileSize=t.tileSize),this.setupGLContext(this.tileSize,this.tileSize),this.captureGPUInfo()}fabric.isWebglSupported=function(e){if(fabric.isLikelyNode)return!1;e=e||fabric.WebglFilterBackend.prototype.tileSize;var i=document.createElement("canvas"),r=i.getContext("webgl")||i.getContext("experimental-webgl"),n=!1;if(r){fabric.maxTextureSize=r.getParameter(r.MAX_TEXTURE_SIZE),n=fabric.maxTextureSize>=e;for(var o=["highp","mediump","lowp"],s=0;3>s;s++)if(t(r,o[s])){fabric.webGlPrecision=o[s];break}}return this.isSupported=n,n},fabric.WebglFilterBackend=e,e.prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i,r="undefined"!=typeof window.performance;try{new ImageData(1,1),i=!0}catch(n){i=!1}var o="undefined"!=typeof ArrayBuffer,s="undefined"!=typeof Uint8ClampedArray;if(r&&i&&o&&s){var a=fabric.util.createCanvasElement(),c=new ArrayBuffer(t*e*4);if(fabric.forceGLPutImageData)return this.imageBuffer=c,void(this.copyGLTo2D=copyGLTo2DPutImageData);var h,l,u,f={imageBuffer:c,destinationWidth:t,destinationHeight:e,targetCanvas:a};a.width=t,a.height=e,h=window.performance.now(),copyGLTo2DDrawImage.call(f,this.gl,f),l=window.performance.now()-h,h=window.performance.now(),copyGLTo2DPutImageData.call(f,this.gl,f),u=window.performance.now()-h,l>u?(this.imageBuffer=c,this.copyGLTo2D=copyGLTo2DPutImageData):this.copyGLTo2D=copyGLTo2DDrawImage}},createWebGLCanvas:function(t,e){var i=fabric.util.createCanvasElement();i.width=t,i.height=e;var r={alpha:!0,premultipliedAlpha:!1,depth:!1,stencil:!1,antialias:!1},n=i.getContext("webgl",r);n||(n=i.getContext("experimental-webgl",r)),n&&(n.clearColor(0,0,0,0),this.canvas=i,this.gl=n)},applyFilters:function(t,e,i,r,n,o){var s,a=this.gl;o&&(s=this.getCachedTexture(o,e));var c={originalWidth:e.width||e.originalWidth,originalHeight:e.height||e.originalHeight,sourceWidth:i,sourceHeight:r,destinationWidth:i,destinationHeight:r,context:a,sourceTexture:this.createTexture(a,i,r,!s&&e),targetTexture:this.createTexture(a,i,r),originalTexture:s||this.createTexture(a,i,r,!s&&e),passes:t.length,webgl:!0,aPosition:this.aPosition,programCache:this.programCache,pass:0,filterBackend:this,targetCanvas:n},h=a.createFramebuffer();return a.bindFramebuffer(a.FRAMEBUFFER,h),t.forEach(function(t){t&&t.applyTo(c)}),resizeCanvasIfNeeded(c),this.copyGLTo2D(a,c),a.bindTexture(a.TEXTURE_2D,null),a.deleteTexture(c.sourceTexture),a.deleteTexture(c.targetTexture),a.deleteFramebuffer(h),n.getContext("2d").setTransform(1,0,0,1,0,0),c},dispose:function(){this.canvas&&(this.canvas=null,this.gl=null),this.clearWebGLCaches()},clearWebGLCaches:function(){this.programCache={},this.textureCache={}},createTexture:function(t,e,i,r){var n=t.createTexture();return t.bindTexture(t.TEXTURE_2D,n),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),r?t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,r):t.texImage2D(t.TEXTURE_2D,0,t.RGBA,e,i,0,t.RGBA,t.UNSIGNED_BYTE,null),n},getCachedTexture:function(t,e){if(this.textureCache[t])return this.textureCache[t];var i=this.createTexture(this.gl,e.width,e.height,e);return this.textureCache[t]=i,i},evictCachesForKey:function(t){this.textureCache[t]&&(this.gl.deleteTexture(this.textureCache[t]),delete this.textureCache[t])},copyGLTo2D:copyGLTo2DDrawImage,captureGPUInfo:function(){if(this.gpuInfo)return this.gpuInfo;var t=this.gl,e={renderer:"",vendor:""};if(!t)return e;var i=t.getExtension("WEBGL_debug_renderer_info");if(i){var r=t.getParameter(i.UNMASKED_RENDERER_WEBGL),n=t.getParameter(i.UNMASKED_VENDOR_WEBGL);r&&(e.renderer=r.toLowerCase()),n&&(e.vendor=n.toLowerCase())}return this.gpuInfo=e,e}}}();!function(){"use strict";function t(){}var e=function(){};fabric.Canvas2dFilterBackend=t,t.prototype={evictCachesForKey:e,dispose:e,clearWebGLCaches:e,resources:{},applyFilters:function(t,e,i,r,n){var o=n.getContext("2d");o.drawImage(e,0,0,i,r);var s=o.getImageData(0,0,i,r),a=o.getImageData(0,0,i,r),c={sourceWidth:i,sourceHeight:r,imageData:s,originalEl:e,originalImageData:a,canvasEl:n,ctx:o,filterBackend:this};return t.forEach(function(t){t.applyTo(c)}),(c.imageData.width!==i||c.imageData.height!==r)&&(n.width=c.imageData.width,n.height=c.imageData.height),o.putImageData(c.imageData,0,0),c}}}();fabric.Image=fabric.Image||{},fabric.Image.filters=fabric.Image.filters||{},fabric.Image.filters.BaseFilter=fabric.util.createClass({type:"BaseFilter",vertexSource:"attribute vec2 aPosition;\nvarying vec2 vTexCoord;\nvoid main() {\nvTexCoord = aPosition;\ngl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n}",fragmentSource:"precision highp float;\nvarying vec2 vTexCoord;\nuniform sampler2D uTexture;\nvoid main() {\ngl_FragColor = texture2D(uTexture, vTexCoord);\n}",initialize:function(t){t&&this.setOptions(t)},setOptions:function(t){for(var e in t)this[e]=t[e]},createProgram:function(t,e,i){e=e||this.fragmentSource,i=i||this.vertexSource,"highp"!==fabric.webGlPrecision&&(e=e.replace(/precision highp float/g,"precision "+fabric.webGlPrecision+" float"));var r=t.createShader(t.VERTEX_SHADER);if(t.shaderSource(r,i),t.compileShader(r),!t.getShaderParameter(r,t.COMPILE_STATUS))throw new Error("Vertex shader compile error for "+this.type+": "+t.getShaderInfoLog(r));var n=t.createShader(t.FRAGMENT_SHADER);if(t.shaderSource(n,e),t.compileShader(n),!t.getShaderParameter(n,t.COMPILE_STATUS))throw new Error("Fragment shader compile error for "+this.type+": "+t.getShaderInfoLog(n));var o=t.createProgram();if(t.attachShader(o,r),t.attachShader(o,n),t.linkProgram(o),!t.getProgramParameter(o,t.LINK_STATUS))throw new Error('Shader link error for "${this.type}" '+t.getProgramInfoLog(o));var s=this.getAttributeLocations(t,o),a=this.getUniformLocations(t,o)||{};return a.uStepW=t.getUniformLocation(o,"uStepW"),a.uStepH=t.getUniformLocation(o,"uStepH"),{program:o,attributeLocations:s,uniformLocations:a}},getAttributeLocations:function(t,e){return{aPosition:t.getAttribLocation(e,"aPosition")}},getUniformLocations:function(){return{}},sendAttributeData:function(t,e,i){var r=e.aPosition,n=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,n),t.enableVertexAttribArray(r),t.vertexAttribPointer(r,2,t.FLOAT,!1,0,0),t.bufferData(t.ARRAY_BUFFER,i,t.STATIC_DRAW)},_setupFrameBuffer:function(t){var e,i,r=t.context;t.passes>1?(e=t.destinationWidth,i=t.destinationHeight,(t.sourceWidth!==e||t.sourceHeight!==i)&&(r.deleteTexture(t.targetTexture),t.targetTexture=t.filterBackend.createTexture(r,e,i)),r.framebufferTexture2D(r.FRAMEBUFFER,r.COLOR_ATTACHMENT0,r.TEXTURE_2D,t.targetTexture,0)):(r.bindFramebuffer(r.FRAMEBUFFER,null),r.finish())},_swapTextures:function(t){t.passes--,t.pass++;var e=t.targetTexture;t.targetTexture=t.sourceTexture,t.sourceTexture=e},isNeutralState:function(){var t=this.mainParameter,e=fabric.Image.filters[this.type].prototype;if(t){if(Array.isArray(e[t])){for(var i=e[t].length;i--;)if(this[t][i]!==e[t][i])return!1;return!0}return e[t]===this[t]}return!1},applyTo:function(t){t.webgl?(this._setupFrameBuffer(t),this.applyToWebGL(t),this._swapTextures(t)):this.applyTo2d(t)},retrieveShader:function(t){return t.programCache.hasOwnProperty(this.type)||(t.programCache[this.type]=this.createProgram(t.context)),t.programCache[this.type]},applyToWebGL:function(t){var e=t.context,i=this.retrieveShader(t);0===t.pass&&t.originalTexture?e.bindTexture(e.TEXTURE_2D,t.originalTexture):e.bindTexture(e.TEXTURE_2D,t.sourceTexture),e.useProgram(i.program),this.sendAttributeData(e,i.attributeLocations,t.aPosition),e.uniform1f(i.uniformLocations.uStepW,1/t.sourceWidth),e.uniform1f(i.uniformLocations.uStepH,1/t.sourceHeight),this.sendUniformData(e,i.uniformLocations),e.viewport(0,0,t.destinationWidth,t.destinationHeight),e.drawArrays(e.TRIANGLE_STRIP,0,4)},bindAdditionalTexture:function(t,e,i){t.activeTexture(i),t.bindTexture(t.TEXTURE_2D,e),t.activeTexture(t.TEXTURE0)},unbindAdditionalTexture:function(t,e){t.activeTexture(e),t.bindTexture(t.TEXTURE_2D,null),t.activeTexture(t.TEXTURE0)},getMainParameter:function(){return this[this.mainParameter]},setMainParameter:function(t){this[this.mainParameter]=t},sendUniformData:function(){},createHelpLayer:function(t){if(!t.helpLayer){var e=document.createElement("canvas");e.width=t.sourceWidth,e.height=t.sourceHeight,t.helpLayer=e}},toObject:function(){var t={type:this.type},e=this.mainParameter;return e&&(t[e]=this[e]),t},toJSON:function(){return this.toObject()}}),fabric.Image.filters.BaseFilter.fromObject=function(t,e){var i=new fabric.Image.filters[t.type](t);return e&&e(i),i};!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.ColorMatrix=r(i.BaseFilter,{type:"ColorMatrix",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nvarying vec2 vTexCoord;\nuniform mat4 uColorMatrix;\nuniform vec4 uConstants;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\ncolor *= uColorMatrix;\ncolor += uConstants;\ngl_FragColor = color;\n}",matrix:[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0],mainParameter:"matrix",colorsOnly:!0,initialize:function(t){this.callSuper("initialize",t),this.matrix=this.matrix.slice(0)},applyTo2d:function(t){var e,i,r,n,o,s=t.imageData,a=s.data,c=a.length,l=this.matrix,h=this.colorsOnly;for(o=0;c>o;o+=4)e=a[o],i=a[o+1],r=a[o+2],h?(a[o]=e*l[0]+i*l[1]+r*l[2]+255*l[4],a[o+1]=e*l[5]+i*l[6]+r*l[7]+255*l[9],a[o+2]=e*l[10]+i*l[11]+r*l[12]+255*l[14]):(n=a[o+3],a[o]=e*l[0]+i*l[1]+r*l[2]+n*l[3]+255*l[4],a[o+1]=e*l[5]+i*l[6]+r*l[7]+n*l[8]+255*l[9],a[o+2]=e*l[10]+i*l[11]+r*l[12]+n*l[13]+255*l[14],a[o+3]=e*l[15]+i*l[16]+r*l[17]+n*l[18]+255*l[19])},getUniformLocations:function(t,e){return{uColorMatrix:t.getUniformLocation(e,"uColorMatrix"),uConstants:t.getUniformLocation(e,"uConstants")}},sendUniformData:function(t,e){var i=this.matrix,r=[i[0],i[1],i[2],i[3],i[5],i[6],i[7],i[8],i[10],i[11],i[12],i[13],i[15],i[16],i[17],i[18]],n=[i[4],i[9],i[14],i[19]];t.uniformMatrix4fv(e.uColorMatrix,!1,r),t.uniform4fv(e.uConstants,n)}}),e.Image.filters.ColorMatrix.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Brightness=r(i.BaseFilter,{type:"Brightness",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uBrightness;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\ncolor.rgb += uBrightness;\ngl_FragColor = color;\n}",brightness:0,mainParameter:"brightness",applyTo2d:function(t){if(0!==this.brightness){var e,i=t.imageData,r=i.data,n=r.length,o=Math.round(255*this.brightness);for(e=0;n>e;e+=4)r[e]=r[e]+o,r[e+1]=r[e+1]+o,r[e+2]=r[e+2]+o}},getUniformLocations:function(t,e){return{uBrightness:t.getUniformLocation(e,"uBrightness")}},sendUniformData:function(t,e){t.uniform1f(e.uBrightness,this.brightness)}}),e.Image.filters.Brightness.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.Convolute=n(r.BaseFilter,{type:"Convolute",opaque:!1,matrix:[0,0,0,0,1,0,0,0,0],fragmentSource:{Convolute_3_1:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[9];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 0);\nfor (float h = 0.0; h < 3.0; h+=1.0) {\nfor (float w = 0.0; w < 3.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\ncolor += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n}\n}\ngl_FragColor = color;\n}",Convolute_3_0:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[9];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 1);\nfor (float h = 0.0; h < 3.0; h+=1.0) {\nfor (float w = 0.0; w < 3.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\ncolor.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n}\n}\nfloat alpha = texture2D(uTexture, vTexCoord).a;\ngl_FragColor = color;\ngl_FragColor.a = alpha;\n}",Convolute_5_1:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[25];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 0);\nfor (float h = 0.0; h < 5.0; h+=1.0) {\nfor (float w = 0.0; w < 5.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\ncolor += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n}\n}\ngl_FragColor = color;\n}",Convolute_5_0:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[25];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 1);\nfor (float h = 0.0; h < 5.0; h+=1.0) {\nfor (float w = 0.0; w < 5.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\ncolor.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n}\n}\nfloat alpha = texture2D(uTexture, vTexCoord).a;\ngl_FragColor = color;\ngl_FragColor.a = alpha;\n}",Convolute_7_1:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[49];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 0);\nfor (float h = 0.0; h < 7.0; h+=1.0) {\nfor (float w = 0.0; w < 7.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\ncolor += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n}\n}\ngl_FragColor = color;\n}",Convolute_7_0:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[49];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 1);\nfor (float h = 0.0; h < 7.0; h+=1.0) {\nfor (float w = 0.0; w < 7.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\ncolor.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n}\n}\nfloat alpha = texture2D(uTexture, vTexCoord).a;\ngl_FragColor = color;\ngl_FragColor.a = alpha;\n}",Convolute_9_1:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[81];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 0);\nfor (float h = 0.0; h < 9.0; h+=1.0) {\nfor (float w = 0.0; w < 9.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\ncolor += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n}\n}\ngl_FragColor = color;\n}",Convolute_9_0:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uMatrix[81];\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = vec4(0, 0, 0, 1);\nfor (float h = 0.0; h < 9.0; h+=1.0) {\nfor (float w = 0.0; w < 9.0; w+=1.0) {\nvec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\ncolor.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n}\n}\nfloat alpha = texture2D(uTexture, vTexCoord).a;\ngl_FragColor = color;\ngl_FragColor.a = alpha;\n}"},retrieveShader:function(t){var e=Math.sqrt(this.matrix.length),i=this.type+"_"+e+"_"+(this.opaque?1:0),r=this.fragmentSource[i];return t.programCache.hasOwnProperty(i)||(t.programCache[i]=this.createProgram(t.context,r)),t.programCache[i]},applyTo2d:function(t){var e,i,r,n,o,s,a,c,l,h,u,f,d,g=t.imageData,p=g.data,m=this.matrix,v=Math.round(Math.sqrt(m.length)),y=Math.floor(v/2),b=g.width,x=g.height,_=t.ctx.createImageData(b,x),C=_.data,S=this.opaque?1:0;for(u=0;x>u;u++)for(h=0;b>h;h++){for(o=4*(u*b+h),e=0,i=0,r=0,n=0,d=0;v>d;d++)for(f=0;v>f;f++)a=u+d-y,s=h+f-y,0>a||a>=x||0>s||s>=b||(c=4*(a*b+s),l=m[d*v+f],e+=p[c]*l,i+=p[c+1]*l,r+=p[c+2]*l,S||(n+=p[c+3]*l));C[o]=e,C[o+1]=i,C[o+2]=r,C[o+3]=S?p[o+3]:n}t.imageData=_},getUniformLocations:function(t,e){return{uMatrix:t.getUniformLocation(e,"uMatrix"),uOpaque:t.getUniformLocation(e,"uOpaque"),uHalfSize:t.getUniformLocation(e,"uHalfSize"),uSize:t.getUniformLocation(e,"uSize")}},sendUniformData:function(t,e){t.uniform1fv(e.uMatrix,this.matrix)},toObject:function(){return i(this.callSuper("toObject"),{opaque:this.opaque,matrix:this.matrix})}}),e.Image.filters.Convolute.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Grayscale=r(i.BaseFilter,{type:"Grayscale",fragmentSource:{average:"precision highp float;\nuniform sampler2D uTexture;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nfloat average = (color.r + color.b + color.g) / 3.0;\ngl_FragColor = vec4(average, average, average, color.a);\n}",lightness:"precision highp float;\nuniform sampler2D uTexture;\nuniform int uMode;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 col = texture2D(uTexture, vTexCoord);\nfloat average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\ngl_FragColor = vec4(average, average, average, col.a);\n}",luminosity:"precision highp float;\nuniform sampler2D uTexture;\nuniform int uMode;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 col = texture2D(uTexture, vTexCoord);\nfloat average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\ngl_FragColor = vec4(average, average, average, col.a);\n}"},mode:"average",mainParameter:"mode",applyTo2d:function(t){var e,i,r=t.imageData,n=r.data,s=n.length,o=this.mode;for(e=0;s>e;e+=4)"average"===o?i=(n[e]+n[e+1]+n[e+2])/3:"lightness"===o?i=(Math.min(n[e],n[e+1],n[e+2])+Math.max(n[e],n[e+1],n[e+2]))/2:"luminosity"===o&&(i=.21*n[e]+.72*n[e+1]+.07*n[e+2]),n[e]=i,n[e+1]=i,n[e+2]=i},retrieveShader:function(t){var e=this.type+"_"+this.mode;if(!t.programCache.hasOwnProperty(e)){var i=this.fragmentSource[this.mode];t.programCache[e]=this.createProgram(t.context,i)}return t.programCache[e]},getUniformLocations:function(t,e){return{uMode:t.getUniformLocation(e,"uMode")}},sendUniformData:function(t,e){var i=1;t.uniform1i(e.uMode,i)},isNeutralState:function(){return!1}}),e.Image.filters.Grayscale.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Invert=r(i.BaseFilter,{type:"Invert",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform int uInvert;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nif (uInvert == 1) {\ngl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n} else {\ngl_FragColor = color;\n}\n}",invert:!0,mainParameter:"invert",applyTo2d:function(t){var e,i=t.imageData,r=i.data,n=r.length;for(e=0;n>e;e+=4)r[e]=255-r[e],r[e+1]=255-r[e+1],r[e+2]=255-r[e+2]},isNeutralState:function(){return!this.invert},getUniformLocations:function(t,e){return{uInvert:t.getUniformLocation(e,"uInvert")}},sendUniformData:function(t,e){t.uniform1i(e.uInvert,this.invert)}}),e.Image.filters.Invert.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.Noise=n(r.BaseFilter,{type:"Noise",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uStepH;\nuniform float uNoise;\nuniform float uSeed;\nvarying vec2 vTexCoord;\nfloat rand(vec2 co, float seed, float vScale) {\nreturn fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n}\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\ncolor.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\ngl_FragColor = color;\n}",mainParameter:"noise",noise:0,applyTo2d:function(t){if(0!==this.noise){var e,i,r=t.imageData,n=r.data,s=n.length,o=this.noise;for(e=0,s=n.length;s>e;e+=4)i=(.5-Math.random())*o,n[e]+=i,n[e+1]+=i,n[e+2]+=i}},getUniformLocations:function(t,e){return{uNoise:t.getUniformLocation(e,"uNoise"),uSeed:t.getUniformLocation(e,"uSeed")}},sendUniformData:function(t,e){t.uniform1f(e.uNoise,this.noise/255),t.uniform1f(e.uSeed,Math.random())},toObject:function(){return i(this.callSuper("toObject"),{noise:this.noise})}}),e.Image.filters.Noise.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Pixelate=r(i.BaseFilter,{type:"Pixelate",blocksize:4,mainParameter:"blocksize",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uBlocksize;\nuniform float uStepW;\nuniform float uStepH;\nvarying vec2 vTexCoord;\nvoid main() {\nfloat blockW = uBlocksize * uStepW;\nfloat blockH = uBlocksize * uStepW;\nint posX = int(vTexCoord.x / blockW);\nint posY = int(vTexCoord.y / blockH);\nfloat fposX = float(posX);\nfloat fposY = float(posY);\nvec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\nvec4 color = texture2D(uTexture, squareCoords);\ngl_FragColor = color;\n}",applyTo2d:function(t){var e,i,r,n,s,o,a,l,c,h,u,f=t.imageData,d=f.data,g=f.height,p=f.width;for(i=0;g>i;i+=this.blocksize)for(r=0;p>r;r+=this.blocksize)for(e=4*i*p+4*r,n=d[e],s=d[e+1],o=d[e+2],a=d[e+3],h=Math.min(i+this.blocksize,g),u=Math.min(r+this.blocksize,p),l=i;h>l;l++)for(c=r;u>c;c++)e=4*l*p+4*c,d[e]=n,d[e+1]=s,d[e+2]=o,d[e+3]=a},isNeutralState:function(){return 1===this.blocksize},getUniformLocations:function(t,e){return{uBlocksize:t.getUniformLocation(e,"uBlocksize"),uStepW:t.getUniformLocation(e,"uStepW"),uStepH:t.getUniformLocation(e,"uStepH")}},sendUniformData:function(t,e){t.uniform1f(e.uBlocksize,this.blocksize)}}),e.Image.filters.Pixelate.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.RemoveColor=n(r.BaseFilter,{type:"RemoveColor",color:"#FFFFFF",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform vec4 uLow;\nuniform vec4 uHigh;\nvarying vec2 vTexCoord;\nvoid main() {\ngl_FragColor = texture2D(uTexture, vTexCoord);\nif(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\ngl_FragColor.a = 0.0;\n}\n}",distance:.02,useAlpha:!1,applyTo2d:function(t){var i,r,n,s,o=t.imageData,a=o.data,l=255*this.distance,c=new e.Color(this.color).getSource(),h=[c[0]-l,c[1]-l,c[2]-l],u=[c[0]+l,c[1]+l,c[2]+l];for(i=0;ih[0]&&n>h[1]&&s>h[2]&&r 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){var i,r,n,s,o,a,l,c=t.imageData,h=c.data,u=h.length,f=1-this.alpha;l=new e.Color(this.color).getSource(),i=l[0]*this.alpha,r=l[1]*this.alpha,n=l[2]*this.alpha;for(var d=0;u>d;d+=4)switch(s=h[d],o=h[d+1],a=h[d+2],this.mode){case"multiply":h[d]=s*i/255,h[d+1]=o*r/255,h[d+2]=a*n/255;break;case"screen":h[d]=255-(255-s)*(255-i)/255,h[d+1]=255-(255-o)*(255-r)/255,h[d+2]=255-(255-a)*(255-n)/255;break;case"add":h[d]=s+i,h[d+1]=o+r,h[d+2]=a+n;break;case"diff":case"difference":h[d]=Math.abs(s-i),h[d+1]=Math.abs(o-r),h[d+2]=Math.abs(a-n);break;case"subtract":h[d]=s-i,h[d+1]=o-r,h[d+2]=a-n;break;case"darken":h[d]=Math.min(s,i),h[d+1]=Math.min(o,r),h[d+2]=Math.min(a,n);break;case"lighten":h[d]=Math.max(s,i),h[d+1]=Math.max(o,r),h[d+2]=Math.max(a,n);break;case"overlay":h[d]=128>i?2*s*i/255:255-2*(255-s)*(255-i)/255,h[d+1]=128>r?2*o*r/255:255-2*(255-o)*(255-r)/255,h[d+2]=128>n?2*a*n/255:255-2*(255-a)*(255-n)/255;break;case"exclusion":h[d]=i+s-2*i*s/255,h[d+1]=r+o-2*r*o/255,h[d+2]=n+a-2*n*a/255;break;case"tint":h[d]=i+s*f,h[d+1]=r+o*f,h[d+2]=n+a*f}},getUniformLocations:function(t,e){return{uColor:t.getUniformLocation(e,"uColor")}},sendUniformData:function(t,i){var r=new e.Color(this.color).getSource();r[0]=this.alpha*r[0]/255,r[1]=this.alpha*r[1]/255,r[2]=this.alpha*r[2]/255,r[3]=this.alpha,t.uniform4fv(i.uColor,r)},toObject:function(){return{type:this.type,color:this.color,mode:this.mode,alpha:this.alpha}}}),e.Image.filters.BlendColor.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric,i=e.Image.filters,n=e.util.createClass;i.BlendImage=n(i.BaseFilter,{type:"BlendImage",image:null,mode:"multiply",alpha:1,vertexSource:"attribute vec2 aPosition;\nvarying vec2 vTexCoord;\nvarying vec2 vTexCoord2;\nuniform mat3 uTransformMatrix;\nvoid main() {\nvTexCoord = aPosition;\nvTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\ngl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n}",fragmentSource:{multiply:"precision highp float;\nuniform sampler2D uTexture;\nuniform sampler2D uImage;\nuniform vec4 uColor;\nvarying vec2 vTexCoord;\nvarying vec2 vTexCoord2;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nvec4 color2 = texture2D(uImage, vTexCoord2);\ncolor.rgba *= color2.rgba;\ngl_FragColor = color;\n}",mask:"precision highp float;\nuniform sampler2D uTexture;\nuniform sampler2D uImage;\nuniform vec4 uColor;\nvarying vec2 vTexCoord;\nvarying vec2 vTexCoord2;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nvec4 color2 = texture2D(uImage, vTexCoord2);\ncolor.a = color2.a;\ngl_FragColor = color;\n}"},retrieveShader:function(t){var e=this.type+"_"+this.mode,i=this.fragmentSource[this.mode];return t.programCache.hasOwnProperty(e)||(t.programCache[e]=this.createProgram(t.context,i)),t.programCache[e]},applyToWebGL:function(t){var e=t.context,i=this.createTexture(t.filterBackend,this.image);this.bindAdditionalTexture(e,i,e.TEXTURE1),this.callSuper("applyToWebGL",t),this.unbindAdditionalTexture(e,e.TEXTURE1)},createTexture:function(t,e){return t.getCachedTexture(e.cacheKey,e._element)},calculateMatrix:function(){var t=this.image,e=t._element.width,i=t._element.height;return[1/t.scaleX,0,0,0,1/t.scaleY,0,-t.left/e,-t.top/i,1]},applyTo2d:function(t){var i,n,r,s,a,o,c,l,h,u,f,d=t.imageData,g=t.filterBackend.resources,p=d.data,m=p.length,y=d.width,v=d.height,x=this.image;g.blendImage||(g.blendImage=e.util.createCanvasElement()),h=g.blendImage,u=h.getContext("2d"),h.width!==y||h.height!==v?(h.width=y,h.height=v):u.clearRect(0,0,y,v),u.setTransform(x.scaleX,0,0,x.scaleY,x.left,x.top),u.drawImage(x._element,0,0,y,v),f=u.getImageData(0,0,y,v).data;for(var b=0;m>b;b+=4)switch(a=p[b],o=p[b+1],c=p[b+2],l=p[b+3],i=f[b],n=f[b+1],r=f[b+2],s=f[b+3],this.mode){case"multiply":p[b]=a*i/255,p[b+1]=o*n/255,p[b+2]=c*r/255,p[b+3]=l*s/255;break;case"mask":p[b+3]=s}},getUniformLocations:function(t,e){return{uTransformMatrix:t.getUniformLocation(e,"uTransformMatrix"),uImage:t.getUniformLocation(e,"uImage")}},sendUniformData:function(t,e){var i=this.calculateMatrix();t.uniform1i(e.uImage,1),t.uniformMatrix3fv(e.uTransformMatrix,!1,i)},toObject:function(){return{type:this.type,image:this.image&&this.image.toObject(),mode:this.mode,alpha:this.alpha}}}),e.Image.filters.BlendImage.fromObject=function(t,i){e.Image.fromObject(t.image,function(n){var r=e.util.object.clone(t);r.image=n,i(new e.Image.filters.BlendImage(r))})}}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=Math.pow,n=Math.floor,r=Math.sqrt,s=Math.abs,a=Math.round,o=Math.sin,c=Math.ceil,l=e.Image.filters,h=e.util.createClass;l.Resize=h(l.BaseFilter,{type:"Resize",resizeType:"hermite",scaleX:1,scaleY:1,lanczosLobes:3,getUniformLocations:function(t,e){return{uDelta:t.getUniformLocation(e,"uDelta"),uTaps:t.getUniformLocation(e,"uTaps")}},sendUniformData:function(t,e){t.uniform2fv(e.uDelta,this.horizontal?[1/this.width,0]:[0,1/this.height]),t.uniform1fv(e.uTaps,this.taps)},retrieveShader:function(t){var e=this.getFilterWindow(),i=this.type+"_"+e;if(!t.programCache.hasOwnProperty(i)){var n=this.generateShader(e);t.programCache[i]=this.createProgram(t.context,n)}return t.programCache[i]},getFilterWindow:function(){var t=this.tempScale;return Math.ceil(this.lanczosLobes/t)},getTaps:function(){for(var t=this.lanczosCreate(this.lanczosLobes),e=this.tempScale,i=this.getFilterWindow(),n=new Array(i),r=1;i>=r;r++)n[r-1]=t(r*e);return n},generateShader:function(t){for(var t,e=new Array(t),i=this.fragmentSourceTOP,n=1;t>=n;n++)e[n-1]=n+".0 * uDelta";return i+="uniform float uTaps["+t+"];\n",i+="void main() {\n",i+=" vec4 color = texture2D(uTexture, vTexCoord);\n",i+=" float sum = 1.0;\n",e.forEach(function(t,e){i+=" color += texture2D(uTexture, vTexCoord + "+t+") * uTaps["+e+"];\n",i+=" color += texture2D(uTexture, vTexCoord - "+t+") * uTaps["+e+"];\n",i+=" sum += 2.0 * uTaps["+e+"];\n"}),i+=" gl_FragColor = color / sum;\n",i+="}"},fragmentSourceTOP:"precision highp float;\nuniform sampler2D uTexture;\nuniform vec2 uDelta;\nvarying vec2 vTexCoord;\n",applyTo:function(t){t.webgl?(t.passes++,this.width=t.sourceWidth,this.horizontal=!0,this.dW=Math.round(this.width*this.scaleX),this.dH=t.sourceHeight,this.tempScale=this.dW/this.width,this.taps=this.getTaps(),t.destinationWidth=this.dW,this._setupFrameBuffer(t),this.applyToWebGL(t),this._swapTextures(t),t.sourceWidth=t.destinationWidth,this.height=t.sourceHeight,this.horizontal=!1,this.dH=Math.round(this.height*this.scaleY),this.tempScale=this.dH/this.height,this.taps=this.getTaps(),t.destinationHeight=this.dH,this._setupFrameBuffer(t),this.applyToWebGL(t),this._swapTextures(t),t.sourceHeight=t.destinationHeight):this.applyTo2d(t)},isNeutralState:function(){return 1===this.scaleX&&1===this.scaleY},lanczosCreate:function(t){return function(e){if(e>=t||-t>=e)return 0;if(1.1920929e-7>e&&e>-1.1920929e-7)return 1;e*=Math.PI;var i=e/t;return o(e)/e*o(i)/i}},applyTo2d:function(t){var e=t.imageData,i=this.scaleX,n=this.scaleY;this.rcpScaleX=1/i,this.rcpScaleY=1/n;var r,s=e.width,o=e.height,c=a(s*i),l=a(o*n);"sliceHack"===this.resizeType?r=this.sliceByTwo(t,s,o,c,l):"hermite"===this.resizeType?r=this.hermiteFastResize(t,s,o,c,l):"bilinear"===this.resizeType?r=this.bilinearFiltering(t,s,o,c,l):"lanczos"===this.resizeType&&(r=this.lanczosResize(t,s,o,c,l)),t.imageData=r},sliceByTwo:function(t,i,r,s,a){var o,c,l=t.imageData,h=.5,u=!1,f=!1,d=i*h,g=r*h,p=e.filterBackend.resources,m=0,y=0,v=i,x=0;for(p.sliceByTwo||(p.sliceByTwo=document.createElement("canvas")),o=p.sliceByTwo,(o.width<1.5*i||o.heightc;c++){for(S.y=(c+.5)*m,C.y=n(S.y),k=0,O=0,A=0,E=0,D=0,w=C.x-x;w<=C.x+x;w++)if(!(0>w||w>=e)){M=n(1e3*s(w-S.x)),_[M]||(_[M]={});for(var P=C.y-b;P<=C.y+b;P++)0>P||P>=a||(L=n(1e3*s(P-S.y)),_[M][L]||(_[M][L]=g(r(i(M*y,2)+i(L*v,2))/1e3)),T=_[M][L],T>0&&(F=4*(P*e+w),k+=T,O+=T*u[F],A+=T*u[F+1],E+=T*u[F+2],D+=T*u[F+3]))}F=4*(c*o+t),d[F]=O/k,d[F+1]=A/k,d[F+2]=E/k,d[F+3]=D/k}return++tf;f++)for(d=0;r>d;d++)for(h=n(b*d),u=n(_*f),g=b*d-h,p=_*f-u,v=4*(u*e+h),m=0;4>m;m++)a=w[v+m],o=w[v+4+m],c=w[v+S+m],l=w[v+S+4+m],y=a*(1-g)*(1-p)+o*g*(1-p)+c*p*(1-g)+l*g*p,F[x++]=y;return T},hermiteFastResize:function(t,e,i,a,o){for(var l=this.rcpScaleX,h=this.rcpScaleY,u=c(l/2),f=c(h/2),d=t.imageData,g=d.data,p=t.ctx.createImageData(a,o),m=p.data,y=0;o>y;y++)for(var v=0;a>v;v++){for(var x=4*(v+y*a),b=0,_=0,S=0,C=0,w=0,T=0,F=0,k=(y+.5)*h,O=n(y*h);(y+1)*h>O;O++)for(var A=s(k-(O+.5))/f,E=(v+.5)*l,D=A*A,M=n(v*l);(v+1)*l>M;M++){var L=s(E-(M+.5))/u,P=r(D+L*L);P>1&&-1>P||(b=2*P*P*P-3*P*P+1,b>0&&(L=4*(M+O*e),F+=b*g[L+3],S+=b,g[L+3]<255&&(b=b*g[L+3]/250),C+=b*g[L],w+=b*g[L+1],T+=b*g[L+2],_+=b))}m[x]=C/_,m[x+1]=w/_,m[x+2]=T/_,m[x+3]=F/S}return p},toObject:function(){return{type:this.type,scaleX:this.scaleX,scaleY:this.scaleY,resizeType:this.resizeType,lanczosLobes:this.lanczosLobes}}}),e.Image.filters.Resize.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.Contrast=n(i.BaseFilter,{type:"Contrast",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uContrast;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nfloat contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\ncolor.rgb = contrastF * (color.rgb - 0.5) + 0.5;\ngl_FragColor = color;\n}",contrast:0,mainParameter:"contrast",applyTo2d:function(t){if(0!==this.contrast){var e,i,n=t.imageData,r=n.data,i=r.length,s=Math.floor(255*this.contrast),a=259*(s+255)/(255*(259-s));for(e=0;i>e;e+=4)r[e]=a*(r[e]-128)+128,r[e+1]=a*(r[e+1]-128)+128,r[e+2]=a*(r[e+2]-128)+128}},getUniformLocations:function(t,e){return{uContrast:t.getUniformLocation(e,"uContrast")}},sendUniformData:function(t,e){t.uniform1f(e.uContrast,this.contrast)}}),e.Image.filters.Contrast.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.Saturation=n(i.BaseFilter,{type:"Saturation",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uSaturation;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nfloat rgMax = max(color.r, color.g);\nfloat rgbMax = max(rgMax, color.b);\ncolor.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\ncolor.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\ncolor.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\ngl_FragColor = color;\n}",saturation:0,mainParameter:"saturation",applyTo2d:function(t){if(0!==this.saturation){var e,i,n=t.imageData,r=n.data,s=r.length,a=-this.saturation;for(e=0;s>e;e+=4)i=Math.max(r[e],r[e+1],r[e+2]),r[e]+=i!==r[e]?(i-r[e])*a:0,r[e+1]+=i!==r[e+1]?(i-r[e+1])*a:0,r[e+2]+=i!==r[e+2]?(i-r[e+2])*a:0}},getUniformLocations:function(t,e){return{uSaturation:t.getUniformLocation(e,"uSaturation")}},sendUniformData:function(t,e){t.uniform1f(e.uSaturation,-this.saturation)}}),e.Image.filters.Saturation.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.Blur=n(i.BaseFilter,{type:"Blur",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform vec2 uDelta;\nvarying vec2 vTexCoord;\nconst float nSamples = 15.0;\nvec3 v3offset = vec3(12.9898, 78.233, 151.7182);\nfloat random(vec3 scale) {\nreturn fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n}\nvoid main() {\nvec4 color = vec4(0.0);\nfloat total = 0.0;\nfloat offset = random(v3offset);\nfor (float t = -nSamples; t <= nSamples; t++) {\nfloat percent = (t + offset - 0.5) / nSamples;\nfloat weight = 1.0 - abs(percent);\ncolor += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\ntotal += weight;\n}\ngl_FragColor = color / total;\n}",blur:0,mainParameter:"blur",applyTo:function(t){t.webgl?(this.aspectRatio=t.sourceWidth/t.sourceHeight,t.passes++,this._setupFrameBuffer(t),this.horizontal=!0,this.applyToWebGL(t),this._swapTextures(t),this._setupFrameBuffer(t),this.horizontal=!1,this.applyToWebGL(t),this._swapTextures(t)):this.applyTo2d(t)},applyTo2d:function(t){t.imageData=this.simpleBlur(t)},simpleBlur:function(t){var i,n,r=t.filterBackend.resources,s=t.imageData.width,a=t.imageData.height;r.blurLayer1||(r.blurLayer1=e.util.createCanvasElement(),r.blurLayer2=e.util.createCanvasElement()),i=r.blurLayer1,n=r.blurLayer2,(i.width!==s||i.height!==a)&&(n.width=i.width=s,n.height=i.height=a);var o,c,l,h,f=i.getContext("2d"),u=n.getContext("2d"),d=15,p=.06*this.blur*.5;for(f.putImageData(t.imageData,0,0),u.clearRect(0,0,s,a),h=-d;d>=h;h++)o=(Math.random()-.5)/4,c=h/d,l=p*c*s+o,u.globalAlpha=1-Math.abs(c),u.drawImage(i,l,o),f.drawImage(n,0,0),u.globalAlpha=1,u.clearRect(0,0,n.width,n.height);for(h=-d;d>=h;h++)o=(Math.random()-.5)/4,c=h/d,l=p*c*a+o,u.globalAlpha=1-Math.abs(c),u.drawImage(i,o,l),f.drawImage(n,0,0),u.globalAlpha=1,u.clearRect(0,0,n.width,n.height);t.ctx.drawImage(i,0,0);var g=t.ctx.getImageData(0,0,i.width,i.height);return f.globalAlpha=1,f.clearRect(0,0,i.width,i.height),g},getUniformLocations:function(t,e){return{delta:t.getUniformLocation(e,"uDelta")}},sendUniformData:function(t,e){var i=this.chooseRightDelta();t.uniform2fv(e.delta,i)},chooseRightDelta:function(){var t,e=1,i=[0,0];return this.horizontal?this.aspectRatio>1&&(e=1/this.aspectRatio):this.aspectRatio<1&&(e=this.aspectRatio),t=e*this.blur*.12,this.horizontal?i[0]=t:i[1]=t,i}}),i.Blur.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.Gamma=n(i.BaseFilter,{type:"Gamma",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform vec3 uGamma;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nvec3 correction = (1.0 / uGamma);\ncolor.r = pow(color.r, correction.r);\ncolor.g = pow(color.g, correction.g);\ncolor.b = pow(color.b, correction.b);\ngl_FragColor = color;\ngl_FragColor.rgb *= color.a;\n}",gamma:[1,1,1],mainParameter:"gamma",initialize:function(t){this.gamma=[1,1,1],i.BaseFilter.prototype.initialize.call(this,t)},applyTo2d:function(t){var e,i=t.imageData,n=i.data,r=this.gamma,s=n.length,a=1/r[0],o=1/r[1],c=1/r[2];for(this.rVals||(this.rVals=new Uint8Array(256),this.gVals=new Uint8Array(256),this.bVals=new Uint8Array(256)),e=0,s=256;s>e;e++)this.rVals[e]=255*Math.pow(e/255,a),this.gVals[e]=255*Math.pow(e/255,o),this.bVals[e]=255*Math.pow(e/255,c);for(e=0,s=n.length;s>e;e+=4)n[e]=this.rVals[n[e]],n[e+1]=this.gVals[n[e+1]],n[e+2]=this.bVals[n[e+2]]},getUniformLocations:function(t,e){return{uGamma:t.getUniformLocation(e,"uGamma")}},sendUniformData:function(t,e){t.uniform3fv(e.uGamma,this.gamma)}}),e.Image.filters.Gamma.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.Composed=n(i.BaseFilter,{type:"Composed",subFilters:[],initialize:function(t){this.callSuper("initialize",t),this.subFilters=this.subFilters.slice(0)},applyTo:function(t){t.passes+=this.subFilters.length-1,this.subFilters.forEach(function(e){e.applyTo(t)})},toObject:function(){return e.util.object.extend(this.callSuper("toObject"),{subFilters:this.subFilters.map(function(t){return t.toObject()})})},isNeutralState:function(){return!this.subFilters.some(function(t){return!t.isNeutralState()})}}),e.Image.filters.Composed.fromObject=function(t,i){var n=t.subFilters||[],r=n.map(function(t){return new e.Image.filters[t.type](t)}),s=new e.Image.filters.Composed({subFilters:r});return i&&i(s),s}}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.Image.filters,n=e.util.createClass;i.HueRotation=n(i.ColorMatrix,{type:"HueRotation",rotation:0,mainParameter:"rotation",calculateMatrix:function(){var t=this.rotation*Math.PI,i=e.util.cos(t),n=e.util.sin(t),r=1/3,s=Math.sqrt(r)*n,o=1-i;this.matrix=[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0],this.matrix[0]=i+o/3,this.matrix[1]=r*o-s,this.matrix[2]=r*o+s,this.matrix[5]=r*o+s,this.matrix[6]=i+r*o,this.matrix[7]=r*o-s,this.matrix[10]=r*o-s,this.matrix[11]=r*o+s,this.matrix[12]=i+r*o},isNeutralState:function(t){return this.calculateMatrix(),i.BaseFilter.prototype.isNeutralState.call(this,t)},applyTo:function(t){this.calculateMatrix(),i.BaseFilter.prototype.applyTo.call(this,t)}}),e.Image.filters.HueRotation.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this);!function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.clone;if(e.Text)return void e.warn("fabric.Text is already defined");var n="fontFamily fontWeight fontSize text underline overline linethrough textAlign fontStyle lineHeight textBackgroundColor charSpacing styles direction path pathStartOffset pathSide pathAlign".split(" ");e.Text=e.util.createClass(e.Object,{_dimensionAffectingProps:["fontSize","fontWeight","fontFamily","fontStyle","lineHeight","text","charSpacing","textAlign","styles","path","pathStartOffset","pathSide","pathAlign"],_reNewline:/\r?\n/,_reSpacesAndTabs:/[ \t\r]/g,_reSpaceAndTab:/[ \t\r]/,_reWords:/\S+/g,type:"text",fontSize:40,fontWeight:"normal",fontFamily:"Times New Roman",underline:!1,overline:!1,linethrough:!1,textAlign:"left",fontStyle:"normal",lineHeight:1.16,superscript:{size:.6,baseline:-.35},subscript:{size:.6,baseline:.11},textBackgroundColor:"",stateProperties:e.Object.prototype.stateProperties.concat(n),cacheProperties:e.Object.prototype.cacheProperties.concat(n),stroke:null,shadow:null,path:null,pathStartOffset:0,pathSide:"left",pathAlign:"baseline",_fontSizeFraction:.222,offsets:{underline:.1,linethrough:-.315,overline:-.88},_fontSizeMult:1.13,charSpacing:0,styles:null,_measuringContext:null,deltaY:0,direction:"ltr",_styleProperties:["stroke","strokeWidth","fill","fontFamily","fontSize","fontWeight","fontStyle","underline","overline","linethrough","deltaY","textBackgroundColor"],__charBounds:[],CACHE_FONT_SIZE:400,MIN_TEXT_WIDTH:2,initialize:function(t,e){this.styles=e?e.styles||{}:{},this.text=t,this.__skipDimension=!0,this.callSuper("initialize",e),this.path&&this.setPathInfo(),this.__skipDimension=!1,this.initDimensions(),this.setCoords(),this.setupState({propertySet:"_dimensionAffectingProps"})},setPathInfo:function(){var t=this.path;t&&(t.segmentsInfo=e.util.getPathSegmentsInfo(t.path))},getMeasuringContext:function(){return e._measuringContext||(e._measuringContext=this.canvas&&this.canvas.contextCache||e.util.createCanvasElement().getContext("2d")),e._measuringContext},_splitText:function(){var t=this._splitTextIntoLines(this.text);return this.textLines=t.lines,this._textLines=t.graphemeLines,this._unwrappedTextLines=t._unwrappedLines,this._text=t.graphemeText,t},initDimensions:function(){this.__skipDimension||(this._splitText(),this._clearCache(),this.path?(this.width=this.path.width,this.height=this.path.height):(this.width=this.calcTextWidth()||this.cursorWidth||this.MIN_TEXT_WIDTH,this.height=this.calcTextHeight()),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.saveState({propertySet:"_dimensionAffectingProps"}))},enlargeSpaces:function(){for(var t,e,i,n,r,s,o,a=0,c=this._textLines.length;c>a;a++)if(("justify"===this.textAlign||a!==c-1&&!this.isEndOfWrapping(a))&&(n=0,r=this._textLines[a],e=this.getLineWidth(a),e=l;l++)s=this.__charBounds[a][l],this._reSpaceAndTab.test(r[l])?(s.width+=t,s.kernedWidth+=t,s.left+=n,n+=t):s.left+=n}},isEndOfWrapping:function(t){return t===this._textLines.length-1},missingNewlineOffset:function(){return 1},toString:function(){return"#'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){if(t.textBaseline="alphabetical",this.path)switch(this.pathAlign){case"center":t.textBaseline="middle";break;case"ascender":t.textBaseline="top";break;case"descender":t.textBaseline="bottom"}t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;i>e;e++){var n=this.getLineWidth(e);n>t&&(t=n)}return t},_renderTextLine:function(t,e,i,n,r,s){this._renderChars(t,e,i,n,r,s)},_renderTextLinesBackground:function(t){if(this.textBackgroundColor||this.styleHas("textBackgroundColor")){for(var e,i,n,r,s,o,a,c=t.fillStyle,l=this._getLeftOffset(),h=this._getTopOffset(),f=0,u=0,d=this.path,p=0,g=this._textLines.length;g>p;p++)if(e=this.getHeightOfLine(p),this.textBackgroundColor||this.styleHas("textBackgroundColor",p)){n=this._textLines[p],i=this._getLineLeftOffset(p),u=0,f=0,r=this.getValueOfPropertyAt(p,0,"textBackgroundColor");for(var m=0,y=n.length;y>m;m++)s=this.__charBounds[p][m],o=this.getValueOfPropertyAt(p,m,"textBackgroundColor"),d?(t.save(),t.translate(s.renderLeft,s.renderTop),t.rotate(s.angle),t.fillStyle=o,o&&t.fillRect(-s.width/2,-e/this.lineHeight*(1-this._fontSizeFraction),s.width,e/this.lineHeight),t.restore()):o!==r?(a=l+i+f,"rtl"===this.direction&&(a=this.width-a-u),t.fillStyle=r,r&&t.fillRect(a,h,u,e/this.lineHeight),f=s.left,u=s.width,r=o):u+=s.kernedWidth;o&&!d&&(a=l+i+f,"rtl"===this.direction&&(a=this.width-a-u),t.fillStyle=o,t.fillRect(a,h,u,e/this.lineHeight)),h+=e}else h+=e;t.fillStyle=c,this._removeShadow(t)}},getFontCache:function(t){var i=t.fontFamily.toLowerCase();e.charWidthsCache[i]||(e.charWidthsCache[i]={});var n=e.charWidthsCache[i],r=t.fontStyle.toLowerCase()+"_"+(t.fontWeight+"").toLowerCase();return n[r]||(n[r]={}),n[r]},_measureChar:function(t,e,i,n){var r,s,o,a,c=this.getFontCache(e),l=this._getFontDeclaration(e),h=this._getFontDeclaration(n),f=i+t,u=l===h,d=e.fontSize/this.CACHE_FONT_SIZE;if(i&&void 0!==c[i]&&(o=c[i]),void 0!==c[t]&&(a=r=c[t]),u&&void 0!==c[f]&&(s=c[f],a=s-o),void 0===r||void 0===o||void 0===s){var p=this.getMeasuringContext();this._setTextStyles(p,e,!0)}return void 0===r&&(a=r=p.measureText(t).width,c[t]=r),void 0===o&&u&&i&&(o=p.measureText(i).width,c[i]=o),u&&void 0===s&&(s=p.measureText(f).width,c[f]=s,a=s-o),{width:r*d,kernedWidth:a*d}},getHeightOfChar:function(t,e){return this.getValueOfPropertyAt(t,e,"fontSize")},measureLine:function(t){var e=this._measureLine(t);return 0!==this.charSpacing&&(e.width-=this._getWidthOfCharSpacing()),e.width<0&&(e.width=0),e},_measureLine:function(t){var i,n,r,s,o,a,c=0,l=this._textLines[t],h=0,f=new Array(l.length),u=0,d=this.path,p="right"===this.pathSide;for(this.__charBounds[t]=f,i=0;i=0:ia?u%=a:0>u&&(u+=a),this._setGraphemeOnPath(u,s,o),u+=s.kernedWidth}return{width:c,numOfSpaces:h}},_setGraphemeOnPath:function(t,i,n){var r=t+i.kernedWidth/2,s=this.path,o=e.util.getPointOnPath(s.path,r,s.segmentsInfo);i.renderLeft=o.x-n.x,i.renderTop=o.y-n.y,i.angle=o.angle+("right"===this.pathSide?Math.PI:0)},_getGraphemeBox:function(t,e,i,n,r){var s,o=this.getCompleteStyleDeclaration(e,i),a=n?this.getCompleteStyleDeclaration(e,i-1):{},c=this._measureChar(t,o,n,a),l=c.kernedWidth,h=c.width;0!==this.charSpacing&&(s=this._getWidthOfCharSpacing(),h+=s,l+=s);var f={width:h,left:0,height:o.fontSize,kernedWidth:l,deltaY:o.deltaY};if(i>0&&!r){var u=this.__charBounds[e][i-1];f.left=u.left+u.width+c.kernedWidth-c.width}return f},getHeightOfLine:function(t){if(this.__lineHeights[t])return this.__lineHeights[t];for(var e=this._textLines[t],i=this.getHeightOfChar(t,0),n=1,r=e.length;r>n;n++)i=Math.max(this.getHeightOfChar(t,n),i);return this.__lineHeights[t]=i*this.lineHeight*this._fontSizeMult},calcTextHeight:function(){for(var t,e=0,i=0,n=this._textLines.length;n>i;i++)t=this.getHeightOfLine(i),e+=i===n-1?t/this.lineHeight:t;return e},_getLeftOffset:function(){return"ltr"===this.direction?-this.width/2:this.width/2},_getTopOffset:function(){return-this.height/2},_renderTextCommon:function(t,e){t.save();for(var i=0,n=this._getLeftOffset(),r=this._getTopOffset(),s=0,o=this._textLines.length;o>s;s++){var a=this.getHeightOfLine(s),c=a/this.lineHeight,l=this._getLineLeftOffset(s);this._renderTextLine(e,t,this._textLines[s],n+l,r+i+c,s),i+=a}t.restore()},_renderTextFill:function(t){(this.fill||this.styleHas("fill"))&&this._renderTextCommon(t,"fillText")},_renderTextStroke:function(t){(this.stroke&&0!==this.strokeWidth||!this.isEmptyStyles())&&(this.shadow&&!this.shadow.affectStroke&&this._removeShadow(t),t.save(),this._setLineDash(t,this.strokeDashArray),t.beginPath(),this._renderTextCommon(t,"strokeText"),t.closePath(),t.restore())},_renderChars:function(t,i,n,r,s,o){var a,c,l,h,f,u=this.getHeightOfLine(o),d=-1!==this.textAlign.indexOf("justify"),p="",g=0,m=this.path,y=!d&&0===this.charSpacing&&this.isEmptyStyles(o)&&!m,v="ltr"===this.direction,x="ltr"===this.direction?1:-1,b=i.canvas.getAttribute("dir");if(i.save(),b!==this.direction&&(i.canvas.setAttribute("dir",v?"ltr":"rtl"),i.direction=v?"ltr":"rtl",i.textAlign=v?"left":"right"),s-=u*this._fontSizeFraction/this.lineHeight,y)return this._renderChar(t,i,o,0,n.join(""),r,s,u),void i.restore();for(var _=0,S=n.length-1;S>=_;_++)h=_===S||this.charSpacing||m,p+=n[_],l=this.__charBounds[o][_],0===g?(r+=x*(l.kernedWidth-l.width),g+=l.width):g+=l.kernedWidth,d&&!h&&this._reSpaceAndTab.test(n[_])&&(h=!0),h||(a=a||this.getCompleteStyleDeclaration(o,_),c=this.getCompleteStyleDeclaration(o,_+1),h=e.util.hasStyleChanged(a,c,!1)),h&&(m?(i.save(),i.translate(l.renderLeft,l.renderTop),i.rotate(l.angle),this._renderChar(t,i,o,_,p,-g/2,0,u),i.restore()):(f=r,this._renderChar(t,i,o,_,p,f,s,u)),p="",a=c,r+=x*g,g=0);i.restore()},_applyPatternGradientTransformText:function(t){var i,n=e.util.createCanvasElement(),r=this.width+this.strokeWidth,s=this.height+this.strokeWidth;return n.width=r,n.height=s,i=n.getContext("2d"),i.beginPath(),i.moveTo(0,0),i.lineTo(r,0),i.lineTo(r,s),i.lineTo(0,s),i.closePath(),i.translate(r/2,s/2),i.fillStyle=t.toLive(i),this._applyPatternGradientTransform(i,t),i.fill(),i.createPattern(n,"no-repeat")},handleFiller:function(t,e,i){var n,r;return i.toLive?"percentage"===i.gradientUnits||i.gradientTransform||i.patternTransform?(n=-this.width/2,r=-this.height/2,t.translate(n,r),t[e]=this._applyPatternGradientTransformText(i),{offsetX:n,offsetY:r}):(t[e]=i.toLive(t,this),this._applyPatternGradientTransform(t,i)):(t[e]=i,{offsetX:0,offsetY:0})},_setStrokeStyles:function(t,e){return t.lineWidth=e.strokeWidth,t.lineCap=this.strokeLineCap,t.lineDashOffset=this.strokeDashOffset,t.lineJoin=this.strokeLineJoin,t.miterLimit=this.strokeMiterLimit,this.handleFiller(t,"strokeStyle",e.stroke)},_setFillStyles:function(t,e){return this.handleFiller(t,"fillStyle",e.fill)},_renderChar:function(t,e,i,n,r,s,o){var a,c,l=this._getStyleDeclaration(i,n),h=this.getCompleteStyleDeclaration(i,n),f="fillText"===t&&h.fill,u="strokeText"===t&&h.stroke&&h.strokeWidth;(u||f)&&(e.save(),f&&(a=this._setFillStyles(e,h)),u&&(c=this._setStrokeStyles(e,h)),e.font=this._getFontDeclaration(h),l&&l.textBackgroundColor&&this._removeShadow(e),l&&l.deltaY&&(o+=l.deltaY),f&&e.fillText(r,s-a.offsetX,o-a.offsetY),u&&e.strokeText(r,s-c.offsetX,o-c.offsetY),e.restore())},setSuperscript:function(t,e){return this._setScript(t,e,this.superscript)},setSubscript:function(t,e){return this._setScript(t,e,this.subscript)},_setScript:function(t,e,i){var n=this.get2DCursorLocation(t,!0),r=this.getValueOfPropertyAt(n.lineIndex,n.charIndex,"fontSize"),s=this.getValueOfPropertyAt(n.lineIndex,n.charIndex,"deltaY"),o={fontSize:r*i.size,deltaY:s+r*i.baseline};return this.setSelectionStyles(o,t,e),this},_getLineLeftOffset:function(t){var e,i=this.getLineWidth(t),n=this.width-i,r=this.textAlign,s=this.direction,o=0,e=this.isEndOfWrapping(t);return"justify"===r||"justify-center"===r&&!e||"justify-right"===r&&!e||"justify-left"===r&&!e?0:("center"===r&&(o=n/2),"right"===r&&(o=n),"justify-center"===r&&(o=n/2),"justify-right"===r&&(o=n),"rtl"===s&&(o-=n),o)},_clearCache:function(){this.__lineWidths=[],this.__lineHeights=[],this.__charBounds=[]},_shouldClearDimensionCache:function(){var t=this._forceClearCache;return t||(t=this.hasStateChanged("_dimensionAffectingProps")),t&&(this.dirty=!0,this._forceClearCache=!1),t},getLineWidth:function(t){if(void 0!==this.__lineWidths[t])return this.__lineWidths[t];var e=this.measureLine(t),i=e.width;return this.__lineWidths[t]=i,i},_getWidthOfCharSpacing:function(){return 0!==this.charSpacing?this.fontSize*this.charSpacing/1e3:0},getValueOfPropertyAt:function(t,e,i){var n=this._getStyleDeclaration(t,e);return n&&"undefined"!=typeof n[i]?n[i]:this[i]},_renderTextDecoration:function(t,e){if(this[e]||this.styleHas(e)){for(var i,n,r,s,o,a,c,l,h,f,u,d,p,g,m,y,v=this._getLeftOffset(),x=this._getTopOffset(),b=this.path,_=this._getWidthOfCharSpacing(),S=this.offsets[e],C=0,T=this._textLines.length;T>C;C++)if(i=this.getHeightOfLine(C),this[e]||this.styleHas(e,C)){c=this._textLines[C],g=i/this.lineHeight,s=this._getLineLeftOffset(C),f=0,u=0,l=this.getValueOfPropertyAt(C,0,e),y=this.getValueOfPropertyAt(C,0,"fill"),h=x+g*(1-this._fontSizeFraction),n=this.getHeightOfChar(C,0),o=this.getValueOfPropertyAt(C,0,"deltaY");for(var w=0,O=c.length;O>w;w++)if(d=this.__charBounds[C][w],p=this.getValueOfPropertyAt(C,w,e),m=this.getValueOfPropertyAt(C,w,"fill"),r=this.getHeightOfChar(C,w),a=this.getValueOfPropertyAt(C,w,"deltaY"),b&&p&&m)t.save(),t.fillStyle=y,t.translate(d.renderLeft,d.renderTop),t.rotate(d.angle),t.fillRect(-d.kernedWidth/2,S*r+a,d.kernedWidth,this.fontSize/15),t.restore();else if((p!==l||m!==y||r!==n||a!==o)&&u>0){var k=v+s+f;"rtl"===this.direction&&(k=this.width-k-u),l&&y&&(t.fillStyle=y,t.fillRect(k,h+S*n+o,u,this.fontSize/15)),f=d.left,u=d.width,l=p,y=m,n=r,o=a}else u+=d.kernedWidth;var k=v+s+f;"rtl"===this.direction&&(k=this.width-k-u),t.fillStyle=m,p&&m&&t.fillRect(k,h+S*n+o,u-_,this.fontSize/15),x+=i}else x+=i;this._removeShadow(t)}},_getFontDeclaration:function(t,i){var n=t||this,r=this.fontFamily,s=e.Text.genericFonts.indexOf(r.toLowerCase())>-1,o=void 0===r||r.indexOf("'")>-1||r.indexOf(",")>-1||r.indexOf('"')>-1||s?n.fontFamily:'"'+n.fontFamily+'"';return[e.isLikelyNode?n.fontWeight:n.fontStyle,e.isLikelyNode?n.fontStyle:n.fontWeight,i?this.CACHE_FONT_SIZE+"px":n.fontSize+"px",o].join(" ")},render:function(t){this.visible&&(!this.canvas||!this.canvas.skipOffscreen||this.group||this.isOnScreen())&&(this._shouldClearDimensionCache()&&this.initDimensions(),this.callSuper("render",t))},_splitTextIntoLines:function(t){for(var i=t.split(this._reNewline),n=new Array(i.length),r=["\n"],s=[],o=0;or;r++){if(t<=i[r].length)return{lineIndex:r,charIndex:t};t-=i[r].length+this.missingNewlineOffset(r)}return{lineIndex:r-1,charIndex:i[r-1].lengthr;r++)n.push(this.getStyleAtPosition(r,i));return n},getStyleAtPosition:function(t,e){var i=this.get2DCursorLocation(t),n=e?this.getCompleteStyleDeclaration(i.lineIndex,i.charIndex):this._getStyleDeclaration(i.lineIndex,i.charIndex);return n||{}},setSelectionStyles:function(t,e,i){"undefined"==typeof e&&(e=this.selectionStart||0),"undefined"==typeof i&&(i=this.selectionEnd||e);for(var n=e;i>n;n++)this._extendStyles(n,t);return this._forceClearCache=!0,this},_getStyleDeclaration:function(t,e){var i=this.styles&&this.styles[t];return i?i[e]:null},getCompleteStyleDeclaration:function(t,e){for(var i,n=this._getStyleDeclaration(t,e)||{},r={},s=0;s-1&&(t.underline=!0),t.textDecoration.indexOf("line-through")>-1&&(t.linethrough=!0),t.textDecoration.indexOf("overline")>-1&&(t.overline=!0),delete t.textDecoration)}fabric.IText=fabric.util.createClass(fabric.Text,fabric.Observable,{type:"i-text",selectionStart:0,selectionEnd:0,selectionColor:"rgba(17,119,255,0.3)",isEditing:!1,editable:!0,editingBorderColor:"rgba(102,153,255,0.25)",cursorWidth:2,cursorColor:"",cursorDelay:1e3,cursorDuration:600,caching:!0,hiddenTextareaContainer:null,_reSpace:/\s|\n/,_currentCursorOpacity:0,_selectionDirection:null,_abortCursorAnimation:!1,__widthOfSpace:[],inCompositionMode:!1,initialize:function(t,e){this.callSuper("initialize",t,e),this.initBehavior()},setSelectionStart:function(t){t=Math.max(t,0),this._updateAndFire("selectionStart",t)},setSelectionEnd:function(t){t=Math.min(t,this.text.length),this._updateAndFire("selectionEnd",t)},_updateAndFire:function(t,e){this[t]!==e&&(this._fireSelectionChanged(),this[t]=e),this._updateTextarea()},_fireSelectionChanged:function(){this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},initDimensions:function(){this.isEditing&&this.initDelayedCursor(),this.clearContextTop(),this.callSuper("initDimensions")},render:function(t){this.clearContextTop(),this.callSuper("render",t),this.cursorOffsetCache={},this.renderCursorOrSelection()},_render:function(t){this.callSuper("_render",t)},clearContextTop:function(t){if(this.isEditing&&this.canvas&&this.canvas.contextTop){var e=this.canvas.contextTop,i=this.canvas.viewportTransform;e.save(),e.transform(i[0],i[1],i[2],i[3],i[4],i[5]),this.transform(e),this._clearTextArea(e),t||e.restore()}},renderCursorOrSelection:function(){if(this.isEditing&&this.canvas&&this.canvas.contextTop){var t=this._getCursorBoundaries(),e=this.canvas.contextTop;this.clearContextTop(!0),this.selectionStart===this.selectionEnd?this.renderCursor(t,e):this.renderSelection(t,e),e.restore()}},_clearTextArea:function(t){var e=this.width+4,i=this.height+4;t.clearRect(-e/2,-i/2,e,i)},_getCursorBoundaries:function(t){"undefined"==typeof t&&(t=this.selectionStart);var e=this._getLeftOffset(),i=this._getTopOffset(),n=this._getCursorBoundariesOffsets(t);return{left:e,top:i,leftOffset:n.left,topOffset:n.top}},_getCursorBoundariesOffsets:function(t){if(this.cursorOffsetCache&&"top"in this.cursorOffsetCache)return this.cursorOffsetCache;var e,i,n,r,s=0,o=0,a=this.get2DCursorLocation(t);n=a.charIndex,i=a.lineIndex;for(var c=0;i>c;c++)s+=this.getHeightOfLine(c);e=this._getLineLeftOffset(i);var l=this.__charBounds[i][n];return l&&(o=l.left),0!==this.charSpacing&&n===this._textLines[i].length&&(o-=this._getWidthOfCharSpacing()),r={top:s,left:e+(o>0?o:0)},"rtl"===this.direction&&(r.left*=-1),this.cursorOffsetCache=r,this.cursorOffsetCache},renderCursor:function(t,e){var i=this.get2DCursorLocation(),n=i.lineIndex,r=i.charIndex>0?i.charIndex-1:0,s=this.getValueOfPropertyAt(n,r,"fontSize"),o=this.scaleX*this.canvas.getZoom(),a=this.cursorWidth/o,c=t.topOffset,l=this.getValueOfPropertyAt(n,r,"deltaY");c+=(1-this._fontSizeFraction)*this.getHeightOfLine(n)/this.lineHeight-s*(1-this._fontSizeFraction),this.inCompositionMode&&this.renderSelection(t,e),e.fillStyle=this.cursorColor||this.getValueOfPropertyAt(n,r,"fill"),e.globalAlpha=this.__isMousedown?1:this._currentCursorOpacity,e.fillRect(t.left+t.leftOffset-a/2,c+t.top+l,a,s)},renderSelection:function(t,e){for(var i=this.inCompositionMode?this.hiddenTextarea.selectionStart:this.selectionStart,n=this.inCompositionMode?this.hiddenTextarea.selectionEnd:this.selectionEnd,r=-1!==this.textAlign.indexOf("justify"),s=this.get2DCursorLocation(i),o=this.get2DCursorLocation(n),a=s.lineIndex,c=o.lineIndex,l=s.charIndex<0?0:s.charIndex,h=o.charIndex<0?0:o.charIndex,u=a;c>=u;u++){var f=this._getLineLeftOffset(u)||0,d=this.getHeightOfLine(u),p=0,g=0,m=0;if(u===a&&(g=this.__charBounds[a][l].left),u>=a&&c>u)m=r&&!this.isEndOfWrapping(u)?this.width:this.getLineWidth(u)||5;else if(u===c)if(0===h)m=this.__charBounds[c][h].left;else{var v=this._getWidthOfCharSpacing();m=this.__charBounds[c][h-1].left+this.__charBounds[c][h-1].width-v}p=d,(this.lineHeight<1||u===c&&this.lineHeight>1)&&(d/=this.lineHeight);var y=t.left+f+g,b=m-g,x=d,_=0;this.inCompositionMode?(e.fillStyle=this.compositionColor||"black",x=1,_=d):e.fillStyle=this.selectionColor,"rtl"===this.direction&&(y=this.width-y-b),e.fillRect(y,t.top+t.topOffset+_,b,x),t.topOffset+=p}},getCurrentCharFontSize:function(){var t=this._getCurrentCharIndex();return this.getValueOfPropertyAt(t.l,t.c,"fontSize")},getCurrentCharColor:function(){var t=this._getCurrentCharIndex();return this.getValueOfPropertyAt(t.l,t.c,"fill")},_getCurrentCharIndex:function(){var t=this.get2DCursorLocation(this.selectionStart,!0),e=t.charIndex>0?t.charIndex-1:0;return{l:t.lineIndex,c:e}}}),fabric.IText.fromObject=function(e,i){if(e.styles=fabric.util.stylesFromArray(e.styles,e.text),t(e),e.styles)for(var n in e.styles)for(var r in e.styles[n])t(e.styles[n][r]);fabric.Object._fromObject("IText",e,i,"text")}}();!function(){var t=fabric.util.object.clone;fabric.util.object.extend(fabric.IText.prototype,{initBehavior:function(){this.initAddedHandler(),this.initRemovedHandler(),this.initCursorSelectionHandlers(),this.initDoubleClickSimulation(),this.mouseMoveHandler=this.mouseMoveHandler.bind(this)},onDeselect:function(){this.isEditing&&this.exitEditing(),this.selected=!1},initAddedHandler:function(){var t=this;this.on("added",function(){var e=t.canvas;e&&(e._hasITextHandlers||(e._hasITextHandlers=!0,t._initCanvasHandlers(e)),e._iTextInstances=e._iTextInstances||[],e._iTextInstances.push(t))})},initRemovedHandler:function(){var t=this;this.on("removed",function(){var e=t.canvas;e&&(e._iTextInstances=e._iTextInstances||[],fabric.util.removeFromArray(e._iTextInstances,t),0===e._iTextInstances.length&&(e._hasITextHandlers=!1,t._removeCanvasHandlers(e)))})},_initCanvasHandlers:function(t){t._mouseUpITextHandler=function(){t._iTextInstances&&t._iTextInstances.forEach(function(t){t.__isMousedown=!1})},t.on("mouse:up",t._mouseUpITextHandler)},_removeCanvasHandlers:function(t){t.off("mouse:up",t._mouseUpITextHandler)},_tick:function(){this._currentTickState=this._animateCursor(this,1,this.cursorDuration,"_onTickComplete")},_animateCursor:function(t,e,i,n){var r;return r={isAborted:!1,abort:function(){this.isAborted=!0}},t.animate("_currentCursorOpacity",e,{duration:i,onComplete:function(){r.isAborted||t[n]()},onChange:function(){t.canvas&&t.selectionStart===t.selectionEnd&&t.renderCursorOrSelection()},abort:function(){return r.isAborted}}),r},_onTickComplete:function(){var t=this;this._cursorTimeout1&&clearTimeout(this._cursorTimeout1),this._cursorTimeout1=setTimeout(function(){t._currentTickCompleteState=t._animateCursor(t,0,this.cursorDuration/2,"_tick")},100)},initDelayedCursor:function(t){var e=this,i=t?0:this.cursorDelay;this.abortCursorAnimation(),this._currentCursorOpacity=1,this._cursorTimeout2=setTimeout(function(){e._tick()},i)},abortCursorAnimation:function(){var t=this._currentTickState||this._currentTickCompleteState,e=this.canvas;this._currentTickState&&this._currentTickState.abort(),this._currentTickCompleteState&&this._currentTickCompleteState.abort(),clearTimeout(this._cursorTimeout1),clearTimeout(this._cursorTimeout2),this._currentCursorOpacity=0,t&&e&&e.clearContext(e.contextTop||e.contextContainer)},selectAll:function(){return this.selectionStart=0,this.selectionEnd=this._text.length,this._fireSelectionChanged(),this._updateTextarea(),this},getSelectedText:function(){return this._text.slice(this.selectionStart,this.selectionEnd).join("")},findWordBoundaryLeft:function(t){var e=0,i=t-1;if(this._reSpace.test(this._text[i]))for(;this._reSpace.test(this._text[i]);)e++,i--;for(;/\S/.test(this._text[i])&&i>-1;)e++,i--;return t-e},findWordBoundaryRight:function(t){var e=0,i=t;if(this._reSpace.test(this._text[i]))for(;this._reSpace.test(this._text[i]);)e++,i++;for(;/\S/.test(this._text[i])&&i-1;)e++,i--;return t-e},findLineBoundaryRight:function(t){for(var e=0,i=t;!/\n/.test(this._text[i])&&i0&&nthis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),(this.selectionStart!==i||this.selectionEnd!==n)&&(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var n=i.slice(0,t),r=fabric.util.string.graphemeSplit(n).length;if(t===e)return{selectionStart:r,selectionEnd:r};var s=i.slice(t,e),o=fabric.util.string.graphemeSplit(s).length;return{selectionStart:r,selectionEnd:r+o}},fromGraphemeToStringSelection:function(t,e,i){var n=i.slice(0,t),r=n.join("").length;if(t===e)return{selectionStart:r,selectionEnd:r};var s=i.slice(t,e),o=s.join("").length;return{selectionStart:r,selectionEnd:r+o}},_updateTextarea:function(){if(this.cursorOffsetCache={},this.hiddenTextarea){if(!this.inCompositionMode){var t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text);this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd}this.updateTextareaPosition()}},updateFromTextArea:function(){if(this.hiddenTextarea){this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords());var t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value);this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition()}},updateTextareaPosition:function(){if(this.selectionStart===this.selectionEnd){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),i=this.get2DCursorLocation(t),n=i.lineIndex,r=i.charIndex,s=this.getValueOfPropertyAt(n,r,"fontSize")*this.lineHeight,o=e.leftOffset,a=this.calcTransformMatrix(),c={x:e.left+o,y:e.top+e.topOffset+s},l=this.canvas.getRetinaScaling(),h=this.canvas.upperCanvasEl,u=h.width/l,f=h.height/l,d=u-s,p=f-s,m=h.clientWidth/u,g=h.clientHeight/f;return c=fabric.util.transformPoint(c,a),c=fabric.util.transformPoint(c,this.canvas.viewportTransform),c.x*=m,c.y*=g,c.x<0&&(c.x=0),c.x>d&&(c.x=d),c.y<0&&(c.y=0),c.y>p&&(c.y=p),c.x+=this.canvas._offset.left,c.y+=this.canvas._offset.top,{left:c.x+"px",top:c.y+"px",fontSize:s+"px",charHeight:s}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,selectable:this.selectable,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.hoverCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.selectable=this._savedProps.selectable,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var i,n,r=this.get2DCursorLocation(t,!0),s=this.get2DCursorLocation(e,!0),o=r.lineIndex,a=r.charIndex,c=s.lineIndex,l=s.charIndex;if(o!==c){if(this.styles[o])for(i=a;i=i;i++)delete this.styles[i];this.shiftLineStyles(c,o-c)}else if(this.styles[o]){n=this.styles[o];var h,u,f=l-a;for(i=a;l>i;i++)delete n[i];for(u in this.styles[o])h=parseInt(u,10),h>=l&&(n[h-f]=n[u],delete n[u])}},shiftLineStyles:function(e,i){var n=t(this.styles);for(var r in this.styles){var s=parseInt(r,10);s>e&&(this.styles[s+i]=n[s],n[s-i]||delete this.styles[s])}},restartCursorIfNeeded:function(){(!this._currentTickState||this._currentTickState.isAborted||!this._currentTickCompleteState||this._currentTickCompleteState.isAborted)&&this.initDelayedCursor()},insertNewlineStyleObject:function(e,i,n,r){var s,o={},a=!1,c=this._unwrappedTextLines[e].length===i;n||(n=1),this.shiftLineStyles(e,n),this.styles[e]&&(s=this.styles[e][0===i?i:i-1]);for(var l in this.styles[e]){var h=parseInt(l,10);h>=i&&(a=!0,o[h-i]=this.styles[e][l],c&&0===i||delete this.styles[e][l])}var u=!1;for(a&&!c&&(this.styles[e+n]=o,u=!0),u&&n--;n>0;)r&&r[n-1]?this.styles[e+n]={0:t(r[n-1])}:s?this.styles[e+n]={0:t(s)}:delete this.styles[e+n],n--;this._forceClearCache=!0},insertCharStyleObject:function(e,i,n,r){this.styles||(this.styles={});var s=this.styles[e],o=s?t(s):{};n||(n=1);for(var a in o){var c=parseInt(a,10);c>=i&&(s[c+n]=o[c],o[c-n]||delete s[c])}if(this._forceClearCache=!0,r)for(;n--;)Object.keys(r[n]).length&&(this.styles[e]||(this.styles[e]={}),this.styles[e][i+n]=t(r[n]));else if(s)for(var l=s[i?i-1:1];l&&n--;)this.styles[e][i+n]=t(l)},insertNewStyleBlock:function(t,e,i){for(var n=this.get2DCursorLocation(e,!0),r=[0],s=0,o=0;o0&&(this.insertCharStyleObject(n.lineIndex,n.charIndex,r[0],i),i=i&&i.slice(r[0]+1)),s&&this.insertNewlineStyleObject(n.lineIndex,n.charIndex+r[0],s);for(var o=1;s>o;o++)r[o]>0?this.insertCharStyleObject(n.lineIndex+o,0,r[o],i):i&&this.styles[n.lineIndex+o]&&i[0]&&(this.styles[n.lineIndex+o][0]=i[0]),i=i&&i.slice(r[o]+1);r[o]>0&&this.insertCharStyleObject(n.lineIndex+o,0,r[o],i)},setSelectionStartEndWithShift:function(t,e,i){t>=i?(e===t?this._selectionDirection="left":"right"===this._selectionDirection&&(this._selectionDirection="left",this.selectionEnd=t),this.selectionStart=i):i>t&&e>i?"right"===this._selectionDirection?this.selectionEnd=i:this.selectionStart=i:(e===t?this._selectionDirection="right":"left"===this._selectionDirection&&(this._selectionDirection="right",this.selectionStart=e),this.selectionEnd=i)},setSelectionInBoundaries:function(){var t=this.text.length;this.selectionStart>t?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}();fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){if(this.canvas){this.__newClickTime=+new Date;var e=t.pointer;this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected}},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,n=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,n,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i,n=this.getLocalPointer(t),r=0,a=0,o=0,s=0,c=0,l=0,h=this._textLines.length;h>l&&o<=n.y;l++)o+=this.getHeightOfLine(l)*this.scaleY,c=l,l>0&&(s+=this._textLines[l-1].length+this.missingNewlineOffset(l-1));e=this._getLineLeftOffset(c),a=e*this.scaleX,i=this._textLines[c],"rtl"===this.direction&&(n.x=this.width*this.scaleX-n.x+a);for(var f=0,u=i.length;u>f&&(r=a,a+=this.__charBounds[c][f].kernedWidth*this.scaleX,a<=n.x);f++)s++;return this._getNewSelectionStartFromOffset(n,r,a,s,u)},_getNewSelectionStartFromOffset:function(t,e,i,n,r){var a=t.x-e,o=i-t.x,s=o>a||0>o?0:1,c=n+s;return this.flipX&&(c=r-c),c>this._text.length&&(c=this._text.length),c}});fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.setAttribute("autocorrect","off"),this.hiddenTextarea.setAttribute("autocomplete","off"),this.hiddenTextarea.setAttribute("spellcheck","false"),this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea",""),this.hiddenTextarea.setAttribute("wrap","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; paddingーtop: "+t.fontSize+";",this.hiddenTextareaContainer?this.hiddenTextareaContainer.appendChild(this.hiddenTextarea):fabric.document.body.appendChild(this.hiddenTextarea),fabric.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(this.hiddenTextarea,"keyup",this.onKeyUp.bind(this)),fabric.util.addListener(this.hiddenTextarea,"input",this.onInput.bind(this)),fabric.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"cut",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionstart",this.onCompositionStart.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionupdate",this.onCompositionUpdate.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionend",this.onCompositionEnd.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),t.keyCode>=33&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){return!this.isEditing||this._copyDone||this.inCompositionMode?void(this._copyDone=!1):void(t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll()))},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,n,r,a,o,s=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,c=this._text.length,l=s.length,h=l-c,f=this.selectionStart,u=this.selectionEnd,d=f!==u;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var p=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),g=f>p.selectionStart;d?(i=this._text.slice(f,u),h+=u-f):c>l&&(i=g?this._text.slice(u+h,u):this._text.slice(f,f-h)),n=s.slice(p.selectionEnd-h,p.selectionEnd),i&&i.length&&(n.length&&(r=this.getSelectionStyles(f,f+1,!1),r=n.map(function(){return r[0]})),d?(a=f,o=u):g?(a=u-i.length,o=u):(a=u,o=u+i.length),this.removeStyleFromTo(a,o)),n.length&&(e&&n.join("")===fabric.copiedText&&!fabric.disableStyleCopyPaste&&(r=fabric.copiedTextStyle),this.insertNewStyleBlock(n,f,r)),this.updateFromTextArea(),this.fire("changed"),this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll())}},onCompositionStart:function(){this.inCompositionMode=!0},onCompositionEnd:function(){this.inCompositionMode=!1},onCompositionUpdate:function(t){this.compositionStart=t.target.selectionStart,this.compositionEnd=t.target.selectionEnd,this.updateTextareaPosition()},copy:function(){this.selectionStart!==this.selectionEnd&&(fabric.copiedText=this.getSelectedText(),fabric.copiedTextStyle=fabric.disableStyleCopyPaste?null:this.getSelectionStyles(this.selectionStart,this.selectionEnd,!0),this._copyDone=!0)},paste:function(){this.fromPaste=!0},_getClipboardData:function(t){return t&&t.clipboardData||fabric.window.clipboardData},_getWidthBeforeCursor:function(t,e){var i,n=this._getLineLeftOffset(t);return e>0&&(i=this.__charBounds[t][e-1],n+=i.left+i.width),n},getDownCursorOffset:function(t,e){var i=this._getSelectionForOffset(t,e),n=this.get2DCursorLocation(i),r=n.lineIndex;if(r===this._textLines.length-1||t.metaKey||34===t.keyCode)return this._text.length-i;var a=n.charIndex,o=this._getWidthBeforeCursor(r,a),s=this._getIndexOnLine(r+1,o),c=this._textLines[r].slice(a);return c.length+s+1+this.missingNewlineOffset(r)},_getSelectionForOffset:function(t,e){return t.shiftKey&&this.selectionStart!==this.selectionEnd&&e?this.selectionEnd:this.selectionStart},getUpCursorOffset:function(t,e){var i=this._getSelectionForOffset(t,e),n=this.get2DCursorLocation(i),r=n.lineIndex;if(0===r||t.metaKey||33===t.keyCode)return-i;var a=n.charIndex,o=this._getWidthBeforeCursor(r,a),s=this._getIndexOnLine(r-1,o),c=this._textLines[r].slice(0,a),l=this.missingNewlineOffset(r-1);return-this._textLines[r-1].length+s-c.length+(1-l)},_getIndexOnLine:function(t,e){for(var i,n,r=this._textLines[t],a=this._getLineLeftOffset(t),o=a,s=0,c=0,l=r.length;l>c;c++)if(i=this.__charBounds[t][c].width,o+=i,o>e){n=!0;var h=o-i,f=o,u=Math.abs(h-e),d=Math.abs(f-e);s=u>d?c:c-1;break}return n||(s=r.length-1),s},moveCursorDown:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){(0!==this.selectionStart||0!==this.selectionEnd)&&this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i="get"+t+"CursorOffset",n=this[i](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(n):this.moveCursorWithoutShift(n),0!==n&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return 0>t?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){(0!==this.selectionStart||0!==this.selectionEnd)&&this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var n;if(t.altKey)n=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;n=this["findLineBoundary"+i](this[e])}return void 0!==typeof n&&this[e]!==n?(this[e]=n,!0):void 0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,i+=e.shiftKey?"Shift":"outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){"undefined"==typeof e&&(e=t+1),this.removeStyleFromTo(t,e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,n){"undefined"==typeof n&&(n=i),n>i&&this.removeStyleFromTo(i,n);var r=fabric.util.string.graphemeSplit(t);this.insertNewStyleBlock(r,i,e),this._text=[].concat(this._text.slice(0,i),r,this._text.slice(n)),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()}});!function(){var t=fabric.util.toFixed,e=/ +/g;fabric.util.object.extend(fabric.Text.prototype,{_toSVG:function(){var t=this._getSVGLeftTopOffsets(),e=this._getSVGTextAndBg(t.textTop,t.textLeft);return this._wrapSVGTextAndBg(e)},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,noStyle:!0,withShadow:!0})},_getSVGLeftTopOffsets:function(){return{textLeft:-this.width/2,textTop:-this.height/2,lineTop:this.getHeightOfLine(0)}},_wrapSVGTextAndBg:function(t){var e=!0,r=this.getSvgTextDecoration(this);return[t.textBgRects.join(""),' ",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var r,i=[],n=[],a=t;this._setSVGBg(n);for(var o=0,s=this._textLines.length;s>o;o++)r=this._getLineLeftOffset(o),(this.textBackgroundColor||this.styleHas("textBackgroundColor",o))&&this._setSVGTextLineBg(n,o,e+r,a),this._setSVGTextLineText(i,o,e+r,a),a+=this.getHeightOfLine(o);return{textSpans:i,textBgRects:n}},_createTextCharSpan:function(r,i,n,a){var o=r!==r.trim()||r.match(e),s=this.getSvgSpanStyles(i,o),c=s?'style="'+s+'"':"",l=i.deltaY,f="",u=fabric.Object.NUM_FRACTION_DIGITS;return l&&(f=' dy="'+t(l,u)+'" '),['",fabric.util.string.escapeXml(r),""].join("")},_setSVGTextLineText:function(t,e,r,i){var n,a,o,s,c,l=this.getHeightOfLine(e),f=-1!==this.textAlign.indexOf("justify"),u="",h=0,d=this._textLines[e];i+=l*(1-this._fontSizeFraction)/this.lineHeight;for(var p=0,g=d.length-1;g>=p;p++)c=p===g||this.charSpacing,u+=d[p],o=this.__charBounds[e][p],0===h?(r+=o.kernedWidth-o.width,h+=o.width):h+=o.kernedWidth,f&&!c&&this._reSpaceAndTab.test(d[p])&&(c=!0),c||(n=n||this.getCompleteStyleDeclaration(e,p),a=this.getCompleteStyleDeclaration(e,p+1),c=fabric.util.hasStyleChanged(n,a,!0)),c&&(s=this._getStyleDeclaration(e,p)||{},t.push(this._createTextCharSpan(u,s,r,i)),u="",n=a,r+=h,h=0)},_pushTextBgRect:function(e,r,i,n,a,o){var s=fabric.Object.NUM_FRACTION_DIGITS;e.push(" \n')},_setSVGTextLineBg:function(t,e,r,i){for(var n,a,o=this._textLines[e],s=this.getHeightOfLine(e)/this.lineHeight,c=0,l=0,f=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,h=o.length;h>u;u++)n=this.__charBounds[e][u],a=this.getValueOfPropertyAt(e,u,"textBackgroundColor"),a!==f?(f&&this._pushTextBgRect(t,f,r+l,i,c,s),l=n.left,c=n.width,f=a):c+=n.kernedWidth;a&&this._pushTextBgRect(t,a,r+l,i,c,s)},_getFillAttributes:function(t){var e=t&&"string"==typeof t?new fabric.Color(t):"";return e&&e.getSource()&&1!==e.getAlpha()?'opacity="'+e.getAlpha()+'" fill="'+e.setAlpha(1).toRgb()+'"':'fill="'+t+'"'},_getSVGLineTopOffset:function(t){for(var e=0,r=0,i=0;t>i;i++)e+=this.getHeightOfLine(i);return r=this.getHeightOfLine(i),{lineTop:e,offset:(this._fontSizeMult-this._fontSizeFraction)*r/(this.lineHeight*this._fontSizeMult)}},getSvgStyles:function(t){var e=fabric.Object.prototype.getSvgStyles.call(this,t);return e+" white-space: pre;"}})}();!function(t){"use strict";var e=t.fabric||(t.fabric={});e.Textbox=e.util.createClass(e.IText,e.Observable,{type:"textbox",minWidth:20,dynamicMinWidth:2,__cachedLines:null,lockScalingFlip:!0,noScaleCache:!1,_dimensionAffectingProps:e.Text.prototype._dimensionAffectingProps.concat("width"),_wordJoiners:/[ \t\r]/,splitByGrapheme:!1,initDimensions:function(){this.__skipDimension||(this.isEditing&&this.initDelayedCursor(),this.clearContextTop(),this._clearCache(),this.dynamicMinWidth=0,this._styleMap=this._generateStyleMap(this._splitText()),this.dynamicMinWidth>this.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,r=0,n=0,i={},a=0;a0?(r=0,n++,e++):!this.splitByGrapheme&&this._reSpaceAndTab.test(t.graphemeText[n])&&a>0&&(r++,n++),i[a]={line:e,offset:r},n+=t.graphemeLines[a].length,r+=t.graphemeLines[a].length;return i},styleHas:function(t,r){if(this._styleMap&&!this.isWrapping){var n=this._styleMap[r];n&&(r=n.line)}return e.Text.prototype.styleHas.call(this,t,r)},isEmptyStyles:function(t){if(!this.styles)return!0;var e,r,n=0,i=t+1,a=!1,o=this._styleMap[t],c=this._styleMap[t+1];o&&(t=o.line,n=o.offset),c&&(i=c.line,a=i===t,e=c.offset),r="undefined"==typeof t?this.styles:{line:this.styles[t]};for(var s in r)for(var l in r[s])if(l>=n&&(!a||e>l))for(var f in r[s][l])return!1;return!0},_getStyleDeclaration:function(t,e){if(this._styleMap&&!this.isWrapping){var r=this._styleMap[t];if(!r)return null;t=r.line,e=r.offset+e}return this.callSuper("_getStyleDeclaration",t,e)},_setStyleDeclaration:function(t,e,r){var n=this._styleMap[t];t=n.line,e=n.offset+e,this.styles[t][e]=r},_deleteStyleDeclaration:function(t,e){var r=this._styleMap[t];t=r.line,e=r.offset+e,delete this.styles[t][e]},_getLineStyle:function(t){var e=this._styleMap[t];return!!this.styles[e.line]},_setLineStyle:function(t){var e=this._styleMap[t];this.styles[e.line]={}},_wrapText:function(t,e){var r,n=[];for(this.isWrapping=!0,r=0;ro;o++){var s=this._getGraphemeBox(t[o],e,o+r,n,a);i+=s.kernedWidth,n=t[o]}return i},_wrapLine:function(t,r,n,i){var a=0,o=this.splitByGrapheme,c=[],s=[],l=o?e.util.string.graphemeSplit(t):t.split(this._wordJoiners),f="",u=0,h=o?"":" ",d=0,b=0,p=0,m=!0,y=this._getWidthOfCharSpacing(),i=i||0;0===l.length&&l.push([]),n-=i;for(var g=0;gn&&!m?(c.push(s),s=[],a=d,m=!0):a+=y,m||o||s.push(h),s=s.concat(f),b=o?0:this._measureWord([h],r,u),u++,m=!1,d>p&&(p=d);return g&&c.push(s),p+i>this.dynamicMinWidth&&(this.dynamicMinWidth=p-y+i),c},isEndOfWrapping:function(t){return this._styleMap[t+1]?this._styleMap[t+1].line!==this._styleMap[t].line?!0:!1:!0},missingNewlineOffset:function(t){return this.splitByGrapheme?this.isEndOfWrapping(t)?1:0:1},_splitTextIntoLines:function(t){for(var r=e.Text.prototype._splitTextIntoLines.call(this,t),n=this._wrapText(r.lines,this.width),i=new Array(n.length),a=0;a", + "hl.simple.post": "", + "hl.snippets": 30, + "hl.fragsize": 100 } # Specify which field to use in the tag cloud on the homepage. diff --git a/app/controllers/flipflop/strategies_controller_decorator.rb b/app/controllers/flipflop/strategies_controller_decorator.rb new file mode 100644 index 0000000000..7c508a5bf4 --- /dev/null +++ b/app/controllers/flipflop/strategies_controller_decorator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# OVERRIDE Flipflop v2.7.1 to allow for custom `Action` labels + +module Flipflop + module StrategiesControllerDecorator + def enable? + values = StrategiesController::ENABLE_VALUES | ADDITIONAL_ENABLE_VALUES + values.include?(params[:commit]) + end + + ADDITIONAL_ENABLE_VALUES = FeaturesHelper::FEATURE_ACTION_LABELS.map { |_, v| v[:on] }.to_set.freeze + end +end + +Flipflop::StrategiesController.prepend(Flipflop::StrategiesControllerDecorator) diff --git a/app/controllers/hyrax/admin/appearances_controller.rb b/app/controllers/hyrax/admin/appearances_controller.rb index b37308ebcb..41fd636e0d 100644 --- a/app/controllers/hyrax/admin/appearances_controller.rb +++ b/app/controllers/hyrax/admin/appearances_controller.rb @@ -26,7 +26,10 @@ def show end def update - form_class.new(update_params).update! + form = form_class.new(update_params) + form.banner_image = update_params[:banner_image] if update_params[:banner_image].present? + + form.update! if update_params['default_collection_image'] # Reindex all Collections and AdminSets to apply new default collection image diff --git a/app/controllers/hyrax/homepage_controller.rb b/app/controllers/hyrax/homepage_controller.rb index a38eda8453..17e67d07b2 100644 --- a/app/controllers/hyrax/homepage_controller.rb +++ b/app/controllers/hyrax/homepage_controller.rb @@ -33,17 +33,13 @@ def search_builder_class # override hyrax v2.9.0 added @home_text - Adding Themes def index - @presenter = presenter_class.new(current_ability, collections) @featured_researcher = ContentBlock.for(:researcher) - @marketing_text = ContentBlock.for(:marketing) @home_text = ContentBlock.for(:home_text) @featured_work_list = FeaturedWorkList.new # OVERRIDE here to add featured collection list @featured_collection_list = FeaturedCollectionList.new - @announcement_text = ContentBlock.for(:announcement) + load_shared_info recent - ir_counts if home_page_theme == 'institutional_repository' - # override hyrax v2.9.0 added for facets on homepage - Adding Themes (@response, @document_list) = search_results(params) @@ -65,11 +61,7 @@ def index def browserconfig; end def all_collections - @presenter = presenter_class.new(current_ability, collections) - @marketing_text = ContentBlock.for(:marketing) - @announcement_text = ContentBlock.for(:announcement) - @collections = collections(rows: 100_000) - ir_counts if home_page_theme == 'institutional_repository' + load_shared_info end # Added from Blacklight 6.23.0 to change url for facets on home page @@ -85,6 +77,26 @@ def search_action_url(options = {}) private + # shared methods for index and all_collections routes + def load_shared_info + @presenter = presenter_class.new(current_ability, collections) + @marketing_text = ContentBlock.for(:marketing) + @announcement_text = ContentBlock.for(:announcement) + @collections = collections(rows: 100_000) + # rubocop:disable Style/GuardClause + # TODO: Why not make these helper methods? As is we rely on a theme to set instance + # variables. Which is fragile. + if home_page_theme == 'institutional_repository' + ir_counts + @top_level_collections ||= load_top_level_collections(@collections) + end + # rubocop:enable Style/GuardClause + end + + def load_top_level_collections(colls) + colls.select { |c| c['member_of_collection_ids_ssim'].nil? } + end + # Return 6 collections def collections(rows: 6) builder = Hyrax::CollectionSearchBuilder.new(self) @@ -120,7 +132,6 @@ def inject_theme_views prepend_view_path(home_theme_view_path) yield # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. view_paths=(original_paths) # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses else diff --git a/app/forms/hyrax/forms/admin/appearance.rb b/app/forms/hyrax/forms/admin/appearance.rb index 7f20affaeb..599297f995 100644 --- a/app/forms/hyrax/forms/admin/appearance.rb +++ b/app/forms/hyrax/forms/admin/appearance.rb @@ -26,6 +26,7 @@ class Appearance 'header_and_footer_background_color' => '#3c3c3c', 'header_and_footer_text_color' => '#dcdcdc', 'navbar_background_color' => '#000000', + 'navbar_link_background_color' => '#375f8c', 'navbar_link_background_hover_color' => '#ffffff', 'navbar_link_text_color' => '#eeeeee', 'navbar_link_text_hover_color' => '#eeeeee', @@ -80,6 +81,8 @@ def persisted? true end + delegate :banner_image=, to: :site + # The alt text for the logo image def logo_image_text block_for('logo_image_text') @@ -138,6 +141,14 @@ def navbar_background_color_active darken_color(navbar_background_color, 0.35) end + def navbar_link_background_color + block_for('navbar_link_background_color') + end + + def navbar_link_background_color_active + darken_color(navbar_link_background_color, 0.35) + end + def navbar_link_background_hover_color block_for('navbar_link_background_hover_color') end @@ -381,6 +392,7 @@ def self.customization_params facet_panel_background_color facet_panel_text_color navbar_background_color + navbar_link_background_color navbar_link_background_hover_color navbar_link_text_color navbar_link_text_hover_color diff --git a/app/forms/hyrax/generic_work_form.rb b/app/forms/hyrax/generic_work_form.rb index 132f11bd82..de0630d1a7 100644 --- a/app/forms/hyrax/generic_work_form.rb +++ b/app/forms/hyrax/generic_work_form.rb @@ -7,6 +7,8 @@ class GenericWorkForm < Hyrax::Forms::WorkForm include Hyrax::FormTerms self.model_class = ::GenericWork include HydraEditor::Form::Permissions - self.terms += %i[resource_type] + include PdfFormBehavior + + self.terms += %i[resource_type bibliographic_citation] end end diff --git a/app/forms/hyrax/image_form.rb b/app/forms/hyrax/image_form.rb index 3d3e09b530..f2b27cd254 100644 --- a/app/forms/hyrax/image_form.rb +++ b/app/forms/hyrax/image_form.rb @@ -6,6 +6,8 @@ module Hyrax class ImageForm < Hyrax::Forms::WorkForm include Hyrax::FormTerms self.model_class = ::Image - self.terms += %i[resource_type extent] + include PdfFormBehavior + + self.terms += %i[resource_type extent bibliographic_citation] end end diff --git a/app/forms/hyrax/pdf_form_behavior.rb b/app/forms/hyrax/pdf_form_behavior.rb new file mode 100644 index 0000000000..cd909a56a1 --- /dev/null +++ b/app/forms/hyrax/pdf_form_behavior.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hyrax + module PdfFormBehavior + extend ActiveSupport::Concern + + included do + class_attribute :hidden_terms + + self.terms += %i[show_pdf_viewer show_pdf_download_button] + self.hidden_terms = %i[show_pdf_viewer show_pdf_download_button] + end + + def hidden?(key) + hidden_terms.include? key.to_sym + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e27eee4aeb..2b4d5d3b4b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,11 +1,17 @@ # frozen_string_literal: true module ApplicationHelper + # Yep, we're ignoring the advice; because the translations are safe as is the markdown converter. + # rubocop:disable Rails/OutputSafety include ::HyraxHelper include Hyrax::OverrideHelperBehavior include GroupNavigationHelper include SharedSearchHelper + def label_for(term:, record_class: nil) + locale_for(type: 'labels', term: term, record_class: record_class) + end + def hint_for(term:, record_class: nil) hint = locale_for(type: 'hints', term: term, record_class: record_class) @@ -16,8 +22,8 @@ def locale_for(type:, term:, record_class:) @term = term.to_s @record_class = record_class.to_s.downcase work_or_collection = @record_class == 'collection' ? 'collection' : 'defaults' - default_locale = t("simple_form.#{type}.#{work_or_collection}.#{@term}") - locale = t("hyrax.#{@record_class}.#{type}.#{@term}") + default_locale = t("simple_form.#{type}.#{work_or_collection}.#{@term}").html_safe + locale = t("hyrax.#{@record_class}.#{type}.#{@term}").html_safe return default_locale if missing_translation(locale) @@ -27,4 +33,15 @@ def locale_for(type:, term:, record_class:) def missing_translation(value) value.include?('translation missing') end + + def markdown(text) + options = %i[ + hard_wrap autolink no_intra_emphasis tables fenced_code_blocks + disable_indented_code_blocks strikethrough lax_spacing space_after_headers + quote footnotes highlight underline + ] + text ||= "" + Markdown.new(text, *options).to_html.html_safe + end + # rubocop:enable Rails/OutputSafety end diff --git a/app/helpers/blacklight/advanced_search_helper.rb b/app/helpers/blacklight/advanced_search_helper.rb new file mode 100644 index 0000000000..2e848270b4 --- /dev/null +++ b/app/helpers/blacklight/advanced_search_helper.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Blacklight + # Helpers related to the advanced search functionality of Blacklight. + module AdvancedSearchHelper + # Retrieves the first six search fields from a given collection. + # + # @param fields [Array] collection of search fields + # @return [Array] a subset of the input collection containing the first six fields + def primary_search_fields_for(fields) + fields.each_with_index.partition { |_, idx| idx < 6 }.first.map(&:first) + end + + # Retrieves all search fields from a given collection except the first six. + # + # @param fields [Array] collection of search fields + # @return [Array] a subset of the input collection excluding the first six fields + def secondary_search_fields_for(fields) + fields.each_with_index.partition { |_, idx| idx < 6 }.last.map(&:first) + end + + # Determines if the provided key represents a local authority. + # + # @param key [String] the key to be checked + # @return [Boolean] true if the key or its pluralized form is found in the local authorities list; false otherwise + def local_authority?(key) + local_qa_names = Qa::Authorities::Local.names + local_qa_names.include?(key.pluralize) || local_qa_names.include?(key) + end + + # Gets the options for a QA select based on a given key. + # + # @param key [String] the key used to fetch the service and retrieve options + # @return [Array, nil] the options available for the select, or nil if the service does not provide any options + def options_for_qa_select(key) + service = fetch_service_for(key) + service.try(:select_all_options) || service.try(:select_options) || service.new.select_all_options + end + + private + + # Fetches the service for a given key. + # + # @param key [String] the key used to determine the service name + # @return [Class, nil] the service class based on the key, or nil if it does not exist + def fetch_service_for(key) + "Hyrax::#{key.camelize}Service".safe_constantize || "Hyrax::#{key.pluralize.camelize}Service".safe_constantize + end + end +end diff --git a/app/helpers/features_helper.rb b/app/helpers/features_helper.rb new file mode 100644 index 0000000000..6d630fd042 --- /dev/null +++ b/app/helpers/features_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module FeaturesHelper + def status_for(feature) + status = @feature_set.status(feature) + label = @feature_set.status(feature) == :enabled ? :on : :off + FEATURE_ACTION_LABELS.fetch(feature.name.to_sym, label => status)[label] + end + + def on(feature) + FEATURE_ACTION_LABELS[feature]&.[](:on) || 'on' + end + + def off(feature) + FEATURE_ACTION_LABELS[feature]&.[](:off) || 'off' + end + + FEATURE_ACTION_LABELS = { + default_pdf_viewer: { on: 'PDF.js', off: 'UV' } + }.freeze +end diff --git a/app/helpers/hyku_helper.rb b/app/helpers/hyku_helper.rb index c33acd741e..dd76ef205e 100644 --- a/app/helpers/hyku_helper.rb +++ b/app/helpers/hyku_helper.rb @@ -19,4 +19,10 @@ def admin_host? def admin_only_tenant_creation? ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_ADMIN_ONLY_TENANT_CREATION', false)) end + + def parent_path(parent_doc) + model = parent_doc['has_model_ssim'].first + path = "hyrax_#{model.underscore}_path" + main_app.send(path, parent_doc.id) + end end diff --git a/app/helpers/iiif_print_helper.rb b/app/helpers/iiif_print_helper.rb new file mode 100644 index 0000000000..0c1859cfa4 --- /dev/null +++ b/app/helpers/iiif_print_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IiifPrintHelper + include IiifPrint::IiifPrintHelperBehavior +end diff --git a/app/helpers/pdf_js_helper.rb b/app/helpers/pdf_js_helper.rb new file mode 100644 index 0000000000..8ba877fa90 --- /dev/null +++ b/app/helpers/pdf_js_helper.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module PdfJsHelper + def pdf_js_url(path) + "/pdf.js/viewer.html?file=#{path}##{query_param}" + end + + def pdf_file_set_presenter(presenter) + # currently only supports one pdf per work, falls back to the first pdf file set in ordered members + representative_presenter(presenter) || presenter.file_set_presenters.select(&:pdf?).first + end + + def representative_presenter(presenter) + presenter.file_set_presenters.find { |file_set_presenter| file_set_presenter.id == presenter.representative_id } + end + + def query_param + return unless params[:q] + + "search=#{params[:q]}&phrase=true" + end + + def render_show_pdf_behavior_checkbox? + return unless Flipflop.default_pdf_viewer? + return if params[:id].nil? + + doc = SolrDocument.find params[:id] + + presenter = @_controller.show_presenter.new(doc, current_ability) + presenter.file_set_presenters.any?(&:pdf?) + end +end diff --git a/app/helpers/shared_search_helper.rb b/app/helpers/shared_search_helper.rb index ef419f6cb4..ee213710bc 100644 --- a/app/helpers/shared_search_helper.rb +++ b/app/helpers/shared_search_helper.rb @@ -17,7 +17,10 @@ def generate_work_url(model, request) id = model["id"] end request_params = %i[protocol host port].map { |method| ["request_#{method}".to_sym, request.send(method)] }.to_h - get_url(id: id, request: request_params, account_cname: account_cname, has_model: has_model) + url = get_url(id: id, request: request_params, account_cname: account_cname, has_model: has_model) + + # pass search query params to work show page + params[:q].present? ? "#{url}?q=#{params[:q]}" : url end private diff --git a/app/indexers/app_indexer.rb b/app/indexers/app_indexer.rb index 0567be9373..1da15015fc 100644 --- a/app/indexers/app_indexer.rb +++ b/app/indexers/app_indexer.rb @@ -12,7 +12,26 @@ class AppIndexer < Hyrax::WorkIndexer # Uncomment this block if you want to add custom indexing behavior: def generate_solr_document super.tap do |solr_doc| - solr_doc["account_cname_tesim"] = Site.instance&.account&.cname + solr_doc['account_cname_tesim'] = Site.instance&.account&.cname + solr_doc['bulkrax_identifier_tesim'] = object.bulkrax_identifier if object.respond_to?(:bulkrax_identifier) + solr_doc['all_text_tsimv'] = full_text(object.file_sets.first&.id) + add_date(solr_doc) end end + + def full_text(file_set_id) + return if !Flipflop.default_pdf_viewer? || file_set_id.blank? + + SolrDocument.find(file_set_id)['all_text_tsimv'] + end + + def add_date(solr_doc) + # The allowed date formats are either YYYY, YYYY-MM, or YYYY-MM-DD + # the date must be formatted as a 4 digit year in order to be sorted. + valid_date_formats = /\A(\d{4})(?:-\d{2}(?:-\d{2})?)?\z/ + date_string = solr_doc['date_created_tesim']&.first + year = date_string&.match(valid_date_formats)&.captures&.first + solr_doc['date_tesi'] = year if year + solr_doc['date_ssi'] = year if year + end end diff --git a/app/indexers/generic_work_indexer.rb b/app/indexers/generic_work_indexer.rb index f0f08119e2..4466bc78e0 100644 --- a/app/indexers/generic_work_indexer.rb +++ b/app/indexers/generic_work_indexer.rb @@ -5,8 +5,8 @@ class GenericWorkIndexer < AppIndexer # Uncomment this block if you want to add custom indexing behavior: # def generate_solr_document - # super.tap do |solr_doc| - # solr_doc['my_custom_field_ssim'] = object.my_custom_property - # end + # super.tap do |solr_doc| + # solr_doc['admin_note_tesim'] = object.admin_note + # end # end end diff --git a/app/indexers/hyrax/file_set_indexer_decorator.rb b/app/indexers/hyrax/file_set_indexer_decorator.rb new file mode 100644 index 0000000000..0fdd59eeb9 --- /dev/null +++ b/app/indexers/hyrax/file_set_indexer_decorator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 3.6.0 to add PDF text to solr document when using the default PDF viewer (PDF.js) + +module Hyrax + module FileSetIndexerDecorator + def generate_solr_document + return super unless Flipflop.default_pdf_viewer? + + super.tap do |solr_doc| + solr_doc['all_text_timv'] = solr_doc['all_text_tsimv'] = pdf_text + end + end + + private + + def pdf_text + return unless object.pdf? + return unless object.original_file&.content.is_a? String + + begin + text = IO.popen(['pdftotext', '-', '-'], 'r+b') do |pdftotext| + pdftotext.write(object.original_file.content) + pdftotext.close_write + pdftotext.read + end + + text.tr("\n", ' ') + .squeeze(' ') + .encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') # remove non-UTF-8 characters + rescue Errno::ENOENT => e + raise e unless e.message.include?("No such file or directory - pdftotext") + Rails.logger.warn("`pdfinfo' is not installed; unable to extract text from the PDF's content") + end + end + end +end + +Hyrax::FileSetIndexer.prepend(Hyrax::FileSetIndexerDecorator) diff --git a/app/jobs/reindex_works_job.rb b/app/jobs/reindex_works_job.rb index 56adcf2d0a..5d9c50cfc2 100644 --- a/app/jobs/reindex_works_job.rb +++ b/app/jobs/reindex_works_job.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true class ReindexWorksJob < ApplicationJob - def perform - Site.instance.available_works.each do |work_type| - work_type.constantize.find_each do |work| - ReindexItemJob.perform_later(work) + def perform(work = nil) + if work.present? + work.update_index + else + Hyrax.config.registered_curation_concern_types.each do |work_type| + work_type.constantize.find_each do |w| + ReindexItemJob.perform_later(w) + end end end end diff --git a/app/models/concerns/pdf_behavior.rb b/app/models/concerns/pdf_behavior.rb new file mode 100644 index 0000000000..af88f8189a --- /dev/null +++ b/app/models/concerns/pdf_behavior.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module RDF + class CustomShowPdfViewerTerm < Vocabulary('http://id.loc.gov/vocabulary/identifiers/') + property 'show_pdf_viewer' + end + + class CustomShowPdfDownloadButtonTerm < Vocabulary('http://id.loc.gov/vocabulary/identifiers/') + property 'show_pdf_download_button' + end +end + +module PdfBehavior + extend ActiveSupport::Concern + + included do + property :show_pdf_viewer, predicate: RDF::CustomShowPdfViewerTerm.show_pdf_viewer, multiple: false do |index| + index.as :stored_searchable + end + + # rubocop:disable Metrics/LineLength + property :show_pdf_download_button, predicate: RDF::CustomShowPdfDownloadButtonTerm.show_pdf_download_button, multiple: false do |index| + index.as :stored_searchable + end + # rubocop:enable Metrics/LineLength + + after_initialize :set_default_show_pdf_viewer, :set_default_show_pdf_download_button + end + + private + + # This is here so that the checkbox is checked by default + def set_default_show_pdf_viewer + self.show_pdf_viewer ||= '1' + end + + def set_default_show_pdf_download_button + self.show_pdf_download_button ||= '1' + end +end diff --git a/app/models/generic_work.rb b/app/models/generic_work.rb index f69aae6abb..ed7d2b53a5 100644 --- a/app/models/generic_work.rb +++ b/app/models/generic_work.rb @@ -2,10 +2,13 @@ class GenericWork < ActiveFedora::Base include ::Hyrax::WorkBehavior + include PdfBehavior include ::Hyrax::BasicMetadata + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_IIIF_PRINT', false)) include IiifPrint.model_configuration( - pdf_split_child_model: self + pdf_split_child_model: GenericWork, + pdf_splitter_service: IiifPrint::TenantConfig::PdfSplitter ) end diff --git a/app/models/image.rb b/app/models/image.rb index 51afe75997..3d6e7bf2de 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -4,9 +4,12 @@ # `rails generate hyrax:work Image` class Image < ActiveFedora::Base include ::Hyrax::WorkBehavior + include PdfBehavior + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_IIIF_PRINT', false)) include IiifPrint.model_configuration( - pdf_split_child_model: self + pdf_split_child_model: GenericWork, + pdf_splitter_service: IiifPrint::TenantConfig::PdfSplitter ) end diff --git a/app/models/solr_document.rb b/app/models/solr_document.rb index 2290e6986b..d14c1840b8 100644 --- a/app/models/solr_document.rb +++ b/app/models/solr_document.rb @@ -45,4 +45,24 @@ class SolrDocument title: 'title_tesim', type: 'human_readable_type_tesim' ) + + def show_pdf_viewer + self['show_pdf_viewer_tesim'] + end + + def show_pdf_download_button + self['show_pdf_download_button_tesim'] + end + + # @return [Array] a list of solr documents in no particular order + def load_parent_docs + query("member_ids_ssim: #{id}", rows: 1000) + .map { |res| ::SolrDocument.new(res) } + end + + # Query solr using POST so that the query doesn't get too large for a URI + def query(query, **opts) + result = Hyrax::SolrService.post(query, **opts) + result.fetch('response').fetch('docs', []) + end end diff --git a/app/presenters/hyku/work_show_presenter.rb b/app/presenters/hyku/work_show_presenter.rb index 12427bc418..2e9f38d99f 100644 --- a/app/presenters/hyku/work_show_presenter.rb +++ b/app/presenters/hyku/work_show_presenter.rb @@ -8,9 +8,17 @@ class WorkShowPresenter < Hyrax::WorkShowPresenter # Hyrax::MemberPresenterFactory.file_presenter_class = Hyrax::FileSetPresenter # Adds behaviors for hyrax-iiif_av plugin. include Hyrax::IiifAv::DisplaysIiifAv + + ## + # NOTE: IIIF Print prepends a IiifPrint::WorkShowPresenterDecorator to Hyrax::WorkShowPresenter + # However, with the above `include Hyrax::IiifAv::DisplaysIiifAv` we obliterate that logic. So + # we need to re-introduce that logic. + prepend IiifPrint::TenantConfig::WorkShowPresenterDecorator + Hyrax::MemberPresenterFactory.file_presenter_class = Hyrax::IiifAv::IiifFileSetPresenter - delegate :title_or_label, :extent, to: :solr_document + delegate :title_or_label, :extent, :source, :bibliographic_citation, :date, + :show_pdf_viewer, :show_pdf_download_button, to: :solr_document # OVERRIDE Hyrax v2.9.0 here to make featured collections work delegate :collection_presenters, to: :member_presenter_factory @@ -30,6 +38,18 @@ def isbns isbns&.flatten&.compact end + # OVERRIDE FILE from Hyrax v2.9.0 + # @return [String] title update for GenericWork + Hyrax::WorkShowPresenter.class_eval do + def page_title + if human_readable_type == "Generic Work" + "#{title.first} | ID: #{id} | #{I18n.t('hyrax.product_name')}" + else + "#{human_readable_type} | #{title.first} | ID: #{id} | #{I18n.t('hyrax.product_name')}" + end + end + end + # OVERRIDE here for featured collection methods # Begin Featured Collections Methods def collection_featurable? @@ -57,26 +77,46 @@ def user_can_feature_collection? end # End Featured Collections Methods - # @return [Boolean] render a IIIF viewer - def iiif_viewer? - Hyrax.config.iiif_image_server? && - representative_id.present? && - representative_presenter.present? && - iiif_media? && - members_include_viewable? + def show_pdf_viewer? + return unless Flipflop.default_pdf_viewer? + return unless show_pdf_viewer + return unless file_set_presenters.any?(&:pdf?) + + show_pdf_viewer.first.to_i.positive? end - private + def show_pdf_download_button? + return unless file_set_presenters.any?(&:pdf?) + return unless show_pdf_download_button - def iiif_media?(presenter: representative_presenter) - presenter.image? || presenter.video? || presenter.audio? || presenter.pdf? - end + show_pdf_download_button.first.to_i.positive? + end - def members_include_viewable? - file_set_presenters.any? do |presenter| - iiif_media?(presenter: presenter) && current_ability.can?(:read, presenter.id) + def viewer? + iiif_viewer? || video_embed_viewer? || show_pdf_viewer? + end + + def parent_works(current_user = nil) + @parent_works ||= begin + docs = solr_document.load_parent_docs + + if current_user + docs.select { |doc| current_user.ability.can?(:read, doc) } + else + docs.select(&:public?) end end + end + + def video_embed_viewer? + extract_video_embed_presence + end + + private + + def extract_video_embed_presence + solr_document[:video_embed_tesim]&.first&.present? + end def extract_from_identifier(rgx) if solr_document['identifier_tesim'].present? diff --git a/app/presenters/hyrax/generic_work_presenter.rb b/app/presenters/hyrax/generic_work_presenter.rb index 162b6ffe19..7ddaa3264b 100644 --- a/app/presenters/hyrax/generic_work_presenter.rb +++ b/app/presenters/hyrax/generic_work_presenter.rb @@ -4,5 +4,6 @@ # `rails generate hyrax:work GenericWork` module Hyrax class GenericWorkPresenter < Hyku::WorkShowPresenter + delegate :abstract, to: :solr_document end end diff --git a/app/services/iiif_print/tenant_config.rb b/app/services/iiif_print/tenant_config.rb new file mode 100644 index 0000000000..96bbaf95ca --- /dev/null +++ b/app/services/iiif_print/tenant_config.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/LineLength +module IiifPrint + ## + # This module encapsulates the logic for whether or not we'll use the IIIF Print services for the + # current tenant/account. The IIIF Print services does the following: + # + # - Skipping IIIF Print based derivative generation + # - Skipping PDF Splitting + # - Ignoring showing PDFs in the UV + # + # @note I am specifically isolating as much of this code into one module as possible, so that it + # it is hopefully easier to understand the configuration requirements and scope to this + # change. At some point, this might make sense to bring into IIIF Print directly. + # + # @see https://github.com/scientist-softserv/palni-palci/issues/656 palni-palci#656 + # @see https://github.com/scientist-softserv/palni-palci/issues/657 palni-palci#657 + # @see https://github.com/scientist-softserv/palni-palci/issues/658 palni-palci#658 + # @see https://github.com/scientist-softserv/palni-palci/issues/659 palni-palci#659 + module TenantConfig + ## + # When we were not planning on calling the underlying IiifPrint service but did due to some kind + # of faulty programming logic. + # + # @note This is raised as a guard to say "Hey, you thought you weren't using IIIF Print but your + # code's logic paths say otherwise." + class LeakyAbstractionError < StandardError + def initialize(klass:, method_name:) + super("Called #{klass}##{method_name} when we had said that #{klass} was not valid because we weren't using IIIF Print") + end + end + + ## + # If the default PDF viewer (PDF.js) is enabled, this method returns false, + # meaning the application should not use IIIF Print. If the default viewer is + # disabled, this method returns true, meaning the application should use IIIF Print. + def self.use_iiif_print? + !::Flipflop.default_pdf_viewer? + end + + ## + # This class implements the interface of the Hyrax::DerivativeService. It is responsible for + # negotiating whether or not the DerivativeService is "on" for the current tenant. + # + # @see https://github.com/samvera/hyrax/blob/08ef6c9a4fac489972eea9be53403e173f4ffb29/app/services/hyrax/derivative_service.rb Hyrax::DerivativeService + class DerivativeService + ## + # This allows you to specify the IIIF derivative service to use when the tenant has chosen to + # use IIIF Print for processing PDFs. + # + # If you are using the DerivativeRodeo, you'd specify something else. + class_attribute :iiif_service_class, default: ::IiifPrint::PluggableDerivativeService + + def initialize(file_set) + @file_set = file_set + end + + delegate :use_iiif_print?, to: TenantConfig + + def valid? + return false unless use_iiif_print? + + iiif_print_service_instance.valid? + end + + %i[create_derivatives cleanup_derivatives].each do |method_name| + define_method(method_name) do |*args| + raise LeakyAbstractionError.new(klass: self.class, method_name: method_name) unless use_iiif_print? + + iiif_print_service_instance.public_send(method_name, *args) + end + end + + ## + # @api private + # + # @note Public to ease testing. + def iiif_print_service_instance + @iiif_print_service_instance ||= iiif_service_class.new(@file_set) + end + end + + ## + # This is the pdf_splitter_service that will be used. If the tenant does not allow PDF splitting + # we will return an empty array. + # + # @example + # + # class MyWork + # include IiifPrint.model_configuration( + # pdf_split_child_model: Attachment, + # pdf_splitter_service: IiifPrint::TenantConfig::PdfSplitter, + # derivative_service_plugins: [ IiifPrint::TextExtractionDerivativeService ]) + # end + # + # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/lib/iiif_print.rb#L86-L138 Documentation for configuring + # @see https://github.com/scientist-softserv/adventist-dl/blob/d7676bdac2c672f09b28086d7145b68306978950/app/models/image.rb#L14-L20 Example implementation + module PdfSplitter + mattr_accessor :iiif_print_splitter + self.iiif_print_splitter = ::IiifPrint::SplitPdfs::PagesToJpgsSplitter + + ## + # @api public + def self.call(*args) + return [] unless TenantConfig.use_iiif_print? + + iiif_print_splitter.call(*args) + end + end + + ## + # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/lib/iiif_print/split_pdfs/child_work_creation_from_pdf_service.rb#L10-L46 Interface of FileSetActor#service + module SkipSplittingPdfService + ## + # @return [Symbol] Always :tenant_does_not_split_pdfs + def self.conditionally_enqueue(*_args) + :tenant_does_not_split_pdfs + end + end + + ## + # This decorator should ensure that we don't call model configured :pdf_splitter_service as + # documented in {TenantConfig::PdfSplitter} and the IIIF Print gem. It avoids the potentially + # expensive conditionally enqueue logic of the super class. + # + # Why not make an `app/actors/hyrax/actors/file_set_actor_decorator.rb`? It would be lost in that + # it is decorating the decoration of the IIIF Print gem. Beside, in bringing this here, we have + # a relatively singular place for all of the configurations. + module FileSetActorDecorator + ## + # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/app/actors/iiif_print/actors/file_set_actor_decorator.rb#L33-L35 Method we're overriding + def service + return TenantConfig::SkipSplittingPdfService unless TenantConfig.use_iiif_print? + + super + end + end + + ## + # OVERRIDE IiifPrint::WorkShowPresenterDecorator + # OVERRIDE Hyrax::WorkShowPresenter + # + # In IiifPrint we overrided #members_include_viewable_image? to query for both file sets and + # child works. (Child works being the pages split off of a PDF) + # + # In Hyrax::WorkShowPresenter we're only looking at the underlying file_sets. But IiifPrint + # needs to look at multiple places. + module WorkShowPresenterDecorator + ## + # @return [Array] predicate methods (e.g. ending in "?") that reflect the types + # of files we want to consider for showing in the IIIF Viewer. + def iiif_media_predicates + if TenantConfig.use_iiif_print? + %i[image? audio? video? pdf?] + else + %i[image? audio? video?] + end + end + + def iiif_media?(presenter: representative_presenter) + iiif_media_predicates.any? { |predicate| presenter.try(predicate) || presenter.try(:solr_document).try(predicate) } + end + + ## + # @return [Boolean] render a IIIF viewer + # + # OVERRIDE Hyrax::WorkShowPresenter; this override introduces behavior to handle over-rides. + def iiif_viewer? + Hyrax.config.iiif_image_server? && + representative_id.present? && + representative_presenter.present? && + iiif_media? && + members_include_iiif_viewable? + end + + def members_include_iiif_viewable? + iiif_presentable_member_presenters.any? do |presenter| + iiif_media?(presenter: presenter) && current_ability.can?(:read, presenter.id) + end + end + + ## + # @return [Array] An array of presenter objects + # + # In a non-IIIF Print using scenario, we use the file_set_presenters value; that is for + # objects that are very specifically file_sets. + # + # In a IIIF Print using scenario, we use the ill-named 'file_set_ids_ssim', because a + # long-standing decision is that this field will have both file_set IDs and child work IDs. + def iiif_presentable_member_presenters + if TenantConfig.use_iiif_print? + presentable_member_ids = Array.wrap(solr_document.try(:file_set_ids) || solr_document.try(:[], 'file_set_ids_ssim')) + member_presenters_for(presentable_member_ids) + else + file_set_presenters + end + end + end + end +end +# rubocop:enable Metrics/LineLength diff --git a/app/views/advanced/_advanced_search_fields.html.erb b/app/views/advanced/_advanced_search_fields.html.erb new file mode 100644 index 0000000000..2f98780be4 --- /dev/null +++ b/app/views/advanced/_advanced_search_fields.html.erb @@ -0,0 +1,35 @@ +<% + # OVERRIDE blacklight_advanced_search v6.4.1 to split the first six fields + # See: CatalogController#search_fields_without_customization= + # See: Blacklight::BlacklightHelperBehavior#primary_search_fields, #secondary_search_fields + # Also using Bootstrap 3 classes to make the form resemble the Hyrax form behaviors +%> +<%- primary_search_fields_for(search_fields_for_advanced_search).each do |key, field_def| -%> +
+ <%= label_tag key, "#{field_def.label}", :class => "col-sm-3 control-label" %> +
+ <% if local_authority?(key) %> + <%= render 'advanced_search_fields_qa', key: key %> + <% else %> + <%= text_field_tag key, label_tag_default_for(key), :class => 'form-control' %> + <% end %> +
+
+<%- end -%> + + + + diff --git a/app/views/advanced/_advanced_search_fields_qa.html.erb b/app/views/advanced/_advanced_search_fields_qa.html.erb new file mode 100644 index 0000000000..24e8ff7cda --- /dev/null +++ b/app/views/advanced/_advanced_search_fields_qa.html.erb @@ -0,0 +1,4 @@ +<%= select_tag key, + options_for_select(options_for_qa_select(key)), + class: 'form-control', + include_blank: true %> 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 c92d3b4c08..6fad2ad6d0 100644 --- a/app/views/hyrax/admin/appearances/_banner_image_form.html.erb +++ b/app/views/hyrax/admin/appearances/_banner_image_form.html.erb @@ -2,18 +2,86 @@
<% require_image = @form.banner_image? ? false : true %> <%# Upload Banner Image %> - <%= f.input :banner_image, as: :file, wrapper: :vertical_file_input, required: require_image, hint: t('hyrax.admin.appearances.show.forms.banner_image.hint') %> + <%= f.input :banner_image, as: :file, wrapper: :vertical_file_input, required: require_image, hint: t('hyrax.admin.appearances.show.forms.banner_image.hint').html_safe, input_html: { name: 'admin_appearance[banner_image]' } %> <%= f.input :banner_image_text, required: true, as: :text, label: 'Banner image alt text' %> - <%= image_tag @form.banner_image.url, class: "img-responsive" if @form.banner_image? %> + + + +
+
+ Banner Image Preview Area +
+ <% end %> + <% if @form.banner_image? %> -<% end %> \ No newline at end of file +<% end %> + +<%# TODO: move this into the assets folder and make it work %> + diff --git a/app/views/hyrax/admin/features/index.html.erb b/app/views/hyrax/admin/features/index.html.erb new file mode 100644 index 0000000000..f1bd85cc3a --- /dev/null +++ b/app/views/hyrax/admin/features/index.html.erb @@ -0,0 +1,76 @@ +<% + # OVERRIDE Hyrax 3.6.0 to make the default PDF viewer switch more intuitive and some styling changes. + # Instead of saying on/off it says PDF.js/IIIF Print. +%> + +<% provide :page_header do %> +

<%= t('.header') %>

+<% end %> +
+
+
+
+
+ + + + + + + + + + + <% @feature_set.grouped_features.each do |group, features| -%> + <% if @feature_set.grouped? -%> + + + + + <% end -%> + <% features.each do |feature| %> + + + + + + <% @feature_set.strategies.each do |strategy| -%> + <% next unless strategy.is_a? Flipflop::Strategies::ActiveRecordStrategy %> + <%# OVERRIDE to add min-width so Actions column can display all toggles on the same line %> + <%# adjust min-width as needed for future overrides %> + + <% end -%> + + <% end -%> + <% end -%> + +
<%= t('.feature') %><%= t('.description') %><%= t('.action') %>
+

+ <%= t(group ? group.name : :default, scope: [:flipflop, :groups], default: group ? group.title : nil) -%> +

+
+ <%= status_for(feature) -%> + <%= feature.name.humanize -%><%= feature.description -%> +
+ <%= form_tag(hyrax.admin_feature_strategy_path(feature.key, strategy.key), method: :put) do -%> +
+ <%# OVERRIDE to use helper, see FeaturesHelper %> + <%= submit_tag on(feature.name.to_sym), + type: "submit", + class: Flipflop.enabled?(feature.name.to_sym) ? 'active' : nil, + disabled: !strategy.switchable? -%> + + <%# OVERRIDE to use helper, see FeaturesHelper %> + <%= submit_tag off(feature.name.to_sym), + type: "submit", + class: Flipflop.enabled?(feature.name.to_sym) ? nil : 'active', + disabled: !strategy.switchable? -%> +
+ <% end -%> +
+
+
+
+
+
+
diff --git a/app/views/hyrax/base/_analytics_button.html.erb b/app/views/hyrax/base/_analytics_button.html.erb new file mode 100644 index 0000000000..bb1da454d7 --- /dev/null +++ b/app/views/hyrax/base/_analytics_button.html.erb @@ -0,0 +1,5 @@ +<% if Hyrax.config.analytics? %> + <% # turbolinks needs to be turned off or the page will use the cache and the %> + <% # analytics graph will not show unless the page is refreshed. %> + <%= link_to t('.analytics'), @presenter.stats_path, id: 'stats', class: 'btn btn-default btn-block center-block', data: { turbolinks: false } %> +<% end %> \ No newline at end of file diff --git a/app/views/hyrax/base/_download_pdf.html.erb b/app/views/hyrax/base/_download_pdf.html.erb new file mode 100644 index 0000000000..b794520cf3 --- /dev/null +++ b/app/views/hyrax/base/_download_pdf.html.erb @@ -0,0 +1,14 @@ +<% if can?(:download, file_set_id) && (Site.account.settings[:allow_downloads].nil? || Site.account.settings[:allow_downloads].to_i.nonzero?) %> +
+ <% if @presenter.representative_presenter.present? && presenter.file_set_presenters.any?(&:pdf?) %> + <%= button_tag type: 'button', + id: "download-pdf-button", + data: { label: @presenter.representative_presenter.id, + path: hyrax.download_path(presenter.representative_presenter) }, + class: "btn btn-success btn-block download-pdf-button center-block", + onclick: "window.open(this.dataset.path, '_blank');" do %> + Download PDF + <% end %> + <% end %> +
+<% end %> diff --git a/app/views/hyrax/base/_form_files.html.erb b/app/views/hyrax/base/_form_files.html.erb new file mode 100644 index 0000000000..d4017615fe --- /dev/null +++ b/app/views/hyrax/base/_form_files.html.erb @@ -0,0 +1,69 @@ +<%# OVERRIDE HYRAX 3.6.0 to add show_pdf_viewer to files tab%> +<% if render_show_pdf_behavior_checkbox? %> + <%= render partial: 'show_pdf_viewer', locals: { f: f } %> + <%= render partial: 'show_pdf_download_button', locals: { f: f } %> +<% end %> +
+ + + + + <% if Hyrax.config.browse_everything? %> + <%= t('hyrax.base.form_files.local_upload_browse_everything_html', contact_href: link_to(t("hyrax.upload.alert.contact_href_text"), hyrax.contact_form_index_path)) %> + <% else %> + <%= t('hyrax.base.form_files.local_upload_html') %> + <% end %> + +
+
+
+
+ + +
+
+ + +
+ <% if Hyrax.config.browse_everything? %> + <%= button_tag(type: 'button', class: 'btn btn-success', id: "browse-btn", + 'data-toggle' => 'browse-everything', 'data-route' => browse_everything_engine.root_path, + 'data-target' => "#{f.object.persisted? ? "#edit_#{f.object.model.model_name.param_key}_#{f.object.model.id}" : "#new_#{f.object.model.model_name.param_key}"}" ) do %> + + <%= t('hyrax.upload.browse_everything.browse_files_button') %> + <% end %> + <% end %> + + + +
+
+
+
+ +
+ +
+
+
+ +
 
+
+
+
+
+
+ <%= t('hyrax.base.form_files.dropzone') %> +
+
+ +<%= render 'hyrax/uploads/js_templates' %> diff --git a/app/views/hyrax/base/_pdf_js.erb b/app/views/hyrax/base/_pdf_js.erb new file mode 100644 index 0000000000..0b65cdca36 --- /dev/null +++ b/app/views/hyrax/base/_pdf_js.erb @@ -0,0 +1,7 @@ +
+ +
diff --git a/app/views/hyrax/base/_relationships.html.erb b/app/views/hyrax/base/_relationships.html.erb new file mode 100644 index 0000000000..72727e6c38 --- /dev/null +++ b/app/views/hyrax/base/_relationships.html.erb @@ -0,0 +1,39 @@ +<%# OVERRIDE Hyrax 2.9 to make relationships only show if they exist %> +<% if !current_user && presenter.grouped_presenters.present? %> + <%# Collection %> +
+

<%= t('hyrax.base.show.relationships') %>

+ <%= render 'relationships_parent_rows', presenter: presenter %> +
+<% end %> + +<% if current_user %> +
+

<%= t('hyrax.base.show.relationships') %>

+ <%# Admin Set %> + <%= presenter.attribute_to_html(:admin_set, render_as: :faceted, html_dl: true) %> + + <%# Collection %> + <% presenter.grouped_presenters(except: presenter.presenter_types).each_pair do |model_name, items| %> + <%= render 'relationships_parent_row', type: model_name, items: items, presenter: presenter %> + <% end %> + <%# Render grouped presenters. Show rows if there are any items of that type %> + <% presenter.presenter_types.each do |type| %> + <% presenter.grouped_presenters(filtered_by: type).each_pair do |_, items| %> + <%= render 'relationships_parent_row', type: type, items: items, presenter: presenter %> + <% end %> + <% end %> + + <%# Parent Work %> + <%# Render a link back to its parent works %> + <% if presenter.parent_works(current_user).present? %> + <%= render 'relationships_parent_works_rows', presenter: presenter %> + <% end %> +
+<% else %> + <%# Parent Work %> + <%# Render a link back to its parent works %> + <% if presenter.parent_works(current_user).present? %> + <%= render 'relationships_parent_works_rows', presenter: presenter %> + <% end %> +<% end %> diff --git a/app/views/hyrax/base/_relationships_parent_works_rows.html.erb b/app/views/hyrax/base/_relationships_parent_works_rows.html.erb new file mode 100644 index 0000000000..4a03ec6e68 --- /dev/null +++ b/app/views/hyrax/base/_relationships_parent_works_rows.html.erb @@ -0,0 +1,12 @@ +
<%= t('hyrax.base.part_of.label') %>
+
+ +
    + <% presenter.parent_works(current_user).each do |parent_doc| %> +
  • + <%= link_to parent_doc.title.first, parent_path(parent_doc) %> +
  • + <% end %> +
+ +
diff --git a/app/views/hyrax/base/_representative_media.html.erb b/app/views/hyrax/base/_representative_media.html.erb new file mode 100644 index 0000000000..b575576780 --- /dev/null +++ b/app/views/hyrax/base/_representative_media.html.erb @@ -0,0 +1,12 @@ +<% if presenter.representative_id.present? && presenter.representative_presenter.present? %> + <% if defined?(viewer) && viewer && presenter.iiif_viewer?%> + <%= iiif_viewer_display presenter %> + <% elsif Flipflop.default_pdf_viewer? && presenter.show_pdf_viewer? && presenter.file_set_presenters.any?(&:pdf?) %> + <%= render 'pdf_js', file_set_presenter: presenter.file_set_presenters.first %> + <% else %> + <%= render media_display_partial(presenter.representative_presenter), file_set: presenter.representative_presenter %> + <% end %> +<% else %> + <% alt = block_for(name: 'default_work_image_text') || 'Default work thumbnail' %> + <%= image_tag default_work_image, class: "canonical-image", alt: alt %> +<% end %> diff --git a/app/views/hyrax/base/_show_actions.html.erb b/app/views/hyrax/base/_show_actions.html.erb index b726f7dba8..eb9c0e049f 100644 --- a/app/views/hyrax/base/_show_actions.html.erb +++ b/app/views/hyrax/base/_show_actions.html.erb @@ -38,11 +38,6 @@ data: { behavior: 'unfeature' }, class: presenter.display_feature_link? ? 'btn btn-default collapse' : 'btn btn-default' %> <% end %> - <% if Hyrax.config.analytics? %> - <% # turbolinks needs to be turned off or the page will use the cache and the %> - <% # analytics graph will not show unless the page is refreshed. %> - <%= link_to t('.analytics'), presenter.stats_path, id: 'stats', class: 'btn btn-default', data: { turbolinks: false } %> - <% end %> diff --git a/app/views/hyrax/base/_show_pdf_download_button.erb b/app/views/hyrax/base/_show_pdf_download_button.erb new file mode 100644 index 0000000000..aa326db706 --- /dev/null +++ b/app/views/hyrax/base/_show_pdf_download_button.erb @@ -0,0 +1,4 @@ +<%= f.input :show_pdf_download_button, + label: "Show Download PDF button", + required: f.object.required?(:show_pdf_download_button), + as: :boolean %> diff --git a/app/views/hyrax/base/_show_pdf_viewer.html.erb b/app/views/hyrax/base/_show_pdf_viewer.html.erb new file mode 100644 index 0000000000..a2478201be --- /dev/null +++ b/app/views/hyrax/base/_show_pdf_viewer.html.erb @@ -0,0 +1,4 @@ +<%= f.input :show_pdf_viewer, + label: "Show PDF.js Viewer", + required: f.object.required?(:show_pdf_viewer), + as: :boolean %> diff --git a/app/views/hyrax/base/show.html.erb b/app/views/hyrax/base/show.html.erb index 01ccf18ca8..725db7d53d 100644 --- a/app/views/hyrax/base/show.html.erb +++ b/app/views/hyrax/base/show.html.erb @@ -19,11 +19,16 @@
<%= render 'representative_media', presenter: @presenter, viewer: true %>
+ <% elsif @presenter.show_pdf_viewer? %> +
+ <%= render 'pdf_js', file_set_presenter: @presenter.file_set_presenters.first %> +
<% end %>
- <%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.iiif_viewer? %> + <%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.iiif_viewer? || @presenter.show_pdf_viewer? %> + <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> <%= render 'citations', presenter: @presenter %> - <%= render 'social_media' %> + <%= render 'analytics_button', presenter: @presenter %>
<%= render 'work_description', presenter: @presenter %> diff --git a/app/views/hyrax/file_sets/_actions.html.erb b/app/views/hyrax/file_sets/_actions.html.erb new file mode 100644 index 0000000000..ea6941a015 --- /dev/null +++ b/app/views/hyrax/file_sets/_actions.html.erb @@ -0,0 +1,58 @@ +<%# Overridden from Hyrax 3.5.0 - To add extra download restrictions %> +<% if (can?(:download, file_set.id) || can?(:destroy, file_set.id) || can?(:edit, file_set.id)) && !workflow_restriction?(@parent) %> + <% if can?(:download, file_set.id) && !(can?(:edit, file_set.id) || can?(:destroy, file_set.id)) %> + <% if (Site.account.settings[:allow_downloads].nil? || Site.account.settings[:allow_downloads].to_i.nonzero?) && + (@presenter.show_pdf_download_button? && file_set.pdf? || !file_set.pdf?) %> + <%= link_to t('.download'), + hyrax.download_path(file_set), + class: 'btn btn-default btn-sm', + title: t('.download_title', file_set: file_set), + target: "_blank", + id: "file_download", + data: { label: file_set.id, work_id: @presenter.id, collection_ids: @presenter.member_of_collection_ids } %> + <% end %> + <% else %> +
+ + + +
+ <% end %> +<% end %> diff --git a/app/views/hyrax/file_sets/media_display/_pdf.html.erb b/app/views/hyrax/file_sets/media_display/_pdf.html.erb new file mode 100644 index 0000000000..c34bb55d26 --- /dev/null +++ b/app/views/hyrax/file_sets/media_display/_pdf.html.erb @@ -0,0 +1,17 @@ +<%# Overridden from Hyrax 3.5.0 - To add extra download restrictions %> +<% if display_media_download_link?(file_set: file_set) && (Site.account.settings[:allow_downloads].nil? || Site.account.settings[:allow_downloads].to_i.nonzero?) %> +
+

<%= t('hyrax.file_set.show.downloadable_content.heading') %>

+ <%= image_tag thumbnail_url(file_set), + class: "representative-media", + alt: "", + role: "presentation" %> +
+<% else %> +
+ <%= image_tag thumbnail_url(file_set), + class: "representative-media", + alt: "", + role: "presentation" %> +
+<% end %> diff --git a/app/views/records/edit_fields/_default.html.erb b/app/views/records/edit_fields/_default.html.erb new file mode 100644 index 0000000000..0e484100ff --- /dev/null +++ b/app/views/records/edit_fields/_default.html.erb @@ -0,0 +1,19 @@ +<%# OVERRIDE: HydraEditor 5.0.5 support dynamic labels and hints for custom worktypes %> +<%# Avoid NoMethod error when rendering partial in Collection or batch edit form %> +<%# Hide unwanted fields from the form %> +<% return if f.object.try(:hidden?, key) %> + +<% record = f.object.model %> +<% if f.object.multiple? key %> + <%= f.input key, + as: :multi_value, + label: label_for(term: key, record_class: record.class), + hint: hint_for(term: key, record_class: record.class), + input_html: { class: 'form-control' }, + required: f.object.required?(key) %> +<% else %> + <%= f.input key, + label: label_for(term: key, record_class: record.class), + hint: hint_for(term: key, record_class: record.class), + required: f.object.required?(key) %> +<% end %> diff --git a/app/views/shared/_appearance_styles.html.erb b/app/views/shared/_appearance_styles.html.erb index cb07843ad5..43efe19312 100644 --- a/app/views/shared/_appearance_styles.html.erb +++ b/app/views/shared/_appearance_styles.html.erb @@ -43,7 +43,13 @@ body.public-facing .image-masthead .navbar .active > a:focus { background-color: <%= appearance.navbar_background_color_active %>; color: <%= appearance.navbar_link_text_color %>; } + +body.public-facing .image-masthead .navbar .navbar-nav li.active a { + background-color: <%= appearance.navbar_link_background_color_active %>; +} + body.public-facing .image-masthead .navbar .navbar-nav a { + background-color: <%= appearance.navbar_link_background_color %>; color: <%= appearance.navbar_link_text_color %>; } diff --git a/app/views/themes/cultural_repository/layouts/homepage.html.erb b/app/views/themes/cultural_repository/layouts/homepage.html.erb index e2e20c18a5..1f07ec851e 100644 --- a/app/views/themes/cultural_repository/layouts/homepage.html.erb +++ b/app/views/themes/cultural_repository/layouts/homepage.html.erb @@ -2,8 +2,7 @@ <% content_for(:navbar) do %>
-
- +
')">
<% if controller_name == 'homepage' %>
diff --git a/app/views/themes/cultural_show/hyrax/base/_relationships.html.erb b/app/views/themes/cultural_show/hyrax/base/_relationships.html.erb index 0a73cb5cd7..09e014a01a 100644 --- a/app/views/themes/cultural_show/hyrax/base/_relationships.html.erb +++ b/app/views/themes/cultural_show/hyrax/base/_relationships.html.erb @@ -1,9 +1,11 @@ <% if !current_user && presenter.grouped_presenters.present? %> + <%# Collection %>

<%= t('hyrax.base.show.relationships') %>

<%= render 'relationships_parent_rows', presenter: presenter %>
<% end %> + <% if current_user %>

<%= t('hyrax.base.show.relationships') %>

@@ -17,5 +19,15 @@ <%= render 'relationships_parent_row', type: type, items: items, presenter: presenter %> <% end %> <% end %> + <%# Render a link back to its parent works %> + <% if presenter.parent_works(current_user).present? %> + <%= render 'relationships_parent_works_rows', presenter: presenter %> + <% end %>
+<% else %> + <%# Parent Work %> + <%# Render a link back to its parent works %> + <% if presenter.parent_works(current_user).present? %> + <%= render 'relationships_parent_works_rows', presenter: presenter %> + <% end %> <% end %> diff --git a/app/views/themes/cultural_show/hyrax/base/show.html.erb b/app/views/themes/cultural_show/hyrax/base/show.html.erb index e69ec0e648..205aaa468f 100644 --- a/app/views/themes/cultural_show/hyrax/base/show.html.erb +++ b/app/views/themes/cultural_show/hyrax/base/show.html.erb @@ -11,31 +11,37 @@
<%= render 'workflow_actions_widget', presenter: @presenter %> - <% if @presenter.universal_viewer? %> +
+ <%= render "show_actions", presenter: @presenter %> +
+ <% if @presenter.iiif_viewer? %>
<%= render 'representative_media', presenter: @presenter, viewer: true %>
+ <% elsif @presenter.show_pdf_viewer? %> +
+ <%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %> +
+ <% else %> +
+ <%= render 'representative_media', presenter: @presenter, viewer: false %> +
<% end %> -
- <%= render "show_actions", presenter: @presenter %> -
-
- <%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.universal_viewer? %> -
-
+
<%= render 'work_description', presenter: @presenter %> <%= render 'metadata', presenter: @presenter %>
-
+
<%= render 'relationships', presenter: @presenter %>
+ <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> + <%= render 'citations', presenter: @presenter %> + + <%#= render 'analytics_button', presenter: @presenter %>
<%= render 'items', presenter: @presenter %> - <%# TODO: we may consider adding these partials in the future %> - <%# = render 'sharing_with', presenter: @presenter %> - <%# = render 'user_activity', presenter: @presenter %>
diff --git a/app/views/themes/image_show/hyrax/base/show.html.erb b/app/views/themes/image_show/hyrax/base/show.html.erb new file mode 100644 index 0000000000..17475eaba9 --- /dev/null +++ b/app/views/themes/image_show/hyrax/base/show.html.erb @@ -0,0 +1,65 @@ +<% content_for(:extra_body_classes, 'works-show ') %> + +<% provide :page_title, @presenter.page_title %> + +<%= render 'shared/citations' %> +<%= render './shared/additional_citations' %> +
+
+ <%= render 'work_type', presenter: @presenter %> +
+
+
+
+ <%= render 'work_title', presenter: @presenter %> +
+
+
+ <%= render 'workflow_actions_widget', presenter: @presenter %> + <% if @presenter.universal_viewer? %> +
+ <%= render 'representative_media', presenter: @presenter, viewer: true %> +
+
+
+ <%= render 'work_description', presenter: @presenter %> +
+
+ <% elsif Flipflop.default_pdf_viewer? && @presenter.show_pdf_viewer? && @presenter.file_set_presenters.any?(&:pdf?) %> +
+ <%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %> +
+ <% else %> +
+ <%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.universal_viewer? || @presenter.show_pdf_viewer? %> +
+
+ <%= render 'work_description', presenter: @presenter %> +
+ <% end %> +
+
+ +
+ <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> + <%= render 'citations', presenter: @presenter %> + + <%#= render 'analytics_button', presenter: @presenter %> +
+
+ <%= render 'relationships', presenter: @presenter %> + <% if @presenter.class == Hyrax::OerPresenter %> + <%= render 'hyrax/oers/related_items', presenter: @presenter %> + <% end %> + <%= render 'items', presenter: @presenter %> + <%# TODO: we may consider adding these partials in the future %> + <%# = render 'sharing_with', presenter: @presenter %> + <%# = render 'user_activity', presenter: @presenter %> +
+
+
+
+
+
diff --git a/app/views/themes/institutional_repository/hyrax/homepage/_resource_type_stats.html.erb b/app/views/themes/institutional_repository/hyrax/homepage/_resource_type_stats.html.erb index b8f0476e72..01d54c28d8 100644 --- a/app/views/themes/institutional_repository/hyrax/homepage/_resource_type_stats.html.erb +++ b/app/views/themes/institutional_repository/hyrax/homepage/_resource_type_stats.html.erb @@ -1,12 +1,14 @@ \ No newline at end of file +
diff --git a/app/views/themes/institutional_repository/hyrax/homepage/all_collections.html.erb b/app/views/themes/institutional_repository/hyrax/homepage/all_collections.html.erb index df3994bbe2..15ab236e1c 100644 --- a/app/views/themes/institutional_repository/hyrax/homepage/all_collections.html.erb +++ b/app/views/themes/institutional_repository/hyrax/homepage/all_collections.html.erb @@ -1,6 +1,6 @@

Browse by Collection

-<% top_level_collections = @collections.select{ |c| c['nesting_collection__deepest_nested_depth_isi'] == 1 }%> -<% top_level_collections.each do |collection| %> + +<% @top_level_collections.each do |collection| %>
- <% roles = @presenter.user_roles(user) %> + <% roles = @presenter.user_group_roles(user) %>
    <% roles.each do |role| %> -
  • <%= role.titleize %>
  • +
  • <%= role.name.titleize %>
  • <% end %>
+ + <% roles = @presenter.user_site_roles(user) %> +
    + <% roles.each do |role| %> +
  • + <%= role.name.titleize %> + <%= link_to main_app.remove_role_admin_user_path(id: user.id, role_id: role.id), method: :delete, data: { confirm: t('hyrax.admin.users.roles.remove.confirmation', user: user.email, role: role.name.titleize) } do %> + <% if current_ability.admin? || (can?(:edit, User) && role.name != 'admin') %> + + <% end %> + <% end %> +
  • + <% end %> +
+ + <%# in the case that a user is created who never signs in, this is necessary %> diff --git a/app/views/hyrax/base/_form.html.erb b/app/views/hyrax/base/_form.html.erb new file mode 100644 index 0000000000..38a9862c2c --- /dev/null +++ b/app/views/hyrax/base/_form.html.erb @@ -0,0 +1,24 @@ +<%# OVERRIDE Hyrax 3.6.0 to remove the broken error alerts from form validations %> + +<%= simple_form_for [main_app, @form], + html: { + data: { behavior: 'work-form', + 'param-key' => @form.model_name.param_key }, + multipart: true + } do |f| %> + <%# OVERRIDE Here to remove broken error alert from form validation %> + <% if Flipflop.batch_upload? && !f.object.persisted? %> + <% provide :metadata_tab do %> +

<%= t('.batch_upload_hint') %> <%= link_to t('.batch_link'), hyrax.new_batch_upload_path(payload_concern: @form.model.class) %>

+ <% end %> + <% end %> + <%= render 'hyrax/base/guts4form', f: f, tabs: form_tabs_for(form: f.object) %> +<% end %> + + \ No newline at end of file diff --git a/app/views/hyrax/base/_video_embed_viewer.html.erb b/app/views/hyrax/base/_video_embed_viewer.html.erb new file mode 100644 index 0000000000..05ca0a939c --- /dev/null +++ b/app/views/hyrax/base/_video_embed_viewer.html.erb @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/app/views/hyrax/base/show.html.erb b/app/views/hyrax/base/show.html.erb index 725db7d53d..5bcdd90ddd 100644 --- a/app/views/hyrax/base/show.html.erb +++ b/app/views/hyrax/base/show.html.erb @@ -1,3 +1,6 @@ +<%# OVERRIDE: Hyrax 3.4.1 to remove social media %> +<%# to add work-show class to works pages %> + <% content_for(:extra_body_classes, 'works-show ') %> <% provide :page_title, @presenter.page_title %> @@ -15,21 +18,31 @@
<%= render 'workflow_actions_widget', presenter: @presenter %> - <% if @presenter.iiif_viewer? %> + <% if @presenter.video_embed_viewer? %> + <%= render 'video_embed_viewer', presenter: @presenter %> + <% elsif @presenter.iiif_viewer? %>
<%= render 'representative_media', presenter: @presenter, viewer: true %>
<% elsif @presenter.show_pdf_viewer? %>
- <%= render 'pdf_js', file_set_presenter: @presenter.file_set_presenters.first %> + <%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %> +
+ <% else %> +
+ <%= render 'representative_media', presenter: @presenter, viewer: false %> + <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> + <%= render 'citations', presenter: @presenter %> +
+ <% end %> + <% if @presenter.viewer? %> +
+ <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> + <%= render 'citations', presenter: @presenter %> + + <%#= render 'analytics_button', presenter: @presenter %>
<% end %> -
- <%= render 'representative_media', presenter: @presenter, viewer: false unless @presenter.iiif_viewer? || @presenter.show_pdf_viewer? %> - <%= render('download_pdf', presenter: @presenter, file_set_id: @presenter.file_set_presenters.first.id) if @presenter.show_pdf_download_button? %> - <%= render 'citations', presenter: @presenter %> - <%= render 'analytics_button', presenter: @presenter %> -
<%= render 'work_description', presenter: @presenter %> <%= render 'metadata', presenter: @presenter %> @@ -60,11 +73,10 @@ <%# = render 'sharing_with', presenter: @presenter %> <%# = render 'user_activity', presenter: @presenter %> - - <% @presenter.member_of_collection_ids.each do |collection_id| %> - - <% end %> - + + <% @presenter.member_of_collection_ids.each do |collection_id| %> + + <% end %>
diff --git a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb index ec2bce02c5..bc33204a1c 100644 --- a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb @@ -3,61 +3,59 @@ <% if can? :manage, Site %>
  • <%= menu.collapsable_section t('hyrax.admin.sidebar.settings'), - icon_class: "fa fa-cog", - id: 'collapseSettings', - open: menu.settings_section?, - title: t('hyrax.admin.sidebar.settings') do %> - <%= menu.nav_link(main_app.edit_admin_account_path, - title: t('hyrax.admin.sidebar.account')) do %> + icon_class: "fa fa-cog", + id: 'collapseSettings', + open: menu.settings_section? do %> + + <%= menu.nav_link(main_app.edit_admin_account_path) do %> <%= t('hyrax.admin.sidebar.account') %> <% end %> - <%= menu.nav_link(main_app.identity_providers_path) do %> - <%= t('hyrax.admin.sidebar.identity_providers') %> + <% if Flipflop.show_identity_provider_in_admin_dashboard? %> + <%= menu.nav_link(main_app.identity_providers_path) do %> + <%= t('hyrax.admin.sidebar.identity_providers') %> + <% end %> <% end %> - <%= menu.nav_link(main_app.edit_site_labels_path, - title: t('hyrax.admin.sidebar.labels')) do %> + <%= menu.nav_link(main_app.edit_site_labels_path) do %> <%= t('hyrax.admin.sidebar.labels') %> <% end %> <% if can?(:update, :appearance) %> - <%= menu.nav_link(hyrax.admin_appearance_path, - title: t('hyrax.admin.sidebar.appearance')) do %> + <%= menu.nav_link(hyrax.admin_appearance_path) do %> <%= t('hyrax.admin.sidebar.appearance') %> <% end %> <% end %> <% if can?(:manage, :collection_types) %> - <%= menu.nav_link(hyrax.admin_collection_types_path, - title: t('hyrax.admin.sidebar.collection_types')) do %> + <%= menu.nav_link(hyrax.admin_collection_types_path) do %> <%= t('hyrax.admin.sidebar.collection_types') %> <% end %> <% end %> <% if can?(:manage, Hyrax::Feature) %> - <%= menu.nav_link(hyrax.edit_pages_path, - title: t('hyrax.admin.sidebar.pages')) do %> + <%= menu.nav_link(hyrax.edit_pages_path) do %> <%= t('hyrax.admin.sidebar.pages') %> <% end %> - <%= menu.nav_link(hyrax.edit_content_blocks_path, - title: t('hyrax.admin.sidebar.content_blocks')) do %> + <%= menu.nav_link(hyrax.edit_content_blocks_path) do %> <%= t('hyrax.admin.sidebar.content_blocks') %> <% end %> - <%= menu.nav_link(hyrax.admin_features_path, - title: t('hyrax.admin.sidebar.technical')) do %> + <%= menu.nav_link(hyrax.admin_features_path) do %> <%= t('hyrax.admin.sidebar.technical') %> <% end %> - <%= menu.nav_link('/admin/work_types/edit', - title: t('hyku.admin.work_types')) do %> + <%= menu.nav_link('/admin/work_types/edit') do %> <%= t('hyku.admin.work_types') %> <% end %> <% end %>
  • <% end %> - <% if can?(:manage, Sipity::WorkflowResponsibility) %> - <%= menu.nav_link(hyrax.admin_workflow_roles_path, - title: t('hyrax.admin.sidebar.workflow_roles')) do %> + <% if Flipflop.show_workflow_roles_menu_item_in_admin_dashboard_sidebar? && can?(:manage, Sipity::WorkflowResponsibility) %> + <%= menu.nav_link(hyrax.admin_workflow_roles_path) do %> <%= t('hyrax.admin.sidebar.workflow_roles') %> <% end %> <% end # end of configuration block %> <%= render 'hyrax/dashboard/sidebar/menu_partials', menu: menu, section: :configuration %> <% end %> + <% if can?(:update, RolesService) %> + <%= menu.nav_link(main_app.admin_roles_service_jobs_path) do %> + <%= t('hyrax.admin.sidebar.roles_service_jobs') %> + <% end %> + <% end %> <% end %> diff --git a/app/views/hyrax/my/_search_header.html.erb b/app/views/hyrax/my/_search_header.html.erb new file mode 100644 index 0000000000..fd05378767 --- /dev/null +++ b/app/views/hyrax/my/_search_header.html.erb @@ -0,0 +1,17 @@ +<%# OVERRIDE Hyrax 3.6.0 to add catalog/sort_widget %> +<%= render 'did_you_mean' %> + +<%= render 'facets' %> +<%= render 'constraints' %> + +
    +
    + <%= render 'hyrax/my/sort_and_per_page' %> + <%= render 'catalog/sort_widget' %> +
    +
    + <%= render 'search_form' %> +
    +
    + +<%= render 'batch_actions' %> diff --git a/app/views/layouts/hyrax/dashboard.html.erb b/app/views/layouts/hyrax/dashboard.html.erb new file mode 100644 index 0000000000..5757dc589e --- /dev/null +++ b/app/views/layouts/hyrax/dashboard.html.erb @@ -0,0 +1,41 @@ +<%# OVERRIDE from Hyrax 3.6.0 to hide broken flash messages on the dashboard edit work pages %> + + + + + <%= render partial: 'layouts/head_tag_content' %> + <%= content_for(:head) %> + + + +
    + <%= link_to "Skip to Content", "#skip-to-content" %> +
    + <%= render '/masthead' %> + <%= content_for(:navbar) %> +
    + +
    + <%# OVERRIDE here to hide broken flash messages on the dashboard edit work pages. %> + <%# regex includes only the paths for works since this is the only known place flash messages are broken. %> + <%= render '/flash_msg' %> + <%= render_breadcrumbs builder: Hyrax::BootstrapBreadcrumbsBuilder %> + <% if content_for?(:page_header) %> +
    +
    + <%= yield(:page_header) %> +
    +
    + <% end %> + + + <%= content_for?(:content) ? yield(:content) : yield %> + +
    + +
    + <%= render 'shared/ajax_modal' %> + + \ No newline at end of file diff --git a/app/views/themes/cultural_show/hyrax/base/show.html.erb b/app/views/themes/cultural_show/hyrax/base/show.html.erb index 205aaa468f..8c9133f902 100644 --- a/app/views/themes/cultural_show/hyrax/base/show.html.erb +++ b/app/views/themes/cultural_show/hyrax/base/show.html.erb @@ -14,20 +14,24 @@
    <%= render "show_actions", presenter: @presenter %>
    - <% if @presenter.iiif_viewer? %> -
    - <%= render 'representative_media', presenter: @presenter, viewer: true %> -
    - <% elsif @presenter.show_pdf_viewer? %> -
    - <%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %> -
    + <% if @presenter.video_embed_viewer? %> + <%= render 'video_embed_viewer', presenter: @presenter %> <% else %> -
    - <%= render 'representative_media', presenter: @presenter, viewer: false %> -
    + <% if @presenter.iiif_viewer? %> +
    + <%= render 'representative_media', presenter: @presenter, viewer: true %> +
    + <% elsif @presenter.show_pdf_viewer? %> +
    + <%= render 'pdf_js', file_set_presenter: pdf_file_set_presenter(@presenter) %> +
    + <% else %> +
    + <%= render 'representative_media', presenter: @presenter, viewer: false %> +
    + <% end %> <% end %> -
    +
    <%= render 'work_description', presenter: @presenter %> <%= render 'metadata', presenter: @presenter %>
    @@ -43,8 +47,12 @@
    <%= render 'items', presenter: @presenter %>
    + + <% @presenter.member_of_collection_ids.each do |collection_id| %> + + <% end %>
    -
    +
    \ No newline at end of file diff --git a/app/views/themes/scholarly_show/hyrax/base/show.html.erb b/app/views/themes/scholarly_show/hyrax/base/show.html.erb index 47b220b1c8..e75e220fe4 100644 --- a/app/views/themes/scholarly_show/hyrax/base/show.html.erb +++ b/app/views/themes/scholarly_show/hyrax/base/show.html.erb @@ -16,8 +16,15 @@
    <%= render 'workflow_actions_widget', presenter: @presenter %> - <% if @presenter.iiif_viewer? %> + <% if @presenter.video_embed_viewer? %> + <%= render 'video_embed_viewer', presenter: @presenter %>
    +
    + <%= render 'work_description', presenter: @presenter %> +
    +
    + <% elsif @presenter.iiif_viewer? %> +
    <%= render 'representative_media', presenter: @presenter, viewer: true %>
    @@ -60,5 +67,9 @@
    + + <% @presenter.member_of_collection_ids.each do |collection_id| %> + + <% end %>
    diff --git a/config/features.rb b/config/features.rb index 6405624b75..5680699195 100644 --- a/config/features.rb +++ b/config/features.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true Flipflop.configure do + feature :show_workflow_roles_menu_item_in_admin_dashboard_sidebar, + default: false, + description: "Shows the Workflow Roles menu item in the admin dashboard sidebar." + feature :show_featured_researcher, default: true, description: "Shows the Featured Researcher tab on the homepage." @@ -24,6 +28,9 @@ # Flipflop.default_pdf_viewer? returning `true` means we use PDF.js and `false` means we use IIIF Print. feature :default_pdf_viewer, default: true, - description: "Choose PDF.js or Universal Viewer to render PDFs. UV uses IIIF Print and requires PDF spltting with OCR. Switching from PDF.js to the UV may require re-ingesting of the PDF." + description: "Choose PDF.js or Universal Viewer to render PDFs. UV uses IIIF Print and requires PDF splitting with OCR. Switching from PDF.js to the UV may require re-ingesting of the PDF." + feature :show_login_link, + default: true, + description: "Show General Login Link at Top Right of Page." end diff --git a/config/initializers/bulkrax.rb b/config/initializers/bulkrax.rb index 1f2d38b283..768d4bf080 100644 --- a/config/initializers/bulkrax.rb +++ b/config/initializers/bulkrax.rb @@ -111,5 +111,10 @@ # config.reserved_properties += ['my_field'] end + # Sidebar for hyrax 3+ support + if Object.const_defined?(:Hyrax) && ::Hyrax::DashboardController&.respond_to?(:sidebar_partials) + Hyrax::DashboardController.sidebar_partials[:repository_content] << "hyrax/dashboard/sidebar/bulkrax_sidebar_additions" + end + Bulkrax::CreateRelationshipsJob.update_child_records_works_file_sets = true end diff --git a/config/routes.rb b/config/routes.rb index a960e3203d..82a22d600d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,8 +12,8 @@ mount Hyrax::IiifAv::Engine, at: '/' mount Riiif::Engine => 'images', as: :riiif if Hyrax.config.iiif_image_server? - authenticate :user, ->(u) { u.is_superadmin || u.is_admin } do - mount Sidekiq::Web => '/jobs' + authenticate :user, ->(u) { u.is_superadmin } do + mount Sidekiq::Web => '/sidekiq' end if ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_MULTITENANT', false)) @@ -27,7 +27,11 @@ namespace :proprietor do resources :accounts - resources :users + resources :users do + member do + post :become + end + end end end end @@ -35,6 +39,7 @@ get 'status', to: 'status#index' mount BrowseEverything::Engine => '/browse' + resource :site, only: [:update] do resources :roles, only: %i[index update] resource :labels, only: %i[edit update] @@ -70,6 +75,7 @@ mount Qa::Engine => '/authorities' mount Blacklight::Engine => '/' + mount Hyrax::Engine, at: '/' mount Bulkrax::Engine, at: '/' if ENV.fetch('HYKU_BULKRAX_ENABLED', 'true') == 'true' @@ -102,6 +108,7 @@ resource :work_types, only: %i[edit update] resources :users, only: [:index, :destroy] do post 'activate', on: :member + delete 'remove_role/:role_id', on: :member, to: 'users#remove_role', as: :remove_role end resources :groups do member do @@ -111,6 +118,8 @@ resources :users, only: %i[index create destroy], param: :user_id, controller: 'group_users' resources :roles, only: %i[index create destroy], param: :role_id, controller: 'group_roles' end + post "roles_service/:job_name_key", to: "roles_service#update_roles", as: :update_roles + get "roles_service", to: "roles_service#index", as: :roles_service_jobs end # OVERRIDE here to add featured collection routes diff --git a/config/tinymce.yml b/config/tinymce.yml index ed2b4c0012..8e23ebf1ca 100644 --- a/config/tinymce.yml +++ b/config/tinymce.yml @@ -3,11 +3,15 @@ default: &default content_block: <<: *default menubar: false - toolbar1: styleselect | bold italic | undo redo - toolbar2: table | fullscreen | image + image_uploadtab: true + toolbar1: styleselect fontselect fontsizeselect | backcolor forecolor | bold italic underline + toolbar2: indent outdent | alignleft aligncenter alignright | table | fullscreen | link hr image | undo redo | code plugins: - table - fullscreen + - link + - autolink - image + - code custom: <<: *default diff --git a/spec/controllers/admin/roles_service_controller_spec.rb b/spec/controllers/admin/roles_service_controller_spec.rb new file mode 100644 index 0000000000..0cb2b98b8a --- /dev/null +++ b/spec/controllers/admin/roles_service_controller_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe Admin::RolesServiceController, type: :controller do + context 'as an anonymous user' do + describe 'GET #index' do + subject { get :index } + + it { is_expected.to redirect_to new_user_session_path } + end + end + + context 'as an admin user' do + before { sign_in create(:admin) } + + describe 'GET #index' do + subject { get :index } + + it { is_expected.to render_template('layouts/hyrax/dashboard') } + it { is_expected.to render_template('admin/roles_service/index') } + end + end + + context 'as an admin user' do + before { sign_in create(:admin) } + + describe 'GET #index' do + subject { get :index } + + it { is_expected.to render_template('layouts/hyrax/dashboard') } + it { is_expected.to render_template('admin/roles_service/index') } + end + + describe 'POST #update_roles' do + it 'submits a job when it receives a valid job name' do + expect(RolesService::CreateCollectionAccessesJob).to receive(:perform_later) + post :update_roles, params: { job_name_key: :create_collection_accesses } + end + end + end +end diff --git a/spec/controllers/hyrax/my/collections_controller_spec.rb b/spec/controllers/hyrax/my/collections_controller_spec.rb new file mode 100644 index 0000000000..370b7f453f --- /dev/null +++ b/spec/controllers/hyrax/my/collections_controller_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe Hyrax::My::CollectionsController, type: :controller do + describe "#configure_facets" do + subject { controller.blacklight_config.sort_fields.keys } + + let(:expected_sort_fields) do + [ + "system_modified_dtsi desc", + "system_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/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/admin_dashboard_spec.rb b/spec/features/admin_dashboard_spec.rb index 3eef1f164a..1ad7b2533b 100644 --- a/spec/features/admin_dashboard_spec.rb +++ b/spec/features/admin_dashboard_spec.rb @@ -43,7 +43,7 @@ expect(page).to have_link('Content Blocks') expect(page).to have_link('Features') expect(page).to have_link('Available Work Types') - expect(page).to have_link('Workflow Roles') + # expect(page).to have_link('Workflow Roles') end end diff --git a/spec/features/manage_user_groups_and_roles_spec.rb b/spec/features/manage_user_groups_and_roles_spec.rb index e69ae89525..d6aeb8e0bc 100644 --- a/spec/features/manage_user_groups_and_roles_spec.rb +++ b/spec/features/manage_user_groups_and_roles_spec.rb @@ -36,10 +36,11 @@ it "lists each user's associated direct and inherited roles" do expect(page).to have_content('Manage Users') - expect(page).to have_css 'th', text: 'Roles' - expect(find("tr##{admin.email.parameterize} td.roles")).to have_text(admin_role.name.titlecase) - expect(find("tr##{user.email.parameterize} td.roles")).to have_text(user_manager_role.name.titlecase) - expect(find("tr##{user.email.parameterize} td.roles")).to have_text(collection_manager_role.name.titlecase) + expect(page).to have_css 'th', text: 'Group roles' + expect(page).to have_css 'th', text: 'Site roles' + expect(find("tr##{admin.email.parameterize} td.group-roles")).to have_text(admin_role.name.titlecase) + expect(find("tr##{user.email.parameterize} td.group-roles")).to have_text(user_manager_role.name.titlecase) + expect(find("tr##{user.email.parameterize} td.site-roles")).to have_text(collection_manager_role.name.titlecase) end it 'can visit Manage Users and invite users with the admin role' do diff --git a/spec/helpers/blacklight/advanced_search_helper_spec.rb b/spec/helpers/blacklight/advanced_search_helper_spec.rb deleted file mode 100644 index 025c3613b5..0000000000 --- a/spec/helpers/blacklight/advanced_search_helper_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Blacklight::AdvancedSearchHelper do - include described_class - - let(:search_fields_for_advanced_search) do - CatalogController.blacklight_config.search_fields.select do |_k, v| - v.include_in_advanced_search || v.include_in_advanced_search.nil? - end - end - - describe "#primary_search_fields" do - it "returns the first 6 advanced search fields" do - expect(primary_search_fields_for(search_fields_for_advanced_search).size).to eq(6) - end - end - - describe "#secondary_search_fields" do - it "returns the rest of the advanced search fields" do - second_search_fields_count = search_fields_for_advanced_search.size - 6 - - expect(secondary_search_fields_for(search_fields_for_advanced_search).size).to eq(second_search_fields_count) - end - end - - describe "#local_authority?" do - context 'when key is valid' do - it "checks if the key exists in QA::Authorities::Local.names" do - valid_local_authorities = search_fields_for_advanced_search.select { |key, _value| local_authority?(key) } - - valid_local_authorities.each_key do |key| - expect(local_authority?(key)).to be true - end - end - end - - context 'when the key is invalid' do - it "checks if the key exists in QA::Authorities::Local.names" do - invalid_local_authorities = search_fields_for_advanced_search.reject { |key, _value| local_authority?(key) } - - invalid_local_authorities.each_key do |key| - expect(local_authority?(key)).to be false - end - end - end - end - - describe "#options_for_qa_select?" do - it "returns the values for a given key" do - valid_local_authority_keys = search_fields_for_advanced_search.select { |key, _value| local_authority?(key) }.keys - - valid_local_authority_keys.each do |key| - local_authority_terms = fetch_local_yml(key)['terms'].map { |term| [term['term'], term['id']] } - - expect(options_for_qa_select(key)).to eq(local_authority_terms) - end - end - end - - # a utility method for finding the correct yml files because the naming is inconsistent, plural vs singular - def fetch_local_yml(key) - YAML.load_file("config/authorities/#{key}.yml") - rescue Errno::ENOENT - YAML.load_file("config/authorities/#{key.pluralize}.yml") - end - - describe "#fetch_service_for" do - it "returns the service class for a given a valid key" do - expect(fetch_service_for('rights_statement')).to eq(Hyrax::RightsStatementService) - end - - it "returns nil for an invalid key" do - expect(fetch_service_for('foo')).to be_nil - end - end -end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index ca8a439cee..aa3265943e 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -91,6 +91,24 @@ it { is_expected.to be_able_to(:manage, :all) } end + describe 'a user_manager user' do + let(:user) { FactoryBot.create(:user) } + let(:ordinary_role) { FactoryBot.create(:role, name: 'ordinary_role') } + + before do + user.add_role :user_manager, Site.instance + end + + context 'when managing User and Role' do + it 'can create, read, update, and edit User and Role' do + expect(ability).to be_able_to(:create, User.new) + expect(ability).to be_able_to(:read, User.new) + expect(ability).to be_able_to(:update, User.new) + expect(ability).to be_able_to(:edit, User.new) + end + end + end + # Brought over from blacklight-access_controls v0.6.2 describe '#user_groups' do subject { ability.user_groups } diff --git a/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb b/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb new file mode 100644 index 0000000000..1ede624a5f --- /dev/null +++ b/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Hyrax::IiifAv::DisplaysContentDecorator do + # We're prepending the DisplaysContentDecorator to the Hyrax::IiifAv::DisplaysContent + describe Hyrax::IiifAv::DisplaysContent do + describe '.public_instance_methods' do + subject { Hyrax::IiifAv::DisplaysContent.public_instance_methods } + + it { is_expected.to include(:solr_document) } + it { is_expected.to include(:current_ability) } + end + end +end diff --git a/spec/routing/admin/roles_service_routing_spec.rb b/spec/routing/admin/roles_service_routing_spec.rb new file mode 100644 index 0000000000..d0c0698654 --- /dev/null +++ b/spec/routing/admin/roles_service_routing_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe Admin::RolesServiceController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/admin/roles_service").to route_to("admin/roles_service#index") + end + + it "routes to #update_roles via POST" do + expect(post: "/admin/roles_service/create_collection_accesses") + .to route_to("admin/roles_service#update_roles", job_name_key: 'create_collection_accesses') + end + end +end diff --git a/spec/views/hyrax/admin/users/index.html.erb_spec.rb b/spec/views/hyrax/admin/users/index.html.erb_spec.rb index 14db24a447..e9e15197a4 100644 --- a/spec/views/hyrax/admin/users/index.html.erb_spec.rb +++ b/spec/views/hyrax/admin/users/index.html.erb_spec.rb @@ -68,7 +68,9 @@ (5..6).each do |i| expect(page).to have_content("admin#{i}@example.com") end - expect(page).to have_selector("div.users-listing td.roles li", text: 'Admin', count: 4) + + expect(page).to have_selector("div.users-listing td.site-roles li", text: 'Admin', count: 2) + expect(page).to have_selector("div.users-listing td.group-roles li", text: 'Admin', count: 2) end end From 016669295c82b158a991d18a03533604e29a1af6 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Tue, 19 Dec 2023 12:47:42 -0500 Subject: [PATCH 03/29] Updating translations --- config/locales/blacklight.de.yml | 1 + config/locales/blacklight.en.yml | 1 + config/locales/blacklight.es.yml | 1 + config/locales/blacklight.fr.yml | 1 + config/locales/blacklight.it.yml | 1 + config/locales/blacklight.pt-BR.yml | 1 + config/locales/blacklight.zh.yml | 1 + config/locales/de.yml | 17 ++++++- config/locales/devise_invitable.en.yml | 2 +- config/locales/en.yml | 44 ++++++++++++++++- config/locales/es.yml | 18 +++++-- config/locales/fr.yml | 17 ++++++- config/locales/hyrax.de.yml | 16 +++++-- config/locales/hyrax.en.yml | 19 +++++--- config/locales/hyrax.es.yml | 16 +++++-- config/locales/hyrax.fr.yml | 9 +++- config/locales/hyrax.it.yml | 13 ++++-- config/locales/hyrax.pt-BR.yml | 9 ++-- config/locales/hyrax.zh.yml | 11 +++-- config/locales/it.yml | 18 +++++-- config/locales/pt-BR.yml | 18 +++++-- config/locales/simple_form.de.yml | 65 +++++++++----------------- config/locales/simple_form.en.yml | 3 +- config/locales/simple_form.es.yml | 48 +++++++++---------- config/locales/simple_form.fr.yml | 59 +++++++++-------------- config/locales/simple_form.it.yml | 59 +++++++++-------------- config/locales/simple_form.pt-BR.yml | 61 +++++++++--------------- config/locales/simple_form.zh.yml | 63 ++++++++++--------------- config/locales/zh.yml | 18 +++++-- 29 files changed, 343 insertions(+), 267 deletions(-) diff --git a/config/locales/blacklight.de.yml b/config/locales/blacklight.de.yml index 307055e883..8a40282c2f 100644 --- a/config/locales/blacklight.de.yml +++ b/config/locales/blacklight.de.yml @@ -47,3 +47,4 @@ de: rights_tesim: Rechte subject_tesim: Fach title_tesim: Titel + start_over: Neue Suche diff --git a/config/locales/blacklight.en.yml b/config/locales/blacklight.en.yml index e5471aaf12..7fce013d26 100644 --- a/config/locales/blacklight.en.yml +++ b/config/locales/blacklight.en.yml @@ -46,3 +46,4 @@ en: rights_tesim: Rights subject_tesim: Subject title_tesim: Title + start_over: New Search diff --git a/config/locales/blacklight.es.yml b/config/locales/blacklight.es.yml index d276a26eb5..4142fb7657 100644 --- a/config/locales/blacklight.es.yml +++ b/config/locales/blacklight.es.yml @@ -47,3 +47,4 @@ es: rights_tesim: Derechos subject_tesim: Tema title_tesim: Título + start_over: Nueva búsqueda diff --git a/config/locales/blacklight.fr.yml b/config/locales/blacklight.fr.yml index d1f107f746..a987fcd716 100644 --- a/config/locales/blacklight.fr.yml +++ b/config/locales/blacklight.fr.yml @@ -47,3 +47,4 @@ fr: rights_tesim: Droits subject_tesim: Assujettir title_tesim: Titre + start_over: Nouvelle recherche diff --git a/config/locales/blacklight.it.yml b/config/locales/blacklight.it.yml index d6f885987a..7b2cafec6b 100644 --- a/config/locales/blacklight.it.yml +++ b/config/locales/blacklight.it.yml @@ -47,3 +47,4 @@ it: rights_tesim: Diritti subject_tesim: Soggetto title_tesim: Titolo + start_over: Nuova ricerca diff --git a/config/locales/blacklight.pt-BR.yml b/config/locales/blacklight.pt-BR.yml index 689970a786..2648b6a6ba 100644 --- a/config/locales/blacklight.pt-BR.yml +++ b/config/locales/blacklight.pt-BR.yml @@ -47,3 +47,4 @@ pt-BR: rights_tesim: Direitos subject_tesim: Sujeito title_tesim: Título + start_over: Nova pesquisa diff --git a/config/locales/blacklight.zh.yml b/config/locales/blacklight.zh.yml index 4432849008..81842f4b38 100644 --- a/config/locales/blacklight.zh.yml +++ b/config/locales/blacklight.zh.yml @@ -47,3 +47,4 @@ zh: rights_tesim: 权 subject_tesim: 学科 title_tesim: 标题 + start_over: 新搜索 diff --git a/config/locales/de.yml b/config/locales/de.yml index 55a462a333..e86a331c19 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,5 +1,12 @@ --- de: + errors: + messages: + valid_embed_url: "muss eine gültige YouTube- oder Vimeo-Einbettungs-URL sein." + activefedora: + models: + generic_work: Arbeiten + image: Bild activerecord: attributes: site: @@ -7,6 +14,8 @@ de: application: tagline: Die Next-Generation Repository-Lösung helpers: + action: + become: Werden submit: add_role_to_group: submit: Hinzufügen @@ -162,6 +171,8 @@ de: forms: banner_image: hint: Um ein Bild als Masthead-Hintergrund zu verwenden, sollten Sie ein Bild (JPG, GIF oder PNG) verwenden, das mindestens 120 Pixel hoch und 1200 Pixel breit ist. Verwenden Sie für optimale Ergebnisse ein Bild mit einer Breite von mindestens 1800 Pixel. + collection_banner_text_color: + hint: Die Farbe für den Text (Titel und Datum der letzten Aktualisierung) im Sammlungsbanner. custom_css: confirm: Benutzerdefiniertes CSS überschreibt immer andere Themenauswahlen. Vorgehen? warning: Wenn Ihre Themenanpassungen nicht korrekt angewendet werden, stellen Sie sicher, dass sie nicht von benutzerdefiniertem CSS überschrieben werden. @@ -171,7 +182,7 @@ de: alert: Bitte laden Sie vor dem Absenden mindestens eine Datei hoch. hint: Für Standardbilder sollten Sie ein Bild (JPG, GIF oder PNG) verwenden, das die gleichen Abmessungen für Höhe und Breite aufweist (100 Pixel breit und 100 Pixel hoch). directory_image: - hint: Um ein Bild als Verzeichnisbild zu verwenden, sollten Sie ein Bild (JPG, GIF oder PNG) verwenden, das nicht höher als der Header und nicht breiter als 400 Pixel ist. + hint: Das Verzeichnisbild sollte ein Bild (JPG, GIF oder PNG) verwenden. Die Breite des Bildes sollte nicht größer als die doppelte Höhe sein. facet_panel_background_color: hint: Gilt für Facetten und zusätzliche Abschnittsüberschriften auf den Arbeitsseiten einiger Themen. facet_panel_text_color: @@ -208,7 +219,6 @@ de: fonts: Schriftarten themes: Themen sidebar: - account: Konto accounts: Konten activity_summary: Aktivitätsübersicht content_blocks: Inhaltsblöcke @@ -237,6 +247,9 @@ de: active: Aktiv pending: noch ausstehend status_label: Status + roles: + remove: + confirmation: Sind Sie sicher, dass Sie die Rolle „%{role}“ vom Benutzer „%{user}“ entfernen möchten? permissions: collections: cannot: diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml index 61260a473e..a7e17d640d 100644 --- a/config/locales/devise_invitable.en.yml +++ b/config/locales/devise_invitable.en.yml @@ -22,7 +22,7 @@ en: accept_until: This invitation will be due in %{due_date}. hello: Hello %{email} ignore: |- - If you don't want to accept the invitation, please ignore this email. + If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password. someone_invited_you: Someone has invited you to %{url}, you can accept it through the link below. subject: Invitation instructions diff --git a/config/locales/en.yml b/config/locales/en.yml index 44e353a89b..47f33071a1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,8 @@ --- en: + errors: + messages: + valid_embed_url: "must be a valid YouTube or Vimeo Embed URL." single_signon: index: sign_in_with_provider: Sign in with %{provider} @@ -9,7 +12,18 @@ en: institution_name_full: Full institution name application: tagline: The next-generation repository solution + activefedora: + models: + generic_work: "Work" + image: "Image" + devise: + mailer: + invitation_instructions: + ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password." + helpers: + action: + become: Become submit: add_role_to_group: submit: Add @@ -199,6 +213,8 @@ en: hint: 'Applies to facets and additional section headers on the work pages in some themes.' facet_panel_text_color: hint: 'Also applies to the color of the caret next to the text and the work title on some themes.' + collection_banner_text_color: + hint: 'The color for the text (title, and last updated date) inside of the collection banner.' tabs: banner_image: "Banner Image" directory_image: "Directory Image" @@ -208,14 +224,35 @@ en: favicon: Favicon fonts: "Fonts" themes: "Themes" + roles_service_jobs: + header: Data Repair Jobs + jobs: + create_admin_set_accesses: + label: Create Admin Set Accesses + description: "Creating a Hyrax::PermissionTemplateAccess record (combined with Ability#user_groups) + will allow Works in all AdminSets to show up in Blacklight / Solr queries." + create_collection_accesses: + label: Create Collection Accesses + description: "Because each collection role has some level of access to every Collection within a tenant, + creating a Hyrax::PermissionTemplateAccess record (combined with Ability#user_groups) + means all Collections will show up in Blacklight / Solr queries." + create_collection_type_participants: + label: Create Collection Type Participants + description: "Because some of the collection roles have access to every Collection within a tenant, create a + Hyrax::CollectionTypeParticipant record for them on every Hyrax::CollectionType (except the AdminSet)" + grant_workflow_roles_for_all_admin_sets: + label: Admin Set Workflow Roles + description: "Permissions to deposit Works are controlled by Workflow Roles on individual AdminSets. + In order for Hyrax::Group and User records who have either the 'Work Editor' or 'Work Depositor' Role + to have the correct permissions for Works, we grant them Workflow Roles for all AdminSets. + NOTE: All AdminSets must have a permission template or this will fail. Run 'Create Admin Set Accesses' first." sidebar: - account: Account accounts: Accounts activity_summary: Activity Summary labels: Labels manage_groups: Manage Groups - repository_activity: Repository Activity system_status: System Status + roles_service_jobs: Data Repair users: activate: confirmation: Are you sure you want to activate the user "%{user}"? @@ -234,6 +271,9 @@ en: active: Active pending: Pending status_label: Status + roles: + remove: + confirmation: Are you sure you want to remove the role "%{role}" from the user "%{user}"? permissions: collections: cannot: diff --git a/config/locales/es.yml b/config/locales/es.yml index 6137a7659b..290ec77523 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,5 +1,12 @@ --- es: + errors: + messages: + valid_embed_url: "debe ser una URL de incrustación válida de YouTube o Vimeo." + activefedora: + models: + generic_work: Trabajar + image: Imagen activerecord: attributes: site: @@ -7,6 +14,8 @@ es: application: tagline: La solución de repositorio de próxima generación helpers: + action: + become: Volverse submit: add_role_to_group: submit: Añadir @@ -163,6 +172,8 @@ es: forms: banner_image: hint: Para usar una imagen como fondo de cabecera, debe usar una imagen (JPG, GIF o PNG) que tenga al menos 120 píxeles de alto y 1200 píxeles de ancho. Para obtener mejores resultados, use una imagen de al menos 1800 píxeles de ancho. + collection_banner_text_color: + hint: El color del texto (título y fecha de última actualización) dentro del banner de la colección. custom_css: confirm: CSS personalizado siempre anulará otras selecciones de temas. ¿Continuar? warning: Si sus personalizaciones de temas no parecen aplicarse correctamente, verifique que CSS personalizado no las sobrescriba. @@ -172,7 +183,7 @@ es: alert: Cargue al menos un archivo antes de enviarlo. hint: Para las imágenes predeterminadas, debe usar una imagen (JPG, GIF o PNG) que tenga las mismas dimensiones de alto y ancho (100 píxeles de ancho y 100 píxeles de alto) directory_image: - hint: Para usar una imagen como imagen de directorio, debe usar una imagen (JPG, GIF o PNG) que no sea más alta que el encabezado ni más ancha que 400 píxeles de ancho. + hint: La imagen del directorio debe usar una imagen (JPG, GIF o PNG). El ancho de la imagen no debe ser más ancho que el doble de la altura. facet_panel_background_color: hint: Se aplica a facetas y encabezados de sección adicionales en las páginas de trabajo en algunos temas. facet_panel_text_color: @@ -209,12 +220,10 @@ es: fonts: Fuentes themes: Temas sidebar: - account: Cuenta accounts: Cuentas activity_summary: Resumen de la actividad labels: Etiquetas manage_groups: Administrar Grupos - repository_activity: Actividad del repositorio system_status: Estado del sistema users: activate: @@ -234,6 +243,9 @@ es: active: Activo pending: Pendiente status_label: Estado + roles: + remove: + confirmation: ¿Está seguro de que desea eliminar el rol "%{role}" del usuario "%{user}"? permissions: collections: cannot: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0a732ce65a..8517263d7e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1,5 +1,12 @@ --- fr: + errors: + messages: + valid_embed_url: "doit être une URL d'incorporation valide de YouTube ou Vimeo." + activefedora: + models: + generic_work: Travail + image: Image activerecord: attributes: site: @@ -7,6 +14,8 @@ fr: application: tagline: La solution de dépôt de nouvelle génération helpers: + action: + become: Devenir submit: add_role_to_group: submit: Ajouter @@ -163,6 +172,8 @@ fr: forms: banner_image: hint: Pour utiliser une image comme arrière-plan d'une bannière Masthead, vous devez utiliser une image (JPG, GIF ou PNG) d'au moins 120 pixels de haut et 1 200 pixels de large. Pour de meilleurs résultats, utilisez une image d'au moins 1800 pixels de large. + collection_banner_text_color: + hint: La couleur du texte (titre et date de la dernière mise à jour) à l'intérieur de la bannière de collection. custom_css: confirm: Le CSS personnalisé remplacera toujours les autres sélections de thèmes. Procéder? warning: Si vos personnalisations de thème ne semblent pas s'appliquer correctement, vérifiez qu'elles ne sont pas écrasées par CSS personnalisé. @@ -172,7 +183,7 @@ fr: alert: Veuillez télécharger au moins un fichier avant de soumettre. hint: Pour les images par défaut, vous devez utiliser une image (JPG, GIF ou PNG) qui a des dimensions de hauteur et de largeur égales (100 pixels de large et 100 pixels de haut) directory_image: - hint: Pour utiliser une image comme image de répertoire, vous devez utiliser une image (JPG, GIF ou PNG) qui ne dépasse pas l'en-tête et ne dépasse pas 400 pixels de large. + hint: L'image du répertoire doit utiliser une image (JPG, GIF ou PNG). La largeur de l'image ne doit pas être plus large que 2x la hauteur. facet_panel_background_color: hint: S'applique aux facettes et aux en-têtes de section supplémentaires sur les pages de travail dans certains thèmes. facet_panel_text_color: @@ -209,7 +220,6 @@ fr: fonts: Les Polices themes: Thèmes sidebar: - account: Compte accounts: Comptes activity_summary: Résumé de l'activité content_blocks: Blocs de contenu @@ -239,6 +249,9 @@ fr: active: actif pending: en attendant status_label: Statut + roles: + remove: + confirmation: Êtes-vous sûr de vouloir supprimer le rôle « %{role} » de l'utilisateur « %{user} » ? permissions: collections: cannot: diff --git a/config/locales/hyrax.de.yml b/config/locales/hyrax.de.yml index 34aecc98cd..9f157430ab 100644 --- a/config/locales/hyrax.de.yml +++ b/config/locales/hyrax.de.yml @@ -243,9 +243,11 @@ de: one: In diesem Repository befindet sich ein %{count}-Benutzer . other: In diesem Repository befinden sich %{count}-Benutzer . group_label: Gruppen + group_role_label: Gruppenrollen id_label: Nutzername reader_title: Benutzer anzeigen role_label: Rollen + site_role_label: Site-Rollen title: Benutzer verwalten workflow_roles: header: Workflow-Rollen @@ -295,7 +297,7 @@ de: background_attribution_html: '' base: citations: - header: 'Zitate' + header: Zitate form_child_work_relationships: actions: remove: Aus dieser Arbeit entfernen @@ -696,7 +698,6 @@ de: sr: batch_checkbox: Aktivieren Sie diese Option, um sie zu einer Sammlung oder Bearbeitungsliste hinzuzufügen check_all_label: Wählen Sie alle Dateien aus, die einer Sammlung hinzugefügt oder bearbeitet werden sollen - collections_batch_checkbox: Aktivieren Sie diese Option, um Sammlungen stapelweise zu löschen. detail_label: Zeigen Sie zusammenfassende Details von an listing: Auflistung der Artikel, in denen Sie hinterlegt haben press_to: drücke um zu @@ -1098,7 +1099,7 @@ de: description: Eine kurze übergreifende Beschreibung, die für alle in diesem Set gesammelten Werke gilt. Zum Beispiel "Thesen und ergänzende Dateien, die von Absolventen der School of Earth Sciences erstellt wurden." title: Ein Name, der die Identifizierung des Verwaltungssatzes erleichtert und ihn von anderen Verwaltungssätzen im Repository unterscheidet. collection: - based_near: Ein Ortsname, der sich auf die Sammlung bezieht, z. B. der Veröffentlichungsort oder die Stadt, das Bundesland oder das Land, um das es in der Sammlung geht. Ruft den GeoNames-Webdienst auf. + based_near: Ein Ortsname, der sich auf das Werk bezieht, z. B. der Ort der Veröffentlichung oder die Stadt, das Bundesland oder das Land, um das es in den Inhalten des Werks geht. Ruft die Geonames-Webdienstseite.
    2. Gehen Sie zu Ihren Geonames-Kontoeinstellungen und wählen Sie unten die Option aus, um kostenlose Dienste zu aktivieren.
    3. Gehen Sie im Dashboard
    zu den Einstellungen Ihres Hyku-Kontos. Geben Sie Ihren Geonames-Benutzernamen in den Hyrax-Kontoeinstellungen ein.
    5. Sie sollten nun die Liste der Standorte in diesem Dropdown-Menü sehen können. contributor: Eine Person oder Gruppe, die Sie als eine Rolle bei der Erstellung der Sammlung erkennen möchten, jedoch nicht als primäre Rolle. creator: Die Person oder Gruppe, die für die Sammlung verantwortlich ist. Normalerweise ist dies der Autor des Inhalts. Persönliche Namen sollten mit dem Nachnamen zuerst eingegeben werden, z. "Smith, John". date_created: Das Datum, an dem die Sammlung erstellt wurde. @@ -1125,7 +1126,7 @@ de: share_applies_to_new_works: Wenn neue Werke direkt in der Sammlung erstellt werden, erteilen Sie Benutzern und Gruppen Berechtigungen für das neue Werk entsprechend ihrer Sammlungsrollen. title: '' defaults: - based_near: Ein Ortsname, der sich auf das Werk bezieht, z. B. der Veröffentlichungsort oder die Stadt, das Bundesland oder das Land, um das es in dem Arbeitsinhalt geht. Ruft den GeoNames-Webdienst auf. + based_near: Ein Ortsname, der sich auf das Werk bezieht, z. B. der Ort der Veröffentlichung oder die Stadt, das Bundesland oder das Land, um das es in den Inhalten des Werks geht. Ruft die Geonames-Webdienstseite.
    2. Gehen Sie zu Ihren Geonames-Kontoeinstellungen und wählen Sie unten die Option aus, um kostenlose Dienste zu aktivieren.
    3. Gehen Sie im Dashboard
    zu den Einstellungen Ihres Hyku-Kontos. Geben Sie Ihren Geonames-Benutzernamen in den Hyrax-Kontoeinstellungen ein.
    5. Sie sollten nun die Liste der Standorte in diesem Dropdown-Menü sehen können. contributor: Eine Person oder Gruppe, die Sie als eine Rolle bei der Erstellung der Arbeit erkennen möchten, jedoch nicht die primäre Rolle. creator: Die Person oder Gruppe, die für die Arbeit verantwortlich ist. Normalerweise ist dies der Autor des Inhalts. Persönliche Namen sollten mit dem Nachnamen zuerst eingegeben werden, z. "Smith, John". date_created: Das Datum, an dem die Arbeit erstellt wurde. @@ -1140,6 +1141,7 @@ de: resource_type: Vordefinierte Kategorien zur Beschreibung der Art des hochgeladenen Inhalts, z. B. "Artikel". oder "Datensatz". Es kann mehr als ein Typ ausgewählt werden. subject: Überschriften oder Indexbegriffe, die beschreiben, worum es in der Arbeit geht; Diese müssen einem vorhandenen Wortschatz entsprechen. title: Ein Name, der bei der Identifizierung eines Werks hilft. + video_embed: Wenn Sie einen Einbettungslink für ein Video eingeben, muss es sich um eine ordnungsgemäß formatierte URL handeln, die mit „http://“ oder „https://“ beginnt. Es muss außerdem einen gültigen Link zu einem gehosteten Video enthalten, das in einem Iframe angezeigt werden kann.

    Beispiele:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: Umfang labels: @@ -1162,12 +1164,13 @@ de: defaults: admin_set_id: Verwaltungsset based_near: Ort + contributor: Mitwirkender creator: Schöpfer date_created: Datum erstellt default_button_background_color: Standardhintergrundfarbe der Schaltfläche default_button_border_color: Standardfarbe des Schaltflächenrahmens default_button_text_color: Standardfarbe für den Schaltflächentext - description: Beschreibung + description: Zusammenfassung oder Zusammenfassung embargo_release_date: bis um facet_panel_background_color: Hintergrundfarbe des Facettenfelds facet_panel_text_color: Textfarbe des Facettenbereichs @@ -1176,6 +1179,7 @@ de: footer_link_hover_color: Hover-Farbe des Fußzeilen-Links header_and_footer_background_color: Hintergrundfarbe für Kopf- und Fußzeile header_and_footer_text_color: Textfarbe für Kopf- und Fußzeile + identifier: Kennung keyword: Stichwort lease_expiration_date: bis um license: Lizenz @@ -1187,8 +1191,10 @@ de: navbar_link_text_color: Textfarbe für Navigationsleisten-Links navbar_link_text_hover_color: Hover-Farbe des Navigationsleisten-Linktextes primary_button_hover_color: Hover-Farbe der primären Schaltfläche + publisher: Verleger related_url: Verwandte URL rights_statement: Rechteerklärung + source: Quelle title: Titel visibility_after_embargo: dann öffne es bis visibility_after_lease: dann beschränken Sie es auf diff --git a/config/locales/hyrax.en.yml b/config/locales/hyrax.en.yml index 9f703862a8..9776e7e98b 100644 --- a/config/locales/hyrax.en.yml +++ b/config/locales/hyrax.en.yml @@ -253,8 +253,9 @@ en: one: There is %{count} user in this repository. other: There are %{count} users in this repository. id_label: Username - role_label: Roles group_label: Groups + group_role_label: Group roles + site_role_label: Site roles title: Manage Users reader_title: View Users workflow_roles: @@ -436,7 +437,7 @@ en: select_label: Select subcollection nested_subcollection: button_label: Add to collection - desc: Add existing collections to this collection + desc: Add this collection to another collection modal_title: Add this Collection Within Another Collection select_label: Select collection browse_view: Browse View @@ -710,7 +711,6 @@ en: shared: Works Shared with Me sr: batch_checkbox: Check to add to a collection or edit list - collections_batch_checkbox: Check to batch delete collections. check_all_label: Select all files to be added to a collection or edited detail_label: Display summary details of listing: Listing of items you have deposited in @@ -1104,7 +1104,8 @@ en: description: A brief overarching description that applies to all works collected in this set. For example, "Theses and supplementary files created by the School of Earth Sciences graduate students." title: A name to aid in identifying the Administrative Set and to distinguish it from other Administrative Sets in the repository. collection: - based_near: A place name related to the collection, such as its site of publication, or the city, state, or country the collection contents are about. Calls upon the GeoNames web service. + based_near: "A place name related to the work, such as its site of publication, or the city, state, or country the work contents are about. Calls upon the GeoNames web service. Please note, you must configure your geonames username in the account settings, or this field will have a loading error.
    +
    Steps to enable this field:
    1. Create a free account on the Geonames web service page.
    2. Go to your geonames account settings and select the option at the bottom to enable free services.
    3. Go into your hyku account settings in the dashboard.
    4. Enter your geonames username in the hyrax account settings.
    5. You should now be able to see the list of locations in this dropdown." contributor: A person or group you want to recognize for playing a role in the creation of the collection, but not the primary role. creator: The person or group responsible for the collection. Usually this is the author of the content. Personal names should be entered with the last name first, e.g. "Smith, John.". date_created: The date on which the collection was created. @@ -1131,10 +1132,11 @@ en: share_applies_to_new_works: When new works are created directly in the collection, grant sharing users and groups permissions for the new work according to their collection roles. title: "" defaults: - based_near: A place name related to the work, such as its site of publication, or the city, state, or country the work contents are about. Calls upon the GeoNames web service. + based_near: "A place name related to the work, such as its site of publication, or the city, state, or country the work contents are about. Calls upon the GeoNames web service. Please note, you must configure your geonames username in the account settings, or this field will have a loading error.
    +
    Steps to enable this field:
    1. Create a free account on the Geonames web service page.
    2. Go to your geonames account settings and select the option at the bottom to enable free services.
    3. Go into your hyku account settings in the dashboard.
    4. Enter your geonames username in the hyrax account settings.
    5. You should now be able to see the list of locations in this dropdown." contributor: A person or group you want to recognize for playing a role in the creation of the work, but not the primary role. creator: The person or group responsible for the work. Usually this is the author of the content. Personal names should be entered with the last name first, e.g. "Smith, John.". - date_created: The date on which the work was created. + date_created: The date on which the work was created. Must adhere to either the format YYYY, YYYY-MM, or YYYY-MM-DD in order to be filtered and sorted. description: Free-text notes about the work. Examples include abstracts of a paper or citation information for a journal article. extent: The extent (size, duration, number, etc.) of the work. identifier: A unique handle identifying the work. An example would be a DOI for a journal article, or an ISBN or OCLC number for a book. @@ -1146,6 +1148,7 @@ en: resource_type: Pre-defined categories to describe the type of content being uploaded, such as "article" or "dataset." More than one type may be selected. subject: Headings or index terms describing what the work is about; these do need to conform to an existing vocabulary. title: A name to aid in identifying a work. + video_embed: "If you enter an embed link for a video, it must be a properly formatted url beginning with 'http://' or 'https://'. It also needs to contain a valid link to a hosted video that can appear in an iframe.

    Examples:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/Znf73dsFdC8
    " labels: collection: size: Size @@ -1166,6 +1169,7 @@ en: defaults: admin_set_id: Administrative Set based_near: Location + contributor: Contributor creator: Creator date_created: Date Created default_button_background_color: Default button background color @@ -1180,6 +1184,7 @@ en: footer_link_hover_color: Footer link hover color header_and_footer_background_color: Header and footer background color header_and_footer_text_color: Header and footer text color + identifier: Identifier keyword: Keyword lease_expiration_date: until license: License @@ -1191,8 +1196,10 @@ en: navbar_link_text_color: Navbar link text color navbar_link_text_hover_color: Navbar link text hover color primary_button_hover_color: Primary button hover color + publisher: Publisher related_url: Related URL rights_statement: Rights statement + source: Source title: Title visibility_after_embargo: then open it up to visibility_after_lease: then restrict it to diff --git a/config/locales/hyrax.es.yml b/config/locales/hyrax.es.yml index 88722366cf..c1135cb99e 100644 --- a/config/locales/hyrax.es.yml +++ b/config/locales/hyrax.es.yml @@ -244,9 +244,11 @@ es: one: Hay usuarios %{count} en este repositorio. other: Hay usuarios de %{count} en este repositorio. group_label: Grupos + group_role_label: Roles grupales id_label: Nombre de usuario reader_title: Ver usuarios role_label: Roles + site_role_label: Roles del sitio title: Administrar usuarios workflow_roles: header: Roles de flujo de trabajo @@ -296,7 +298,7 @@ es: background_attribution_html: '' base: citations: - header: 'Citación' + header: Citación form_child_work_relationships: actions: remove: Eliminar de este trabajo @@ -697,7 +699,6 @@ es: sr: batch_checkbox: Marque para agregar a una colección o editar lista check_all_label: Seleccione todos los archivos para agregar a una colección o editar - collections_batch_checkbox: Marque para eliminar colecciones por lotes. detail_label: Mostrar detalles de resumen de listing: Listado de artículos que ha depositado en press_to: Presione para @@ -1099,7 +1100,7 @@ es: description: Una breve descripción general que se aplica a todos los trabajos recopilados en este conjunto. Por ejemplo, "Tesis y archivos complementarios creados por los estudiantes graduados de la Facultad de Ciencias de la Tierra". title: Un nombre para ayudar a identificar el conjunto administrativo y distinguirlo de otros conjuntos administrativos en el repositorio. collection: - based_near: Un nombre de lugar relacionado con la colección, como su sitio de publicación, o la ciudad, el estado o el país del que trata el contenido de la colección. Llama al servicio web GeoNames . + based_near: El nombre de un lugar relacionado con la obra, como su sitio de publicación, o la ciudad, el estado o el país sobre el que trata el contenido de la obra. Llama a Servicio web de GeoNames. Tenga en cuenta que debe configurar su nombre de usuario de geonames en la configuración de la cuenta, o este campo tendrá un error de carga.

    Pasos para habilitar este campo:
    1. Cree una cuenta gratuita en la página del servicio web de Geonames.
    2. Vaya a su configuración de cuenta de geonames y seleccione la opción en la parte inferior para habilitar los servicios gratuitos.
    3. Vaya a la configuración de su cuenta hyku en el panel de control.
    4. Ingrese su nombre de usuario de geonames en la configuración de la cuenta hyrax.
    5. Ahora debería poder ver la lista de ubicaciones en este menú desplegable contributor: Una persona o grupo que desea reconocer por desempeñar un papel en la creación de la colección, pero no el rol principal. creator: La persona o grupo responsable de la colección. Por lo general, este es el autor del contenido. Los nombres personales deben ingresarse primero con el apellido, p. & quot; Smith, John & quot ;. date_created: La fecha en que se creó la colección. @@ -1126,7 +1127,7 @@ es: share_applies_to_new_works: Cuando se crean nuevos trabajos directamente en la colección, otorgue permisos para compartir usuarios y grupos para el nuevo trabajo de acuerdo con sus roles de colección. title: '' defaults: - based_near: Un nombre de lugar relacionado con el trabajo, como su sitio de publicación, o la ciudad, el estado o el país del que trata el contenido del trabajo. Llama al servicio web GeoNames . + based_near: El nombre de un lugar relacionado con la obra, como su sitio de publicación, o la ciudad, el estado o el país sobre el que trata el contenido de la obra. Llama a Servicio web de GeoNames. Tenga en cuenta que debe configurar su nombre de usuario de geonames en la configuración de la cuenta, o este campo tendrá un error de carga.

    Pasos para habilitar este campo:
    1. Cree una cuenta gratuita en la página del servicio web de Geonames.
    2. Vaya a su configuración de cuenta de geonames y seleccione la opción en la parte inferior para habilitar los servicios gratuitos.
    3. Vaya a la configuración de su cuenta hyku en el panel de control.
    4. Ingrese su nombre de usuario de geonames en la configuración de la cuenta hyrax.
    5. Ahora debería poder ver la lista de ubicaciones en este menú desplegable. contributor: Una persona o grupo que desea reconocer por desempeñar un papel en la creación del trabajo, pero no el papel principal. creator: La persona o grupo responsable del trabajo. Por lo general, este es el autor del contenido. Los nombres personales deben ingresarse primero con el apellido, p. & quot; Smith, John & quot ;. date_created: La fecha en que se creó el trabajo. @@ -1141,6 +1142,7 @@ es: resource_type: Categorías predefinidas para describir el tipo de contenido que se está cargando, como "artículo". o "conjunto de datos". Se puede seleccionar más de un tipo. subject: Encabezados o términos de índice que describen de qué trata el trabajo; estos deben ajustarse a un vocabulario existente. title: Un nombre para ayudar a identificar una obra. + video_embed: Si ingresa un enlace para insertar para un video, debe ser una URL con el formato adecuado que comience con 'http://' o 'https://'. También debe contener un enlace válido a un vídeo alojado que pueda aparecer en un iframe.

    Ejemplos:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: Extensión labels: @@ -1163,12 +1165,13 @@ es: defaults: admin_set_id: Conjunto administrativo based_near: Ubicación + contributor: Contribuyente creator: Creador date_created: fecha de creacion default_button_background_color: Color de fondo predeterminado del botón default_button_border_color: Color predeterminado del borde del botón default_button_text_color: Color de texto de botón predeterminado - description: Descripción + description: Resumen o resumen embargo_release_date: hasta facet_panel_background_color: Color de fondo del panel de facetas facet_panel_text_color: Color del texto del panel de facetas @@ -1177,6 +1180,7 @@ es: footer_link_hover_color: Color de desplazamiento del enlace de pie de página header_and_footer_background_color: Color de fondo de encabezado y pie de página header_and_footer_text_color: Color de texto de encabezado y pie de página + identifier: Identificador keyword: Palabra clave lease_expiration_date: hasta license: Licencia @@ -1188,8 +1192,10 @@ es: navbar_link_text_color: Color del texto del enlace de la barra de navegación navbar_link_text_hover_color: Color de desplazamiento del texto del vínculo de la barra de navegación primary_button_hover_color: Color de desplazamiento del botón principal + publisher: Editor related_url: URL relacionada rights_statement: Declaración de derechos + source: Fuente title: Título visibility_after_embargo: luego ábralo para visibility_after_lease: luego restringirlo a diff --git a/config/locales/hyrax.fr.yml b/config/locales/hyrax.fr.yml index 15557a4a78..8974552ba5 100644 --- a/config/locales/hyrax.fr.yml +++ b/config/locales/hyrax.fr.yml @@ -242,9 +242,11 @@ fr: one: Il y a un utilisateur %{count} dans ce référentiel. other: Il y a des utilisateurs %{count} dans ce référentiel. group_label: Groupes + group_role_label: Rôles de groupe id_label: Nom d'utilisateur reader_title: Afficher les utilisateurs role_label: Les rôles + site_role_label: Rôles sur le site title: gérer les utilisateurs workflow_roles: header: Rôles de workflow @@ -294,7 +296,7 @@ fr: background_attribution_html: '' base: citations: - header: 'Citation' + header: Citation form_child_work_relationships: actions: remove: Supprimer de ce travail @@ -695,7 +697,6 @@ fr: sr: batch_checkbox: Cocher pour ajouter à une collection ou modifier la liste check_all_label: Sélectionnez tous les fichiers à ajouter à une collection ou à modifier - collections_batch_checkbox: Cochez pour supprimer les collections par lots. detail_label: Afficher les détails du résumé de listing: Liste des articles dans lesquels vous avez déposé press_to: Appuyez sur pour @@ -1139,6 +1140,7 @@ fr: resource_type: Catégories prédéfinies pour décrire le type de contenu en cours de téléchargement, telles que & quot; article & quot; ou & quot; ensemble de données. & quot; Plusieurs types peuvent être sélectionnés. subject: En-têtes ou termes d'index décrivant l'objet du travail; ceux-ci doivent se conformer à un vocabulaire existant. title: Un nom pour aider à identifier une œuvre. + video_embed: Si vous saisissez un lien d'intégration pour une vidéo, il doit s'agir d'une URL correctement formatée commençant par « http:// » ou « https:// ». Il doit également contenir un lien valide vers une vidéo hébergée pouvant apparaître dans une iframe.

    Exemples :
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: Ampleur labels: @@ -1175,6 +1177,7 @@ fr: footer_link_hover_color: Couleur de survol du lien de pied de page header_and_footer_background_color: Couleur d'arrière-plan de l'en-tête et du pied de page header_and_footer_text_color: Couleur du texte de l'en-tête et du pied de page + identifier: Identifiant keyword: Mot-clé lease_expiration_date: jusqu'à ce que license: Licence @@ -1186,8 +1189,10 @@ fr: navbar_link_text_color: Couleur du texte du lien de la barre de navigation navbar_link_text_hover_color: Couleur de survol du texte du lien de la barre de navigation primary_button_hover_color: Couleur de survol du bouton principal + publisher: Éditeur related_url: URL associée rights_statement: Déclaration des droits + source: La source title: Titre visibility_after_embargo: puis ouvrez-le pour visibility_after_lease: puis limitez-le à diff --git a/config/locales/hyrax.it.yml b/config/locales/hyrax.it.yml index 2a941f62c9..498843e857 100644 --- a/config/locales/hyrax.it.yml +++ b/config/locales/hyrax.it.yml @@ -244,9 +244,11 @@ it: one: C'è un utente %{count} in questo repository. other: Ci sono utenti %{count} in questo repository. group_label: Gruppi + group_role_label: Ruoli del gruppo id_label: Nome utente reader_title: Visualizza utenti role_label: ruoli + site_role_label: Ruoli del sito title: Gestisci utenti workflow_roles: header: Ruoli del flusso di lavoro @@ -296,7 +298,7 @@ it: background_attribution_html: '' base: citations: - header: 'Citazioni' + header: Citazioni form_child_work_relationships: actions: remove: Rimuovi da questo lavoro @@ -697,7 +699,6 @@ it: sr: batch_checkbox: Seleziona per aggiungere a una raccolta o modificare l'elenco check_all_label: Seleziona tutti i file da aggiungere a una raccolta o modificati - collections_batch_checkbox: Selezionare per eliminare in batch le raccolte. detail_label: Visualizza i dettagli di riepilogo di listing: Elenco degli oggetti in cui hai depositato press_to: Premere per @@ -1099,7 +1100,7 @@ it: description: Una breve descrizione generale che si applica a tutte le opere raccolte in questo set. Ad esempio, "Tesi e file supplementari creati dagli studenti laureati della School of Earth Sciences". title: Un nome per facilitare l'identificazione del set amministrativo e per distinguerlo dagli altri set amministrativi nel repository. collection: - based_near: Un nome di luogo correlato alla raccolta, ad esempio il sito di pubblicazione o la città, lo stato o il paese di cui sono contenuti i contenuti della raccolta. Invita il servizio web GeoNames . + based_near: Un nome di luogo correlato all'opera, ad esempio il sito di pubblicazione o la città, lo stato o il paese di cui tratta il contenuto dell'opera. Invita Servizio web GeoNames. Tieni presente che devi configurare il tuo nome utente GeoNames nelle impostazioni dell'account, altrimenti questo campo avrà un errore di caricamento.

    Passaggi per abilitare questo campo:
    1. Crea un account gratuito sulla pagina del servizio web Geonames.
    2. Vai alle impostazioni dell'account geonames e seleziona l'opzione in basso per abilitare i servizi gratuiti.
    3. Accedi alle impostazioni del tuo account hyku nella dashboard.
    4. Inserisci il tuo nome utente geonames nelle impostazioni dell'account hyrax.
    5. Ora dovresti essere in grado di visualizzare l'elenco delle località in questo menu a discesa contributor: Una persona o un gruppo che desideri riconoscere per avere un ruolo nella creazione della collezione, ma non il ruolo principale. creator: La persona o il gruppo responsabile della raccolta. Di solito questo è l'autore del contenuto. I nomi personali devono essere inseriti prima con il cognome, ad es. "Smith, John." date_created: La data in cui è stata creata la raccolta. @@ -1126,7 +1127,7 @@ it: share_applies_to_new_works: Quando le nuove opere vengono create direttamente nella raccolta, concedere le autorizzazioni di condivisione di utenti e gruppi per la nuova opera in base ai ruoli della raccolta. title: '' defaults: - based_near: Un nome di luogo correlato all'opera, ad esempio il suo sito di pubblicazione, o la città, lo stato o il paese di cui si tratta. Invita il servizio web GeoNames . + based_near: Un nome di luogo correlato all'opera, ad esempio il sito di pubblicazione o la città, lo stato o il paese di cui tratta il contenuto dell'opera. Invita Servizio web GeoNames. Tieni presente che devi configurare il tuo nome utente GeoNames nelle impostazioni dell'account, altrimenti questo campo avrà un errore di caricamento.

    Passaggi per abilitare questo campo:
    1. Crea un account gratuito sulla pagina del servizio web Geonames.
    2. Vai alle impostazioni dell'account geonames e seleziona l'opzione in basso per abilitare i servizi gratuiti.
    3. Accedi alle impostazioni del tuo account hyku nella dashboard.
    4. Inserisci il tuo nome utente geonames nelle impostazioni dell'account hyrax.
    5. Ora dovresti essere in grado di visualizzare l'elenco delle località in questo menu a discesa contributor: Una persona o un gruppo che vuoi riconoscere per avere un ruolo nella creazione dell'opera, ma non il ruolo principale. creator: La persona o il gruppo responsabile del lavoro. Di solito questo è l'autore del contenuto. I nomi personali devono essere inseriti prima con il cognome, ad es. "Smith, John." date_created: La data di creazione dell'opera. @@ -1141,6 +1142,7 @@ it: resource_type: Categorie predefinite per descrivere il tipo di contenuto da caricare, come "articolo" o & quot; set di dati. & quot; È possibile selezionare più di un tipo. subject: Intestazioni o termini indicativi che descrivono di cosa tratta il lavoro; questi devono conformarsi a un vocabolario esistente. title: Un nome per aiutare a identificare un'opera. + video_embed: Se inserisci un link per incorporare un video, deve essere un URL formattato correttamente che inizia con "http://" o "https://". Deve inoltre contenere un collegamento valido a un video ospitato che può essere visualizzato in un iframe.

    Esempi:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: Estensione labels: @@ -1177,6 +1179,7 @@ it: footer_link_hover_color: Colore al passaggio del mouse del collegamento a piè di pagina header_and_footer_background_color: Colore di sfondo dell'intestazione e del piè di pagina header_and_footer_text_color: Colore del testo dell'intestazione e del piè di pagina + identifier: Identificatore keyword: Parola chiave lease_expiration_date: fino a license: Licenza @@ -1188,8 +1191,10 @@ it: navbar_link_text_color: Colore del testo del collegamento alla barra di navigazione navbar_link_text_hover_color: Colore al passaggio del mouse del testo del collegamento della barra di navigazione primary_button_hover_color: Colore del pulsante principale al passaggio del mouse + publisher: Editore related_url: URL correlato rights_statement: Dichiarazione dei diritti + source: fonte title: Titolo visibility_after_embargo: quindi aprilo fino a visibility_after_lease: quindi limitalo a diff --git a/config/locales/hyrax.pt-BR.yml b/config/locales/hyrax.pt-BR.yml index a97a1025fe..f07f8f96a8 100644 --- a/config/locales/hyrax.pt-BR.yml +++ b/config/locales/hyrax.pt-BR.yml @@ -244,9 +244,11 @@ pt-BR: one: Há um usuário %{count} neste repositório. other: Existem usuários %{count} neste repositório. group_label: Grupos + group_role_label: Funções do grupo id_label: Nome do usuário reader_title: Ver usuários role_label: Funções + site_role_label: Funções do site title: Gerenciar usuários workflow_roles: header: Funções de fluxo de trabalho @@ -697,7 +699,6 @@ pt-BR: sr: batch_checkbox: Marque para adicionar a uma coleção ou editar uma lista check_all_label: Selecione todos os arquivos a serem adicionados a uma coleção ou editados - collections_batch_checkbox: Marque para excluir coleções em lote. detail_label: Exibir detalhes resumidos de listing: Lista de itens nos quais você depositou press_to: Pressione para @@ -1099,7 +1100,7 @@ pt-BR: description: Uma breve descrição abrangente que se aplica a todos os trabalhos coletados neste conjunto. Por exemplo, "Teses e arquivos complementares criados pelos alunos de graduação da Escola de Ciências da Terra". title: Um nome para ajudar na identificação do Conjunto Administrativo e para distingui-lo de outros Conjuntos Administrativos no repositório. collection: - based_near: Um nome de local relacionado à coleção, como o site da publicação ou a cidade, estado ou país em que o conteúdo da coleção se refere. Solicita o serviço da web GeoNames . + based_near: Um nome de local relacionado ao trabalho, como seu site de publicação, ou a cidade, estado ou país sobre o qual o conteúdo do trabalho se refere. Solicita Serviço da web GeoNames. Observe que você deve configurar seu nome de usuário geonames nas configurações da conta, ou este campo terá um erro de carregamento.

    Passos para habilitar este campo:
    1. Crie uma conta gratuita na página do serviço web Geonames.
    2. Acesse suas configurações de conta de geonames e selecione a opção na parte inferior para habilitar serviços gratuitos.
    3. Acesse as configurações da sua conta hyku no painel.
    4. Digite seu nome de usuário geonames nas configurações da conta hyrax.
    5. Agora você deve conseguir ver a lista de locais neste menu suspenso contributor: Uma pessoa ou grupo que você deseja reconhecer por desempenhar um papel na criação da coleção, mas não o papel principal. creator: A pessoa ou grupo responsável pela coleção. Normalmente, este é o autor do conteúdo. Os nomes pessoais devem ser inseridos com o sobrenome primeiro, p. "Smith, John". date_created: A data em que a coleção foi criada. @@ -1126,7 +1127,7 @@ pt-BR: share_applies_to_new_works: Quando novos trabalhos são criados diretamente na coleção, conceda aos usuários e grupos de compartilhamento permissões para o novo trabalho, de acordo com suas funções de coleção. title: '' defaults: - based_near: Um nome de local relacionado ao trabalho, como o site da publicação ou a cidade, estado ou país em que o conteúdo do trabalho se refere. Solicita o serviço da web GeoNames . + based_near: Um nome de local relacionado ao trabalho, como seu site de publicação, ou a cidade, estado ou país sobre o qual o conteúdo do trabalho se refere. Solicita Serviço da web GeoNames. Observe que você deve configurar seu nome de usuário geonames nas configurações da conta, ou este campo terá um erro de carregamento.

    Passos para habilitar este campo:
    1. Crie uma conta gratuita na página do serviço web Geonames.
    2. Acesse suas configurações de conta de geonames e selecione a opção na parte inferior para habilitar serviços gratuitos.
    3. Acesse as configurações da sua conta hyku no painel.
    4. Digite seu nome de usuário geonames nas configurações da conta hyrax.
    5. Agora você deve conseguir ver a lista de locais neste menu suspenso contributor: Uma pessoa ou grupo que você deseja reconhecer por desempenhar um papel na criação do trabalho, mas não o papel principal. creator: A pessoa ou grupo responsável pelo trabalho. Normalmente, este é o autor do conteúdo. Os nomes pessoais devem ser inseridos com o sobrenome primeiro, p. "Smith, John". date_created: A data em que o trabalho foi criado. @@ -1141,6 +1142,7 @@ pt-BR: resource_type: Categorias predefinidas para descrever o tipo de conteúdo que está sendo carregado, como "artigo" ou "conjunto de dados". Mais de um tipo pode ser selecionado. subject: Cabeçalhos ou termos do índice que descrevem o que é o trabalho; estes precisam estar em conformidade com um vocabulário existente. title: Um nome para ajudar na identificação de um trabalho. + video_embed: Se você inserir um link incorporado para um vídeo, ele deverá ser um URL formatado corretamente, começando com 'http://' ou 'https://'. Ele também precisa conter um link válido para um vídeo hospedado que possa aparecer em um iframe.

    Exemplos:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: Extensão labels: @@ -1177,6 +1179,7 @@ pt-BR: footer_link_hover_color: Cor do foco do link do rodapé header_and_footer_background_color: Cor de fundo do cabeçalho e rodapé header_and_footer_text_color: Cor do texto do cabeçalho e rodapé + identifier: Identificador keyword: Palavra-chave lease_expiration_date: até license: Licença diff --git a/config/locales/hyrax.zh.yml b/config/locales/hyrax.zh.yml index 6c2732bf0f..7260273a74 100644 --- a/config/locales/hyrax.zh.yml +++ b/config/locales/hyrax.zh.yml @@ -244,9 +244,11 @@ zh: one: 该存储库中有%{count}个用户 。 other: 此存储库中有%{count}个用户 。 group_label: 团体 + group_role_label: 团体角色 id_label: 用户名 reader_title: 查看用户 role_label: 的角色 + site_role_label: 站点角色 title: 管理用户 workflow_roles: header: 工作流程角色 @@ -697,7 +699,6 @@ zh: sr: batch_checkbox: 检查添加到收藏夹或编辑列表 check_all_label: 选择所有要添加到集合或编辑的文件 - collections_batch_checkbox: 勾选批量删除集合。 detail_label: 显示的摘要详细信息 listing: 您存放的物品清单 press_to: 按到 @@ -1099,7 +1100,7 @@ zh: description: 简短的总体描述,适用于本系列中收集的所有作品。例如,“地球科学学院研究生创建的论文和补充文件”。 title: 一个名称,以帮助标识管理集并将其与存储库中的其他管理集区分开。 collection: - based_near: 与馆藏相关的地名,例如馆藏的出版地点,馆藏内容所在的城市,州或国家。调用 GeoNames Web服务。 + based_near: 与作品相关的地名,例如其出版地点,或作品内容所涉及的城市、州或国家。调用 GeoNames Web 服务。请注意,您必须在帐户设置中配置您的 geonames 用户名,否则此字段将出现加载错误。

    启用此字段的步骤:
    1.在 Geonames 网络服务页面
    创建一个免费帐户。2.转到您的 geonames 帐户设置,然后选择底部的选项以启用免费服务。
    3.在仪表板中进入您的 hyku 帐户设置。
    4.在 hyrax 帐户设置中输入您的 geonames 用户名。
    5.您现在应该能够在此下拉列表中看到位置列表 contributor: 您想通过在集合创建过程中扮演的角色而被认可的个人或团体,而不是主要角色。 creator: 负责收集的个人或团体。通常,这是内容的作者。输入的个人名字应使用姓氏,例如“约翰·史密斯”。 date_created: 集合创建的日期。 @@ -1126,7 +1127,7 @@ zh: share_applies_to_new_works: 在集合中直接创建新作品时,请根据其收藏角色授予共享用户和组新作品的权限。 title: '' defaults: - based_near: 与该作品相关的地名,例如其出版地或该作品所涉及的城市,州或国家。调用 GeoNames Web服务。 + based_near: 与作品相关的地名,例如其出版地点,或作品内容所涉及的城市、州或国家。调用 GeoNames Web 服务。请注意,您必须在帐户设置中配置您的 geonames 用户名,否则此字段将出现加载错误。

    启用此字段的步骤:
    1.在 Geonames 网络服务页面
    创建一个免费帐户。2.转到您的 geonames 帐户设置,然后选择底部的选项以启用免费服务。
    3.在仪表板中进入您的 hyku 帐户设置。
    4.在 hyrax 帐户设置中输入您的 geonames 用户名。
    5.您现在应该能够在此下拉列表中看到位置列表 contributor: 您希望因在作品创作中扮演的角色而被认可的个人或团体,而不是主要角色。 creator: 负责工作的个人或团体。通常,这是内容的作者。输入的个人名字应使用姓氏,例如“约翰·史密斯”。 date_created: 工作创建的日期。 @@ -1141,6 +1142,7 @@ zh: resource_type: 用于描述要上载的内容类型的预定义类别,例如“商品”,“商品”,“商品”,或“数据集”。可以选择一种以上的类型。 subject: 描述工作内容的标题或索引词;这些确实需要符合现有词汇。 title: 有助于识别作品的名称。 + video_embed: 如果您输入视频的嵌入链接,它必须是格式正确、以“http://”或“https://”开头的网址。它还需要包含指向可显示在 iframe 中的托管视频的有效链接。

    示例:
    https://player.vimeo.com/video/467264493?h=b089de0eab
    https://www.youtube.com/embed/ Znf73dsFdC8
    labels: extent: 程度 labels: @@ -1177,6 +1179,7 @@ zh: footer_link_hover_color: 页脚链接悬停颜色 header_and_footer_background_color: 页眉和页脚背景颜色 header_and_footer_text_color: 页眉和页脚文本颜色 + identifier: 识别码 keyword: 关键词 lease_expiration_date: 直到 license: 执照 @@ -1188,8 +1191,10 @@ zh: navbar_link_text_color: 导航栏链接文字颜色 navbar_link_text_hover_color: 导航栏链接文本悬停颜色 primary_button_hover_color: 主按钮悬停颜色 + publisher: 发行人 related_url: 相关网址 rights_statement: 权利声明 + source: 资源 title: 标题 visibility_after_embargo: 然后打开它 visibility_after_lease: 然后将其限制为 diff --git a/config/locales/it.yml b/config/locales/it.yml index 16ec913965..3cae90ac30 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,5 +1,12 @@ --- it: + errors: + messages: + valid_embed_url: "deve essere un URL di incorporamento valido di YouTube o Vimeo." + activefedora: + models: + generic_work: Lavoro + image: Immagine activerecord: attributes: site: @@ -7,6 +14,8 @@ it: application: tagline: La soluzione di repository di nuova generazione helpers: + action: + become: Diventare submit: add_role_to_group: submit: Inserisci @@ -163,6 +172,8 @@ it: forms: banner_image: hint: Per utilizzare un'immagine come sfondo di masthead, è necessario utilizzare un'immagine (JPG, GIF o PNG) alta almeno 120 pixel e larga 1200 pixel. Per risultati ottimali, utilizzare un'immagine larga almeno 1800 pixel. + collection_banner_text_color: + hint: Il colore del testo (titolo e data dell'ultimo aggiornamento) all'interno del banner della raccolta. custom_css: confirm: I CSS personalizzati sostituiranno sempre altre selezioni di temi. Procedere? warning: Se le personalizzazioni dei temi non sembrano essere applicate correttamente, verifica che non vengano sovrascritte dal CSS personalizzato. @@ -172,7 +183,7 @@ it: alert: Carica almeno un file prima di inviarlo. hint: Per le immagini predefinite, è necessario utilizzare un'immagine (JPG, GIF o PNG) che abbia le stesse dimensioni in altezza e larghezza (100 pixel in larghezza e 100 pixel in altezza) directory_image: - hint: Per utilizzare un'immagine come immagine della directory, devi utilizzare un'immagine (JPG, GIF o PNG) non più alta dell'intestazione e non più larga di 400 pixel. + hint: L'immagine della directory deve utilizzare un'immagine (JPG, GIF o PNG). La larghezza dell'immagine non dovrebbe essere più ampia di 2 volte l'altezza. facet_panel_background_color: hint: Si applica ai facet e alle intestazioni di sezione aggiuntive nelle pagine di lavoro in alcuni temi. facet_panel_text_color: @@ -209,12 +220,10 @@ it: fonts: Font themes: Temi sidebar: - account: Account accounts: conti activity_summary: Riepilogo attività labels: etichette manage_groups: Gestisci gruppi - repository_activity: Attività di deposito system_status: Stato del sistema users: activate: @@ -234,6 +243,9 @@ it: active: Attivo pending: in attesa di status_label: Stato + roles: + remove: + confirmation: Sei sicuro di voler rimuovere il ruolo "%{role}" dall'utente "%{user}"? permissions: collections: cannot: diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index f4da8af585..117fce0431 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1,5 +1,12 @@ --- pt-BR: + errors: + messages: + valid_embed_url: "deve ser uma URL de incorporação válida do YouTube ou Vimeo." + activefedora: + models: + generic_work: Trabalhar + image: Imagem activerecord: attributes: site: @@ -7,6 +14,8 @@ pt-BR: application: tagline: A solução de repos helpers: + action: + become: Tornar-se submit: add_role_to_group: submit: Adicionar @@ -163,6 +172,8 @@ pt-BR: forms: banner_image: hint: Para usar uma imagem como plano de fundo do cabeçalho, você deve usar uma imagem (JPG, GIF ou PNG) com pelo menos 120 pixels de altura e 1200 pixels de largura. Para obter melhores resultados, use uma imagem com pelo menos 1800 pixels de largura. + collection_banner_text_color: + hint: A cor do texto (título e data da última atualização) dentro do banner da coleção. custom_css: confirm: CSS personalizado sempre substituirá outras seleções de temas. Continuar? warning: Se suas personalizações de temas não parecem estar sendo aplicadas corretamente, verifique se elas não estão sendo substituídas pelo CSS personalizado. @@ -172,7 +183,7 @@ pt-BR: alert: Faça upload de pelo menos um arquivo antes de enviar. hint: Para imagens padrão, você deve usar uma imagem (JPG, GIF ou PNG) que tenha dimensões iguais de altura e largura (100 pixels de largura e 100 pixels de altura) directory_image: - hint: Para usar uma imagem como imagem do diretório, você deve usar uma imagem (JPG, GIF ou PNG) que não seja mais alta que o cabeçalho e não tenha mais de 400 pixels de largura. + hint: A imagem do diretório deve usar uma imagem (JPG, GIF ou PNG). A largura da imagem não deve ser maior que 2x a altura. facet_panel_background_color: hint: Aplica-se a facetas e cabeçalhos de seção adicionais nas páginas de trabalho em alguns temas. facet_panel_text_color: @@ -209,12 +220,10 @@ pt-BR: fonts: Fontes themes: Temas sidebar: - account: Conta accounts: Contas activity_summary: Resumo da atividade labels: Etiquetas manage_groups: Gerenciar grupos - repository_activity: Atividade do repositório system_status: Status do sistema users: activate: @@ -234,6 +243,9 @@ pt-BR: active: Ativo pending: Pendente status_label: Status + roles: + remove: + confirmation: Tem certeza de que deseja remover a função "%{role}" do usuário "%{user}"? permissions: collections: cannot: diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 3480960160..2591a0b825 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -7,55 +7,34 @@ de: hints: account: admin_emails: Geben Sie jeweils eine E-Mail-Adresse ein - allow_signup: Erlauben Sie Benutzern, sich bei Ihrem Repository anzumelden - cache_api: Aktiviert den Cache für API-Endpunkte. Experimental - contact_email: E-Mail-Adresse, die Kontakt-E-Mails erhalten soll - contact_email_to: E-Mail-Empfänger von Nachrichten, die über das Kontaktformular gesendet werden - doi_reader: Zeigen Sie die Fähigkeit, aus Datacite zu lesen, um Datensätze zu füllen. WIP nicht verwenden - doi_writer: Schreiben Sie DOIs für Datensätze. WIP nicht verwenden - email_format: Legen Sie eine Liste von E-Mail-Domains fest, die sich bei diesem Repository anmelden dürfen, z. B. (@ubiquitypress.com @gmail.com). Lassen Sie zwischen jeder Domäne ein einzelnes Leerzeichen. - email_subjet_prefix: Zeichenfolge, die den System-E-Mail-Betreffs vorangestellt werden soll. + allow_signup: Benutzern erlauben, sich für Ihr Repository anzumelden + cache_api: Cache für API-Endpunkte aktivieren. Experimentell + contact_email: Die E-Mail-Adresse, von der Systembenachrichtigungen gesendet werden. Eine zusätzliche Konfiguration ist erforderlich, um eine Adresse von anderen Domänen als der Domäne der Website hinzuzufügen + contact_email_to: Die E-Mail-Adresse, an die über die Kontaktseite gesendete Nachrichten gesendet werden + doi_reader: Zeigen Sie die Fähigkeit an, von Datacite zu lesen, um Datensätze zu füllen. WIP nicht verwenden + doi_writer: DOIs für Datensätze schreiben. WIP nicht verwenden + email_format: Legen Sie eine Liste von E-Mail-Domänen fest, die sich für dieses Repository anmelden dürfen, z. B. (@ubiquitypress.com @gmail.com). Lassen Sie jeweils ein Leerzeichen zwischen den Domänen. + email_subjet_prefix: Zeichenfolge, die vor System-E-Mail-Betreffs eingefügt wird. enable_oai_metadata: OAI-Link aktivieren oder deaktivieren - fcrepo_endpoint: - base_path: Fedora-Basispfad sollte mit einem Schrägstrich beginnen und nicht mit einem Schrägstrich enden - url: Fedora-URL sollte nicht mit einem Schrägstrich enden - file_acl: Deaktivieren Sie diese Option, wenn Sie ein Dateisystem wie Samba oder NFS verwenden, das das Festlegen von Zugriffssteuerungslisten nicht unterstützt - file_size_limit: Dieser sollte mindestens auf 536870912000 eingestellt sein - geonames_username: Registrieren Sie sich unter http://www.geonames.org/manageaccount - gtm_id: Die ID Ihres Google Tag Manager-Kontos - is_public: 'Können Benutzer Ihre Seite auf der Startseite entdecken oder auf Ihre Seiten zugreifen, ohne einen speziellen Benutzernamen / Passwort?' fcrepo_endpoint: base_path: Der Fedora-Basispfad sollte mit einem Schrägstrich beginnen UND nicht mit einem Schrägstrich enden url: Die Fedora-URL sollte nicht mit einem Schrägstrich enden - name: Ein einzelner oder mit Bindestrichen versehener Name, der für technische Aspekte des Repositories verwendet wird (z.B. "acme" oder "acme-bibliothek"). - gtm_id: Die ID Ihres Google Tag Manager-Kontos + file_acl: Deaktivieren Sie dies, wenn Sie ein Dateisystem wie Samba oder NFS verwenden, das das Festlegen von Zugriffssteuerungslisten nicht unterstützt + file_size_limit: Dies sollte mindestens auf 536870912000 eingestellt sein + geonames_username: Registrieren Sie sich unter http://www.geonames.org/manageaccount google_analytics_id: Die ID Ihres Google Analytics-Kontos - google_oauth_app_name: Der Name der Google-Anwendung in der Google API-Konsole - google_oauth_app_version: Die Version der Google-Anwendung in der Google API-Konsole - google_oauth_private_key_value: Der Wert der p12-Datei mit Base64-Verschlüsselung. (Sie können den privaten Schlüsselwert ODER den Pfad verwenden; der Wert hat Vorrang) - google_oauth_private_key_path: Der vollständige Pfad zu Ihrer p12-Schlüsseldatei. (Sie können den privaten Schlüsselwert ODER den Pfad verwenden; der Wert hat Vorrang) - google_oauth_private_key_secret: Das Geheimnis, das Sie beim Erstellen des p12-Schlüssels angegeben haben - google_oauth_client_email: OAuth-Client-E-Mail-Adresse - contact_email: Die E-Mail-Adresse, an die über die Kontaktseite gesendete Nachrichten gesendet werden - weekly_email_list: Liste von E-Mail-Adressen, an die der wöchentliche Bericht gesendet wird. Lassen Sie jeweils ein Leerzeichen zwischen den E-Mails + gtm_id: Die ID Ihres Google Tag Manager-Kontos + is_public: Können Benutzer Ihre Seite auf der Startseite entdecken oder auf Ihre Seiten zugreifen, ohne einen speziellen Benutzernamen / Passwort? + locale_name: 'Der Name des mandantenspezifischen Gebietsschemasuffix, der ihren locale.yml-Dateien hinzugefügt wird. Es sollten nur alphabetische Zeichen hinzugefügt werden, keine Symbole oder Zahlen, diese werden dann großgeschrieben. + + ' monthly_email_list: Liste von E-Mail-Adressen, an die der monatliche Bericht gesendet wird. Lassen Sie jeweils ein Leerzeichen zwischen den E-Mails - yearly_email_list: Liste von E-Mail-Adressen, an die der jährliche Bericht gesendet wird. Lassen Sie jeweils ein Leerzeichen zwischen den E-Mails - email_format: Legen Sie eine Liste von E-Mail-Domänen fest, die sich für dieses Repository anmelden dürfen, z. B. (@ubiquitypress.com @gmail.com). Lassen Sie jeweils ein Leerzeichen zwischen den Domänen. - allow_signup: Benutzern erlauben, sich für Ihr Repository anzumelden - enable_oai_metadata: OAI-Link aktivieren oder deaktivieren - shared_login: Gemeinsamen Login aktivieren oder deaktivieren - file_size_limit: Dies sollte mindestens auf 536870912000 eingestellt sein - locale_name: | - Der Name des mandantenspezifischen Gebietsschemasuffix, der ihren locale.yml-Dateien hinzugefügt wird. Es sollten nur alphabetische Zeichen hinzugefügt werden, keine Symbole oder Zahlen, diese werden dann großgeschrieben. + name: Ein einzelner oder mit Bindestrichen versehener Name, der für technische Aspekte des Repositories verwendet wird (z.B. "acme" oder "acme-bibliothek"). oai_admin_email: OAI-Endpunkt-Kontakt-E-Mail-Adresse - doi_reader: Zeigen Sie die Fähigkeit an, von Datacite zu lesen, um Datensätze zu füllen. WIP nicht verwenden - doi_writer: DOIs für Datensätze schreiben. WIP nicht verwenden - cache_api: Cache für API-Endpunkte aktivieren. Experimentell - email_subjet_prefix: Zeichenfolge, die vor System-E-Mail-Betreffs eingefügt wird. - contact_email_to: Die E-Mail-Adresse, von der Systembenachrichtigungen gesendet werden. Eine zusätzliche Konfiguration ist erforderlich, um eine Adresse von anderen Domänen als der Domäne der Website hinzuzufügen - geonames_username: Registrieren Sie sich unter http://www.geonames.org/manageaccount - file_acl: Deaktivieren Sie dies, wenn Sie ein Dateisystem wie Samba oder NFS verwenden, das das Festlegen von Zugriffssteuerungslisten nicht unterstützt + shared_login: Gemeinsamen Login aktivieren oder deaktivieren ssl_configured: Setzen Sie es auf wahr, wenn Sie https verwenden + weekly_email_list: Liste von E-Mail-Adressen, an die der wöchentliche Bericht gesendet wird. Lassen Sie jeweils ein Leerzeichen zwischen den E-Mails + yearly_email_list: Liste von E-Mail-Adressen, an die der jährliche Bericht gesendet wird. Lassen Sie jeweils ein Leerzeichen zwischen den E-Mails hyku_group: description: Eine kurze Zusammenfassung der Rolle der Gruppe user: @@ -87,11 +66,11 @@ de: email: E-Mail-Adresse user_search: uq: Benutzer suchen - 'no': 'Nein' + 'no': Nein placeholders: user_search: uq: Name oder Benutzername required: mark: "*" text: erforderlich - 'yes': 'Ja' + 'yes': Ja diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 9f7f082f4c..f5612f62cf 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -13,6 +13,8 @@ en: url: Fedora URL should not end with a slash name: A single or hyphenated name used for technical aspects of the repository (e.g., "acme" or "acme-library"). gtm_id: The ID of your Google Tag Manager account + google_analytics_id: The ID of your Google Analytics account + contact_email_to: The email address that messages submitted via the contact page are sent to weekly_email_list: List of email addresses to email the weekly report. Leave a single space between each email monthly_email_list: List of email addresses to email the monthly report. Leave a single space between each email yearly_email_list: List of email addresses to email the yearly report. Leave a single space between each email @@ -29,7 +31,6 @@ en: cache_api: Turns on cache for API endpoints. Experimental email_subjet_prefix: String to put in front of system email subjects. contact_email: The email address that system notifications will be sent from. Additional configuration is required to add an address from domains other than the site's domain - contact_email_to: The email address that messages submitted via the contact page are sent to geonames_username: Register at http://www.geonames.org/manageaccount file_acl: Turn off if using a file system like samba or nfs that does not support setting access control lists ssl_configured: Set it true if using https diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 15e7d41b31..a1b2d12686 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -6,37 +6,35 @@ es: default_message: 'Por favor, revise los problemas a continuación:' hints: account: - admin_emails: Introduce una dirección de correo electrónico a la vez - allow_signup: Permita que los usuarios se registren en su repositorio - cache_api: Activa la memoria caché para los puntos finales de la API. Experimental - contact_email: Dirección de correo electrónico que debe recibir correos electrónicos de contacto - contact_email_to: Destinatario de correo electrónico de los mensajes enviados a través del formulario de contacto - doi_reader: Mostrar la capacidad de leer de Datacite para completar registros. WIP no usar - doi_writer: Escriba DOI para los registros. WIP no usar - email_format: Establezca una lista de dominios de correo electrónico que pueden registrarse en este repositorio, por ejemplo, (@ubiquitypress.com @gmail.com). Deje un solo espacio entre cada dominio. - email_subjet_prefix: Cadena para colocar delante de los asuntos de correo electrónico del sistema. + admin_emails: Introduzca una dirección de correo electrónico a la vez + allow_signup: Permitir a los usuarios registrarse en su repositorio + cache_api: Activa la caché para puntos finales de API. Experimental + contact_email: La dirección de correo electrónico desde la que se enviarán las notificaciones del sistema. Se requiere una configuración adicional para agregar una dirección de dominios distintos al dominio del sitio + contact_email_to: La dirección de correo electrónico a la que se envían los mensajes enviados a través de la página de contacto + doi_reader: Mostrar la capacidad de leer de Datacite para rellenar registros. WIP no usar + doi_writer: Escribir DOIs para registros. WIP no usar + email_format: Establezca una lista de dominios de correo electrónico que pueden registrarse en este repositorio, por ejemplo (@ubiquitypress.com @gmail.com). Deje un espacio entre cada dominio. + email_subjet_prefix: Cadena para poner al frente de los asuntos de correo electrónico del sistema. enable_oai_metadata: Habilitar o deshabilitar el enlace OAI fcrepo_endpoint: base_path: La ruta base de Fedora debe comenzar con una barra y NO terminar con una barra url: La URL de Fedora no debe terminar con una barra - file_acl: Desactívelo si usa un sistema de archivos como samba o nfs que no admita la configuración de listas de control de acceso - file_size_limit: Esto debe establecerse en al menos 536870912000 + file_acl: Desactívelo si usa un sistema de archivos como samba o nfs que no admite listas de control de acceso + file_size_limit: Esto debe establecerse al menos en 536870912000 geonames_username: Regístrese en http://www.geonames.org/manageaccount - gtm_id: El ID de su cuenta de Google Tag Manager - is_public: "¿Los usuarios pueden descubrir su sitio en la página de inicio o acceder a sus páginas sin un nombre de usuario/contraseña especiales?" - locale_name: 'El nombre del sufijo de configuración regional específico del arrendatario agregado a sus archivos locale.yml. Solo se deben agregar caracteres alfabéticos, sin símbolos ni números, estos se escribirán en mayúscula.' - name: Un nombre único o con guión utilizado para los aspectos técnicos del repositorio (por ejemplo, "acme" o "acme-library"). - oai_admin_email: Dirección de correo electrónico de contacto del terminal OAI - shared_login: Habilitar o deshabilitar el inicio de sesión compartido - ssl_configured: Establézcalo en verdadero si usa https google_analytics_id: La ID de su cuenta de Google Analytics - google_oauth_app_name: El nombre de la aplicación de Google en la consola de API de Google - google_oauth_app_version: La versión de la aplicación de Google en la consola de API de Google - google_oauth_private_key_value: El valor del archivo p12 con cifrado base64. (Puede usar el valor de la clave privada O el camino; el valor tiene prioridad) - google_oauth_private_key_path: La ruta completa a su archivo p12, archivo clave. (Puede usar el valor de la clave privada O el camino; el valor tiene prioridad) - google_oauth_private_key_secret: El secreto proporcionado cuando creó la clave p12 - google_oauth_client_email: Dirección de correo electrónico del cliente OAuth + gtm_id: La ID de su cuenta de Google Tag Manager + is_public: "¿Pueden los usuarios descubrir su sitio en la página principal o acceder a sus páginas sin un nombre de usuario/contraseña especial?" + locale_name: 'El nombre del sufijo de configuración regional específico del inquilino añadido a sus archivos locale.yml. Solo se deben agregar caracteres alfabéticos, no símbolos o números, estos se capitalizarán. + + ' + monthly_email_list: Lista de direcciones de correo electrónico para enviar el informe mensual. Deje un espacio entre cada correo electrónico + name: Un nombre único o con guiones utilizado para aspectos técnicos del repositorio (por ejemplo, "acme" o "acme-biblioteca"). + oai_admin_email: Dirección de correo electrónico de contacto del punto final OAI shared_login: Habilitar o deshabilitar inicio de sesión compartido + ssl_configured: Establézcalo como verdadero si está utilizando https + weekly_email_list: Lista de direcciones de correo electrónico para enviar el informe semanal. Deje un espacio entre cada correo electrónico + yearly_email_list: Lista de direcciones de correo electrónico para enviar el informe anual. Deje un espacio entre cada correo electrónico hyku_group: description: Un breve resumen del papel del grupo user: @@ -75,4 +73,4 @@ es: required: mark: "*" text: requerido - 'yes': 'Sí' + 'yes': Sí diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index a938ff7390..d7c08b62d4 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -7,49 +7,34 @@ fr: hints: account: admin_emails: Entrez une adresse e-mail à la fois - allow_signup: Autoriser les utilisateurs à s'inscrire à votre référentiel - cache_api: Active le cache pour les points de terminaison d'API. Expérimental - contact_email: Adresse e-mail qui doit recevoir les e-mails de contact - contact_email_to: Email destinataire des messages envoyés via le formulaire de contact - doi_reader: Afficher la possibilité de lire à partir de Datacite pour remplir les enregistrements. WIP ne pas utiliser - doi_writer: Rédigez des DOI pour les enregistrements. WIP ne pas utiliser - email_format: Définissez une liste de domaines de messagerie autorisés à s'inscrire à ce référentiel, par exemple (@ubiquitypress.com @gmail.com). Laissez un seul espace entre chaque domaine. - email_subjet_prefix: Chaîne à placer devant les sujets des e-mails système. + allow_signup: Autoriser les utilisateurs à s'inscrire à votre dépôt + cache_api: Active le cache pour les points d'accès API. Expérimental + contact_email: L'adresse e-mail à partir de laquelle les notifications système seront envoyées. Une configuration supplémentaire est nécessaire pour ajouter une adresse provenant de domaines autres que le domaine du site + contact_email_to: L'adresse e-mail à laquelle les messages soumis via la page de contact sont envoyés + doi_reader: Afficher la capacité à lire depuis Datacite pour remplir les enregistrements. WIP ne pas utiliser + doi_writer: Écrire des DOIs pour les enregistrements. WIP ne pas utiliser + email_format: Définissez une liste de domaines de messagerie autorisés à s'inscrire à ce dépôt, par exemple (@ubiquitypress.com @gmail.com). Laissez un espace entre chaque domaine. + email_subjet_prefix: Chaîne à mettre devant les sujets des e-mails système. enable_oai_metadata: Activer ou désactiver le lien OAI fcrepo_endpoint: - base_path: Le chemin de base de Fedora doit commencer par une barre oblique ET ne pas se terminer par une barre oblique - url: Fedora URL ne devrait pas se terminer par une barre oblique - file_acl: Désactiver si vous utilisez un système de fichiers comme samba ou nfs qui ne prend pas en charge la définition de listes de contrôle d'accès - file_size_limit: Il doit être défini sur au moins 536870912000 + base_path: Le chemin de base de Fedora doit commencer par un slash et ne PAS se terminer par un slash + url: L'URL de Fedora ne doit pas se terminer par un slash + file_acl: Désactivez si vous utilisez un système de fichiers comme samba ou nfs qui ne prend pas en charge les listes de contrôle d'accès + file_size_limit: Ceci doit être réglé à au moins 536870912000 geonames_username: Inscrivez-vous sur http://www.geonames.org/manageaccount google_analytics_id: L'ID de votre compte Google Analytics - google_oauth_app_name: Le nom de l'application Google dans la console API de Google - google_oauth_app_version: La version de l'application Google dans la console API de Google - google_oauth_private_key_value: La valeur du fichier p12 avec cryptage base64. (Vous pouvez utiliser la valeur de la clé privée OU le chemin ; la valeur prime) - google_oauth_private_key_path: Le chemin complet vers votre fichier p12, fichier clé. (Vous pouvez utiliser la valeur de la clé privée OU le chemin ; la valeur prime) - google_oauth_private_key_secret: Le secret fourni lorsque vous avez créé la clé p12 - google_oauth_client_email: Adresse e-mail du client OAuth - gtm_id: L'identifiant de votre compte Google Tag Manager - is_public: Les utilisateurs peuvent-ils découvrir votre site sur la page d'accueil ou accéder à vos pages sans nom d'utilisateur/mot de passe particulier ? - locale_name: 'Le nom du suffixe de paramètres régionaux spécifiques au locataire ajouté à leurs fichiers locale.yml. Seuls les caractères alphabétiques doivent être ajoutés, pas de symboles ni de chiffres, ceux-ci seront alors en majuscules. + gtm_id: L'ID de votre compte Google Tag Manager + is_public: Les utilisateurs peuvent-ils découvrir votre site sur la page d'accueil ou accéder à vos pages sans identifiant/mot de passe spécifique? + locale_name: 'Le nom du suffixe de locale spécifique au locataire ajouté à leurs fichiers locale.yml. Seuls les caractères alphabétiques doivent être ajoutés, pas de symboles ou de chiffres, ceux-ci seront alors mis en majuscule. ' - monthly_email_list: Liste des adresses e-mail pour envoyer le rapport mensuel. Laissez un seul espace entre chaque email - name: Nom unique ou un trait d'union utilisé pour les aspects techniques du référentiel (par exemple, "acme" ou "acme-bibliothèque"). - oai_admin_email: Adresse e-mail de contact du point de terminaison OAI - shared_login: Activer ou désactiver la connexion partagée - file_size_limit: Ceci doit être réglé à au moins 536870912000 - locale_name: | - Le nom du suffixe de locale spécifique au locataire ajouté à leurs fichiers locale.yml. Seuls les caractères alphabétiques doivent être ajoutés, pas de symboles ou de chiffres, ceux-ci seront alors mis en majuscule. + monthly_email_list: Liste des adresses e-mail pour envoyer le rapport mensuel. Laissez un espace entre chaque e-mail + name: Un nom simple ou avec des tirets utilisé pour les aspects techniques du dépôt (par exemple, "acme" ou "acme-bibliothèque"). oai_admin_email: Adresse e-mail de contact du point d'accès OAI - doi_reader: Afficher la capacité à lire depuis Datacite pour remplir les enregistrements. WIP ne pas utiliser - doi_writer: Écrire des DOIs pour les enregistrements. WIP ne pas utiliser - cache_api: Active le cache pour les points d'accès API. Expérimental - email_subjet_prefix: Chaîne à mettre devant les sujets des e-mails système. - contact_email_to: L'adresse e-mail à partir de laquelle les notifications système seront envoyées. Une configuration supplémentaire est nécessaire pour ajouter une adresse provenant de domaines autres que le domaine du site - geonames_username: Inscrivez-vous sur http://www.geonames.org/manageaccount - file_acl: Désactivez si vous utilisez un système de fichiers comme samba ou nfs qui ne prend pas en charge les listes de contrôle d'accès + shared_login: Activer ou désactiver la connexion partagée ssl_configured: Mettez-le sur vrai si vous utilisez https + weekly_email_list: Liste des adresses e-mail pour envoyer le rapport hebdomadaire. Laissez un espace entre chaque e-mail + yearly_email_list: Liste des adresses e-mail pour envoyer le rapport annuel. Laissez un espace entre chaque e-mail hyku_group: description: Un bref résumé du rôle du groupe user: @@ -81,11 +66,11 @@ fr: email: Adresse e-mail user_search: uq: Recherche d'utilisateur - 'no': 'Non' + 'no': Non placeholders: user_search: uq: Nom ou nom d'utilisateur required: mark: "*" text: requis - 'yes': 'Oui' + 'yes': Oui diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 3c80f0470b..19e5647ee0 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -1,5 +1,5 @@ --- -eit: +it: simple_form: cancel: Annulla error_notification: @@ -7,49 +7,34 @@ eit: hints: account: admin_emails: Inserisci un indirizzo email alla volta - allow_signup: Consenti agli utenti di registrarsi al tuo repository - cache_api: Attiva la cache per gli endpoint API. Sperimentale - contact_email: Indirizzo e-mail che dovrebbe ricevere le e-mail di contatto - contact_email_to: Email destinatario dei messaggi inviati tramite il modulo di contatto - doi_reader: Mostra la capacità di leggere da Datacite per popolare i record. WIP non utilizzare - doi_writer: Scrivi DOI per i record. WIP non utilizzare - email_format: Impostare un elenco di domini di posta elettronica a cui è consentito registrarsi a questo repository, ad esempio (@ubiquitypress.com @gmail.com). Lascia un singolo spazio tra ogni dominio. + allow_signup: Permetti agli utenti di iscriversi al tuo repository + cache_api: Abilita la cache per gli endpoint API. Sperimentale + contact_email: L'indirizzo email da cui saranno inviate le notifiche di sistema. È richiesta una configurazione aggiuntiva per aggiungere un indirizzo da domini diversi dal dominio del sito + contact_email_to: L'indirizzo email al quale i messaggi inviati tramite la pagina di contatto sono inviati + doi_reader: Mostra la capacità di leggere da Datacite per popolare i record. Lavoro in corso, non utilizzare + doi_writer: Scrivi DOI per i record. Lavoro in corso, non utilizzare + email_format: Imposta una lista di domini email che possono iscriversi a questo repository es. (@ubiquitypress.com @gmail.com). Lascia uno spazio tra ogni dominio. email_subjet_prefix: Stringa da mettere davanti agli oggetti delle email di sistema. - enable_oai_metadata: Abilita o disabilita il collegamento OAI - + enable_oai_metadata: Abilita o disabilita il link OAI fcrepo_endpoint: base_path: Il percorso base di Fedora dovrebbe iniziare con una barra e NON terminare con una barra url: L'URL di Fedora non dovrebbe terminare con una barra - name: Un nome singolo o con trattino usato per gli aspetti tecnici del repository (ad es., "acme" o "acme-biblioteca"). - gtm_id: L'ID del tuo account Google Tag Manager + file_acl: Disattiva se stai utilizzando un sistema di file come samba o nfs che non supporta le liste di controllo degli accessi + file_size_limit: Questo dovrebbe essere impostato almeno a 536870912000 + geonames_username: Registrati su http://www.geonames.org/manageaccount google_analytics_id: L'ID del tuo account Google Analytics - google_oauth_app_name: Il nome dell'applicazione Google nella console API di Google - google_oauth_app_version: La versione dell'applicazione Google nella console API di Google - google_oauth_private_key_value: Il valore del file p12 con crittografia base64. (Puoi utilizzare il valore della chiave privata OPPURE il percorso; il valore ha la precedenza) - google_oauth_private_key_path: Il percorso completo del tuo file p12, file chiave. (Puoi utilizzare il valore della chiave privata OPPURE il percorso; il valore ha la precedenza) - google_oauth_private_key_secret: Il segreto fornito quando hai creato la chiave p12 - google_oauth_client_email: Indirizzo email del client OAuth - contact_email: L'indirizzo email al quale i messaggi inviati tramite la pagina di contatto sono inviati - weekly_email_list: Lista di indirizzi email ai quali inviare il rapporto settimanale. Lascia uno spazio tra ogni email + gtm_id: L'ID del tuo account Google Tag Manager + is_public: Gli utenti possono scoprire il tuo sito nella pagina principale o accedere alle tue pagine senza un nome utente/password speciale? + locale_name: 'Il nome del suffisso specifico del tenant aggiunto ai loro file locale.yml. Aggiungi solo caratteri alfabetici, niente simboli o numeri, questi saranno poi capitalizzati. + + ' monthly_email_list: Lista di indirizzi email ai quali inviare il rapporto mensile. Lascia uno spazio tra ogni email - yearly_email_list: Lista di indirizzi email ai quali inviare il rapporto annuale. Lascia uno spazio tra ogni email - email_format: Imposta una lista di domini email che possono iscriversi a questo repository es. (@ubiquitypress.com @gmail.com). Lascia uno spazio tra ogni dominio. - allow_signup: Permetti agli utenti di iscriversi al tuo repository - enable_oai_metadata: Abilita o disabilita il link OAI - shared_login: Abilita o disabilita l'accesso condiviso - file_size_limit: Questo dovrebbe essere impostato almeno a 536870912000 - is_public: 'Gli utenti possono scoprire il tuo sito nella pagina principale o accedere alle tue pagine senza un nome utente/password speciale?' - locale_name: | - Il nome del suffisso specifico del tenant aggiunto ai loro file locale.yml. Aggiungi solo caratteri alfabetici, niente simboli o numeri, questi saranno poi capitalizzati. + name: Un nome singolo o con trattino usato per gli aspetti tecnici del repository (ad es., "acme" o "acme-biblioteca"). oai_admin_email: Indirizzo email di contatto dell'endpoint OAI - doi_reader: Mostra la capacità di leggere da Datacite per popolare i record. Lavoro in corso, non utilizzare - doi_writer: Scrivi DOI per i record. Lavoro in corso, non utilizzare - cache_api: Abilita la cache per gli endpoint API. Sperimentale - email_subjet_prefix: Stringa da mettere davanti agli oggetti delle email di sistema. - contact_email_to: L'indirizzo email da cui saranno inviate le notifiche di sistema. È richiesta una configurazione aggiuntiva per aggiungere un indirizzo da domini diversi dal dominio del sito - geonames_username: Registrati su http://www.geonames.org/manageaccount - file_acl: Disattiva se stai utilizzando un sistema di file come samba o nfs che non supporta le liste di controllo degli accessi + shared_login: Abilita o disabilita l'accesso condiviso ssl_configured: Impostalo su vero se stai utilizzando https + weekly_email_list: Lista di indirizzi email ai quali inviare il rapporto settimanale. Lascia uno spazio tra ogni email + yearly_email_list: Lista di indirizzi email ai quali inviare il rapporto annuale. Lascia uno spazio tra ogni email hyku_group: description: Un breve riassunto del ruolo del gruppo user: @@ -88,4 +73,4 @@ eit: required: mark: "*" text: richiesto - 'yes': 'Sì' + 'yes': Sì diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml index 366ad74781..b9d839343a 100644 --- a/config/locales/simple_form.pt-BR.yml +++ b/config/locales/simple_form.pt-BR.yml @@ -6,50 +6,35 @@ pt-BR: default_message: 'Por favor, reveja os problemas abaixo:' hints: account: - admin_emails: Digite um endereço de email de cada vez - allow_signup: Permitir que os usuários se inscrevam em seu repositório - cache_api: Ativa o cache para terminais de API. Experimental - contact_email: Endereço de e-mail que deve receber e-mails de contato - contact_email_to: Destinatário de e-mail de mensagens enviadas por meio do formulário de contato - doi_reader: Mostrar capacidade de ler do Datacite para preencher registros. WIP não use - doi_writer: Escreva DOIs para registros. WIP não use - email_format: Defina uma lista de domínios de e-mail que podem se inscrever neste repositório, por exemplo (@ubiquitypress.com @gmail.com). Deixe um único espaço entre cada domínio. - email_subjet_prefix: String a ser colocada antes dos assuntos do e-mail do sistema. - enable_oai_metadata: Ativar ou desativar o link OAI + admin_emails: Insira um endereço de e-mail por vez + allow_signup: Permitir que usuários se inscrevam no seu repositório + cache_api: Ativar cache para pontos finais de API. Experimental + contact_email: O endereço de e-mail do qual as notificações do sistema serão enviadas. Uma configuração adicional é necessária para adicionar um endereço de domínios diferentes do domínio do site + contact_email_to: O endereço de e-mail para o qual as mensagens enviadas via página de contato são enviadas + doi_reader: Mostrar capacidade de ler da Datacite para preencher registros. Em desenvolvimento, não use + doi_writer: Escrever DOIs para registros. Em desenvolvimento, não use + email_format: Defina uma lista de domínios de e-mail que são permitidos para se inscrever neste repositório, por exemplo (@ubiquitypress.com @gmail.com). Deixe um espaço único entre cada domínio. + email_subjet_prefix: String para colocar na frente dos assuntos dos e-mails do sistema. + enable_oai_metadata: Habilitar ou desabilitar link OAI fcrepo_endpoint: - base_path: O caminho base do Fedora deve começar com uma barra E não terminar com uma barra - url: O URL do Fedora não deve terminar com uma barra - file_acl: Desligue se estiver usando um sistema de arquivos como samba ou nfs que não suporta a configuração de listas de controle de acesso - file_size_limit: Isso deve ser definido para pelo menos 536870912000 + base_path: O caminho base do Fedora deve começar com uma barra e NÃO terminar com uma barra + url: A URL do Fedora não deve terminar com uma barra + file_acl: Desative se estiver usando um sistema de arquivos como samba ou nfs que não suporta listas de controle de acesso + file_size_limit: Isso deve ser configurado para pelo menos 536870912000 geonames_username: Registre-se em http://www.geonames.org/manageaccount google_analytics_id: O ID da sua conta no Google Analytics - google_oauth_app_name: O nome do aplicativo Google no console da API do Google - google_oauth_app_version: A versão do aplicativo Google no console da API do Google - google_oauth_private_key_value: O valor do arquivo p12 com criptografia base64. (Você pode usar o valor da chave privada OU o caminho; o valor tem precedência) - google_oauth_private_key_path: O caminho completo para o seu arquivo p12, arquivo de chave. (Você pode usar o valor da chave privada OU o caminho; o valor tem precedência) - google_oauth_private_key_secret: O segredo fornecido quando você criou a chave p12 - google_oauth_client_email: Endereço de e-mail do cliente OAuth - gtm_id: O ID da sua conta do Gerenciador de tags do Google + gtm_id: O ID da sua conta no Google Tag Manager is_public: Os usuários podem descobrir seu site na página inicial ou acessar suas páginas sem um nome de usuário/senha especial? - locale_name: 'O nome do sufixo de localidade específico do locatário incluído em seus arquivos locale.yml. Apenas caracteres alfabéticos devem ser adicionados, sem símbolos ou números, estes serão então maiúsculos. + locale_name: 'O nome do sufixo específico do inquilino adicionado aos seus arquivos locale.yml. Apenas caracteres alfabéticos devem ser adicionados, sem símbolos ou números, estes serão então capitalizados. ' - monthly_email_list: Lista de endereços de e-mail para enviar o relatório mensal por e-mail. Deixe um único espaço entre cada e-mail - name: Um nome único ou hifenizado usado para aspectos técnicos do repositório (por exemplo, "acme" ou "acme-library"). - oai_admin_email: Endereço de e-mail de contato do terminal OAI - shared_login: Habilitar ou desabilitar login compartilhado - file_size_limit: Isso deve ser configurado para pelo menos 536870912000 - locale_name: | - O nome do sufixo específico do inquilino adicionado aos seus arquivos locale.yml. Apenas caracteres alfabéticos devem ser adicionados, sem símbolos ou números, estes serão então capitalizados. + monthly_email_list: Lista de endereços de e-mail para enviar o relatório mensal. Deixe um espaço único entre cada e-mail + name: Um nome simples ou hifenizado usado para aspectos técnicos do repositório (por exemplo, "acme" ou "acme-biblioteca"). oai_admin_email: Endereço de e-mail de contato do ponto final OAI - doi_reader: Mostrar capacidade de ler da Datacite para preencher registros. Em desenvolvimento, não use - doi_writer: Escrever DOIs para registros. Em desenvolvimento, não use - cache_api: Ativar cache para pontos finais de API. Experimental - email_subjet_prefix: String para colocar na frente dos assuntos dos e-mails do sistema. - contact_email_to: O endereço de e-mail do qual as notificações do sistema serão enviadas. Uma configuração adicional é necessária para adicionar um endereço de domínios diferentes do domínio do site - geonames_username: Registre-se em http://www.geonames.org/manageaccount - file_acl: Desative se estiver usando um sistema de arquivos como samba ou nfs que não suporta listas de controle de acesso + shared_login: Habilitar ou desabilitar login compartilhado ssl_configured: Defina como verdadeiro se estiver usando https + weekly_email_list: Lista de endereços de e-mail para enviar o relatório semanal. Deixe um espaço único entre cada e-mail + yearly_email_list: Lista de endereços de e-mail para enviar o relatório anual. Deixe um espaço único entre cada e-mail hyku_group: description: Um breve resumo do papel do grupo user: @@ -81,11 +66,11 @@ pt-BR: email: Endereço de e-mail user_search: uq: Pesquisar por usuário - 'no': 'Não' + 'no': Não placeholders: user_search: uq: Nome ou nome de usuário required: mark: "*" text: obrigatório - 'yes': 'Sim' + 'yes': Sim diff --git a/config/locales/simple_form.zh.yml b/config/locales/simple_form.zh.yml index 9b8745ed47..f83f657a7e 100644 --- a/config/locales/simple_form.zh.yml +++ b/config/locales/simple_form.zh.yml @@ -3,53 +3,38 @@ zh: simple_form: cancel: 取消 error_notification: - default_message: '请检查以下问题:' + default_message: 请检查以下问题: hints: account: admin_emails: 一次输入一个电子邮件地址 - allow_signup: 允许用户注册到您的存储库 - cache_api: 为 API 端点打开缓存。实验性的 - contact_email: 应接收联系电子邮件的电子邮件地址 - contact_email_to: 通过联系表发送的邮件的电子邮件收件人 - doi_reader: 显示从 Datacite 读取数据以填充记录的能力。 WIP 不使用 - doi_writer: 为记录写 DOI。 WIP 不使用 - email_format: 设置允许注册此存储库的电子邮件域列表,例如 (@ubiquitypress.com @gmail.com)。在每个域之间留一个空格。 + allow_signup: 允许用户注册您的存储库 + cache_api: 打开API端点的缓存。实验性 + contact_email: 系统通知将从其中发送的电子邮件地址。需要额外的配置才能从网站域以外的域添加地址 + contact_email_to: 通过联系页面提交的消息发送到的电子邮件地址 + doi_reader: 显示从Datacite读取以填充记录的能力。仍在进行中,请勿使用 + doi_writer: 为记录编写DOIs。仍在进行中,请勿使用 + email_format: 设置允许注册此存储库的电子邮件域的列表,例如(@ubiquitypress.com @gmail.com)。每个域之间留一个空格。 email_subjet_prefix: 放在系统电子邮件主题前面的字符串。 - enable_oai_metadata: 启用或禁用 OAI 链接 + enable_oai_metadata: 启用或禁用OAI链接 fcrepo_endpoint: - base_path: Fedora 基本路径应以斜线开始,而不是以斜杠结尾 - url: Fedora 网址不应以斜线结尾 - file_acl: 如果使用不支持设置访问控制列表的文件系统(如 samba 或 nfs),请关闭 - file_size_limit: 这应该至少设置为 536870912000 - geonames_username: 在 http://www.geonames.org/manageaccount 注册 + base_path: Fedora基本路径应该以斜杠开始且不应该以斜杠结束 + url: Fedora URL不应该以斜杠结束 + file_acl: 如果使用不支持设置访问控制列表的文件系统(如samba或nfs),请关闭它 + file_size_limit: 此值应至少设置为536870912000 + geonames_username: 在http://www.geonames.org/manageaccount上注册 google_analytics_id: 您的Google Analytics帐户ID - google_oauth_app_name: Google API控制台中的Google应用程序名称 - google_oauth_app_version: Google API控制台中的Google应用版本 - google_oauth_private_key_value: 带有base64加密的p12文件的值。(您可以使用私钥值或路径;值优先) - google_oauth_private_key_path: 您的p12密钥文件的完整路径。(您可以使用私钥值或路径;值优先) - google_oauth_private_key_secret: 创建p12密钥时提供的秘密 - google_oauth_client_email: OAuth客户电子邮件地址 - gtm_id: 您的 Google 跟踪代码管理器帐户的 ID - is_public: 用户是否可以在主页上发现您的站点或在没有特殊用户名/密码的情况下访问您的页面? - locale_name: '添加到其 locale.yml 文件的租户特定区域设置后缀的名称。只能添加字母字符,不能添加符号或数字,这些将被大写。 + gtm_id: 您的Google标签管理器帐户ID + is_public: 用户可以在主页上发现您的网站,或者不需要特殊的用户名/密码就可以访问您的页面吗? + locale_name: '添加到其locale.yml文件的租户特定语言环境后缀的名称。只应添加字母字符,不应添加符号或数字,这些将随后大写。 ' - monthly_email_list: 用于发送月度报告的电子邮件地址列表。在每封电子邮件之间留一个空格 - name: 用于存储库技术方面的单个或连字符名称(例如“acme”或“acme-library”)。 - oai_admin_email: OAI 端点联系人电子邮件地址 - shared_login: 启用或禁用共享登录 - file_size_limit: 此值应至少设置为536870912000 - locale_name: | - 添加到其locale.yml文件的租户特定语言环境后缀的名称。只应添加字母字符,不应添加符号或数字,这些将随后大写。 + monthly_email_list: 每月报告的电子邮件地址列表。每个电子邮件之间留一个空格 + name: 用于存储技术方面的单一或连字符名称(例如,“acme”或“acme-library”)。 oai_admin_email: OAI端点联系电子邮件地址 - doi_reader: 显示从Datacite读取以填充记录的能力。仍在进行中,请勿使用 - doi_writer: 为记录编写DOIs。仍在进行中,请勿使用 - cache_api: 打开API端点的缓存。实验性 - email_subjet_prefix: 放在系统电子邮件主题前面的字符串。 - contact_email_to: 系统通知将从其中发送的电子邮件地址。需要额外的配置才能从网站域以外的域添加地址 - geonames_username: 在http://www.geonames.org/manageaccount上注册 - file_acl: 如果使用不支持设置访问控制列表的文件系统(如samba或nfs),请关闭它 + shared_login: 启用或禁用共享登录 ssl_configured: 如果使用https,请将其设置为true + weekly_email_list: 每周报告的电子邮件地址列表。每个电子邮件之间留一个空格 + yearly_email_list: 每年报告的电子邮件地址列表。每个电子邮件之间留一个空格 hyku_group: description: 该组的角色简介 user: @@ -81,11 +66,11 @@ zh: email: 电子邮件地址 user_search: uq: 用户搜索 - 'no': '否' + 'no': 否 placeholders: user_search: uq: 名称或用户名 required: mark: "*" text: 必填 - 'yes': '是' + 'yes': 是 diff --git a/config/locales/zh.yml b/config/locales/zh.yml index b37f0c56af..ccf4770faf 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1,5 +1,12 @@ --- zh: + errors: + messages: + valid_embed_url: "必须是有效的YouTube或Vimeo嵌入URL。" + activefedora: + models: + generic_work: 工作 + image: 图像 activerecord: attributes: site: @@ -7,6 +14,8 @@ zh: application: tagline: 下一代存储库解决方案 helpers: + action: + become: 成為 submit: add_role_to_group: submit: 加 @@ -163,6 +172,8 @@ zh: forms: banner_image: hint: 要将图像用作标头广告背景,您应该使用至少120像素高和1200像素宽的图像(JPG,GIF或PNG)。为了获得最佳效果,请使用至少1800像素宽的图像。 + collection_banner_text_color: + hint: 集合横幅内文本(标题和上次更新日期)的颜色。 custom_css: confirm: 自定义CSS将始终覆盖其他主题选择。继续? warning: 如果您的主题自定义设置似乎无法正确应用,请确认自定义CSS不会覆盖它们。 @@ -172,7 +183,7 @@ zh: alert: 请至少上传一个文件,然后再提交。 hint: 对于默认图像,您应该使用高度和宽度尺寸(宽度为100像素,高度为100像素)的图像(JPG,GIF或PNG) directory_image: - hint: 要将图像用作目录图像,应使用高度不超过标题且宽度不超过 400 像素的图像(JPG、GIF 或 PNG)。 + hint: 目录图像应使用图像(JPG、GIF 或 PNG)。图片的宽度不应超过高度的 2 倍。 facet_panel_background_color: hint: 适用于某些主题中工作页面上的分面和附加节标题。 facet_panel_text_color: @@ -209,12 +220,10 @@ zh: fonts: 字形 themes: 主题 sidebar: - account: 帐户 accounts: 帐号 activity_summary: 活动摘要 labels: 标签 manage_groups: 管理组 - repository_activity: 存储库活动 system_status: 系统状态 users: activate: @@ -234,6 +243,9 @@ zh: active: 活性 pending: 有待 status_label: 状态 + roles: + remove: + confirmation: 您确定要从用户“%{user}”中删除角色“%{role}”吗? permissions: collections: cannot: From d2649f93e3316d70c534e58403c7cb77705fec05 Mon Sep 17 00:00:00 2001 From: Jeremy Friesen Date: Tue, 19 Dec 2023 13:56:57 -0500 Subject: [PATCH 04/29] =?UTF-8?q?=F0=9F=8E=81=20Contribute=20back=20work?= =?UTF-8?q?=20from=20PALNI/PALCI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to Issues: - https://github.com/samvera/hyku/issues/1815 - https://github.com/scientist-softserv/palni-palci/issues/951 Related to Pull Requests: - https://github.com/scientist-softserv/palni-palci/pull/952 --- .../hyrax/collection_presenter_decorator.rb | 19 +++++++ .../collection_presenter_decorator_spec.rb | 52 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 spec/presenters/hyrax/collection_presenter_decorator_spec.rb diff --git a/app/presenters/hyrax/collection_presenter_decorator.rb b/app/presenters/hyrax/collection_presenter_decorator.rb index c348e58881..1adb737b10 100644 --- a/app/presenters/hyrax/collection_presenter_decorator.rb +++ b/app/presenters/hyrax/collection_presenter_decorator.rb @@ -96,6 +96,25 @@ def collection_featurable? user_can_feature_collection? && solr_document.public? end + ## + # OVERRIDE to handle search_only tenant's not having access to the collection type badge from + # the document's home tenant. + # + # @return [String] + # + # @see https://github.com/scientist-softserv/palni-palci/issues/951 + # @see https://github.com/samvera/hyku/issues/1815 + def collection_type_badge + return "" unless Site.account&.present? + return "" if Site.account.search_only? + + super + rescue ActiveRecord::RecordNotFound + # This is a fail-safe if we deleted the underlying Hyrax::CollectionType but have not yet + # cleaned up the SOLR records. + "" + end + def display_feature_collection_link? collection_featurable? && FeaturedCollection.can_create_another? && !collection_featured? end diff --git a/spec/presenters/hyrax/collection_presenter_decorator_spec.rb b/spec/presenters/hyrax/collection_presenter_decorator_spec.rb new file mode 100644 index 0000000000..d5c795cbdf --- /dev/null +++ b/spec/presenters/hyrax/collection_presenter_decorator_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Hyrax::CollectionPresenter do + describe '#collection_type_badge' do + subject { presenter.collection_type_badge } + + # We're decorating an alternate base class so that we don't need the full pre-amble for testing + # our decoration. In other words, let's trust Hyrax::CollectionPresenter's specs for the + # "super" method call. + let(:base_class) do + Class.new do + def collection_type_badge + "" + end + prepend Hyrax::CollectionPresenterDecorator + end + end + let(:presenter) { base_class.new } + + before { allow(Site).to receive(:account).and_return(account) } + + context 'when the Site.account is nil' do + let(:account) { nil } + + it { is_expected.to eq("") } + end + + context 'when the Site.account is search_only' do + let(:account) { FactoryBot.build(:account, search_only: true) } + + it { is_expected.to eq("") } + end + + context 'when the Site.account is NOT search_only' do + let(:account) { FactoryBot.build(:account, search_only: false) } + + it { is_expected.to start_with(" Date: Fri, 5 Jan 2024 10:12:52 -0800 Subject: [PATCH 05/29] :gift: Hyrax 5 upgrade (#2047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove dogbiscuit crossover, fix routes * add spec for search history * add missing methods to avoid content block errors * Update Hyku Gemfile.lock * add option to support good job for background jobs instead of sidekiq * update gemfile lock * Update good_job.rb * merge * fix routes file, add missing js file * :lipstick: styling fix * :lipstick: rubocop fixes * remove iiif_print/iiif_print require cause of Sprockets::FileNotFound in Splash#index error. Doesn't seem necessary. * :gift: :broom: add missing file and format with semicolons The previous build revealed that admin_color_select.js was missing. It also complained about missing semicolons. * :lipstick: correct missing semicolons appease the hound by formatting js file with semicolons. * :broom: remove call to iiif_print/iiif_print Cause of build error. It's already being included in sass. * :lipstick: Rubocop fixes * ✅ Fix test setup for catalog_controller_spec This commit will add additional setup to the catalog_controller_spec. * Add knapsack helper * remove Adventist from application.rb * 🎁 Add conditional to run correct command for worker This commit adds a conditional to run the correct command for sidekiq or good_job, when running docker compose up. * Make appearance constants overrideable This allows for the knapsack to override the constants by defining and using a method rather than a constant in the look-ups. * add reporting fix to hyku ci * spec loading fixes * rubocop * 🧹Make appearance defaults overrideable * bump bulkrax to 5.4.0 This commit pulls in a small collection of bug fixes. * 🧹 Include knapsack css * fix job loading when selecting good job * adjust docker compose to use the startup script * :gift: install tesseract eng_best * ♻️ revert tesseract best changes to dockerfile this change belongs in the knapsack directory instead * ♻️ revert tesseract best changes to dockerfile this change belongs in the knapsack directory instead * 🐛 Bring fix for entry show page not showing link This commit will bring in a fix from Bulkrax that correctly shows the object from the entry show page. ref: - https://github.com/samvera-labs/bulkrax/commit/0e68a5ea81307dc9e06f9a1e7c0c4cc8cd3de873 * 🧹 Revert previous commit and update Bulkrax This commit will update Bulkrax to 5.4.1 instead of using the override from the last commit. We also revert the changes from the last commit. * :bug: subject can't be blank for the contact form Previously there was a bug because even if you typed in a subject, the contact form would error saying that it was blank. Part of issue: - https://github.com/scientist-softserv/adventist-dl/issues/608 * mend * ♻️ Add handling for Knapsack theme overrides Prior to this commit, we were looking for themes yaml files relative to the directory of the spawning script. For Hyku that was always the `Rails.root` directory. However, when running specs in Knapsack, that directory was `Knapsack::Engine.root`. This unearthed a potential configuration issue; namely that we want Knapsack's to control what themes are available, meaning we don't want to require amending Hyku's themes. So, we introduce a mechanism for looking up files first in the Knapsack then in Hyku. I discovered this bug in the specs for knapsack (below is the *Error stack trace*)
    Error stack trace ``` 2) Hyrax::Admin::AppearancesController with an administrator GET #show assigns the requested site as @site Failure/Error: get :show, params: {} Errno::ENOENT: No such file or directory @ rb_sysopen - config/home_themes.yml # /usr/local/bundle/gems/psych-3.3.4/lib/psych.rb:582:in `initialize' # /usr/local/bundle/gems/psych-3.3.4/lib/psych.rb:582:in `open' # /usr/local/bundle/gems/psych-3.3.4/lib/psych.rb:582:in `unsafe_load_file' # ./hyrax-webapp/app/controllers/hyrax/admin/appearances_controller.rb:19:in `show' # /usr/local/bundle/gems/rails-controller-testing-1.0.5/lib/rails/controller/testing/template_assertions.rb:62:in `process' # /usr/local/bundle/gems/devise-4.9.2/lib/devise/test/controller_helpers.rb:35:in `block in process' # /usr/local/bundle/gems/devise-4.9.2/lib/devise/test/controller_helpers.rb:104:in `catch' # /usr/local/bundle/gems/devise-4.9.2/lib/devise/test/controller_helpers.rb:104:in `_catch_warden' # /usr/local/bundle/gems/devise-4.9.2/lib/devise/test/controller_helpers.rb:35:in `process' # /usr/local/bundle/gems/rails-controller-testing-1.0.5/lib/rails/controller/testing/integration.rb:16:in `block (2 levels) in ' # ./spec/controllers/hyrax/hyrax/admin/appearances_controller_spec.rb:31:in `block (4 levels) in ' # /usr/local/bundle/gems/webmock-3.19.1/lib/webmock/rspec.rb:39:in `block (2 levels) in ' # ./hyrax-webapp/spec/support/multitenancy_metadata.rb:50:in `block (2 levels) in ' # /usr/local/bundle/gems/rspec-retry-0.6.2/lib/rspec/retry.rb:124:in `block in run' # /usr/local/bundle/gems/rspec-retry-0.6.2/lib/rspec/retry.rb:110:in `loop' # /usr/local/bundle/gems/rspec-retry-0.6.2/lib/rspec/retry.rb:110:in `run' # /usr/local/bundle/gems/rspec-retry-0.6.2/lib/rspec_ext/rspec_ext.rb:12:in `run_with_retry' # /usr/local/bundle/gems/rspec-retry-0.6.2/lib/rspec/retry.rb:37:in `block (2 levels) in setup' # ./spec/spec_helper.rb:10:in `block (2 levels) in ' ```
    Related to: - https://github.com/samvera/hyku/pull/2007 - https://github.com/samvera/hyku/pull/2008 The above two commits will require some reconciliation once this is incorporated. * ♻️ Favor Hyku::Application.path_for over Rails.root Given the existence of Knapsack we need to consider how overrides in Knapsack will take precedence over Hyku files. This change handles cases where we want to use the Knapsack's uploaded thumbnails. Related to: - #2010 * 🎁 Add highlight key to UV This commit allows for the `parent_query` to be highlighted in the UV so users won't have to do a catalog search and also a UV search. * 🐛 Mixin `HykuKnapsack::ApplicationHelper` This commit will mixin the `HykuKnapsack::ApplicationHelper` into the ApplicationHelper which will allow us to use #render_ocr_snippets for snippet highlighting. * Revert "🐛 Mixin `HykuKnapsack::ApplicationHelper`" This reverts commit ae093f1c04e036057c17ec14e8a8ebeeac32a6bb. * ♻️ Favor class_attribute over constant In Adventist, we're needing to override the constant's values. By making this a class_attribute we can more readily do the override via configuration instead of obliteration of a constant. * ♻️ Favor configurable html head title value Prior to this commit, we hard-coded the page title; this is something that should be far more configurable. And this refactor is a step towards that. This also allows for downstream implementors to not have to override the view simply to change the title element. * 🐛 Add custom rendering for license Prior to this commit, the License would render as a plain URL. With this change, we are now coercing the license into a URL that is labeled and titled with the name of the license. This is copied and modified based on [Rights show partial][1] Related to: - https://github.com/scientist-softserv/adventist-dl/issues/620 [1]: https://github.com/samvera/hyrax/blob/b334e186e77691d7da8ed59ff27f091be1c2a700/app/views/records/show_fields/_rights.html.erb * ♻️ Add option to override devise configuration For Adventist, we wanted to disable registration of accounts as this was creating a case where folks were creating their own accounts and tenants. Related to: - https://github.com/scientist-softserv/adventist-dl/issues/492 - https://github.com/scientist-softserv/adventist-dl/issues/618 - https://github.com/samvera/hyku/pull/1964 - https://github.com/scientist-softserv/adventist-dl/pull/493 * run asset build later in process to allow knapsack to run it only one time * 🐛 Fix Add to Collection for page 2+ of works Prior to this commit, when you were on page 2 of your works and selected a work to add to a collection, the query for available collections would use the page 2 as part of the collection query. This would mean the first 100 collections (default page size) that you had access to add works to were skipped. With this commit, we omit the query parameters from the works page and then query collections. Related to: - https://github.com/samvera/hyrax/pull/5972 - https://github.com/samvera/hyrax/issues/5969 - https://github.com/scientist-softserv/adventist-dl/issues/625 Co-authored-by: LaRita Robinson * test that invited users are added to the registered group * Add invited users to the registered group Fixes a bug where users who were invited with no roles would not show up in the users list at all * 🐛 Change Hyky to Hyku (#2029) Typo caused inability to upload collection thumbnail. * 🎁 Modify labels in UV for V3 manifests This commit will add the same treatment as we have for V2 manifests to V3 manifests. This will allow the UV to add a more human readable label to the pages. Ref: - https://github.com/scientist-softserv/adventist-dl/issues/628 * 🐛 Fix bad method name Related to: - https://github.com/samvera/hyku/pull/2023/files - https://github.com/scientist-softserv/atla-hyku/pull/198 * 🐛 Move some methods to be public This commit will move #solr_document, #current_ability, and #request to be public methods. Since these methods in Hyrax were not private, in the decorator it should not be private either. Ref: - https://github.com/samvera/hyrax/blob/b334e186e77691d7da8ed59ff27f091be1c2a700/app/presenters/hyrax/file_set_presenter.rb#L10 * First attempt for upgrade to Hyrax 5.0.0.rc2 So far the changes throw an error `The adapter `nulldb_adapter` is not yet supported` when doing a `docker compose build`. Commenting out `RUN RAILS_ENV=production SECRET_KEY_BASE=`bin/rake secret` DB_ADAPTER=nulldb DB_URL='postgresql://fake' bundle exec rake assets:precompile && yarn install` in the Dockerfile allows the build to complete but still can't boot yet. * 🧹 Get assets to precompile This commit has a lot in it, but of note: - We forked apartment gem to expand activerecord version support - Used the Hyrax upgrade guide to update assets - Replaced bootstrap 3 variables with hard coded values - Commented out blacklight_helper_behavior.rb because Blacklight 7 removed it, we'll have to figure out how to get the same functionality in the new version * 🧹 Switch to Hyrax `double_combo` branch We are using this branch on Hyrax because it has quite a few Valkyrie related fixes in it. * 🧹 use class attribute instead of constant to correct failing specs * Fix specs * 🧹 Get the page to load This commit will get the proprietor page to load. You can create a tenant and vist it as well. * ♻️ Introduce Hyku::Application.theme_view_path_roots With the introduction of [HykuKnapsack][1], we are adjusting how we create instances of Hyku. Namely we don't clone Hyku but instead we incorporate Hyku as a submodule into a Knapsack. The Knapsack is a Rails engine that is mounted in the Hyku application. What this means is that when we want to Prior to this commit, the only way to adjust themed views would have been to add them to the Rails application (e.g. Hyku) directly. Which would work in a non-Knapsack ecosystem. However, with Knapsack we need a means of saying "Hey, for themes we want to be able to add/adjust views within the knapsack." Hence this change. [1]: https://github.com/samvera-labs/hyku_knapsack * 🧹 Prepping for rubocop This commit will change the Docker image to rc1 because rc2 stopped working (at least locally). Also getting things ready to run rubocop. * 🧹 Ran `bundle exec rubocop -a` * 🧹 Autocorrect frozen string literals ```sh rubocop --only Style/FrozenStringLiteralComment -A ``` * 🧹 Rubocop'd lengths and other low hanging fruit Various length metrics were disabled in this commit. Also other various cops that were easy to fix. * 🧹 Remove deprecated Blacklight code removing the deprecated blacklight code allows the specs to run. We should consider if finding a replacement is necessary in the following ticket: - https://github.com/scientist-softserv/hykuup_knapsack/issues/54 Issue: - https://github.com/scientist-softserv/hykuup_knapsack/issues/35 * 🧹 Avoid `#present?` in non-Rails situations Rails provides [`Object#present?`][1], which is a method that is not generally available in Ruby. However in the changed context, the scripts do not have access to Rails methods. By using this adjusted approach we favor baseline Ruby methods. [1]: https://api.rubyonrails.org/classes/Object.html#method-i-present-3F * 🧹 Get Homepage Controller specs passing (#2058) Refactor homepage controller to more closely align with Hyrax's homepage controller. Adjust theme views to render `modal` instead of `ajax_modal`. refs: https://github.com/scientist-softserv/hykuup_knapsack/issues/56 * ♻️ Replace Homepage Presenter with decorator (#2059) * 🧹Get CatalogController specs working (#2060) * 🧹 Hyrax 5 get additional specs passing (#2062) * 🧹 Get chrome.hyku.test working * 🧹 Get SitesControllerSpecs working File fixtures were broken. Ref https://github.com/scientist-softserv/hykuup_knapsack/issues/56 * 🧹 Remove google analytics from config reload This is a piece of a fix for analytics that came in via https://github.com/scientist-softserv/palni-palci/pull/946 Additional backporting of analytics work is still needed, but this fixes numerous specs so it is being pulled in earlier. * 🧹 Update ruby version for circleci * 🧹 Update rails version for circleci * 🧹Database migration & schema update (#2063) For table hyrax_counter_metrics * 🧹 Fix Hyrax 5 remaining controller spec failures (#2064) * 🧹Remove ActiveFedora monkeypatch * 🧹Fix file fixture * 🧹 Upgrade views from Bootstrap 3 to 4 This commit is a first swing of upgrading all the views from Bootstrap 3 to 4. There's still a lot of work to be done to make the views look good but this is a good first step. * Hyrax 5 upgrade rubocop fixes & get specs running in CI (#2065) * 🧹Rubocop Fixes * Attempt to fix circleci * 🧹 Fix CollapsableSectionPresenter override The override was causing issues not passing the nav-link class into the anchor tags. This fix specifically passes the title attribute in vs the previous implementation of trying to pass all html options in. * 🧹 Remove required translations from locales This commit will remove the override for the `required` translations since it changed to a badge badge-style in Bootstrap 4. This is accounted for in Hyrax so we can fall back to that. * 🐛 Fix some javascript errors This commit will fix a few javascript errors, at least enough to get the the bootstrap javascript to work correctly. We're still getting a the almond-rails js error but that seems to exist in previous Hyku as well. * Hyrax 5 upgrade rubocop fixes & get specs running in CI (#2065) * 🧹Rubocop Fixes * Attempt to fix circleci * 🧹 Post review adjustments These are adjustments made after the PR has been reviewed. * 🧹 Clean up a couple blacklight views This commit will account for the new versions of Blacklight and Blacklight Gallery. The masonry view no longer exists so we can't tell how it's broken as of right now. I'm sure we'll run into it during our testing. NOTE: These changes refer to the shared search tenant. * 🧹 Bring back `BlacklightHelperBehavior` This commit will bring back the `BlacklightHelperBehavior` module but it will be namespaced under `Hyku` to avoid conflicts. We will then include it in the `HyraxHelper` module so those methods should override the ones in `Blacklight::BlacklightHelperBehavior`. * 🧹 Hyrax 5 upgrade additional specs (#2067) * 🧹 More spec & controller fixes Ref https://github.com/scientist-softserv/hykuup_knapsack/issues/55 - Contact form controller and pages controller duplicate some of the homepage controller behavior, so corresponding fixes are needed. - Update collections factory and collection_ability_spec to match hyrax and stop calling `.gid` on a collection_type. - Adjust permission_template_form_spec to adjust for removal of method `reset_access_controls!` and add a few new expectations instead. * 🧹 Begin work on Roles Service Working with Permission Templates has changed, requiring an adjustment to both logic and specs. * 🧹 Solr Document Ability Spec * Rubocop fixes * Fix typo of collection variable * Reinstate some collections factory overrides * Revert Rubocop changes Caused spec failure in roles_service_spec. * Fix create_default_admin_set_job_spec * Restore mistakenly removed capta use * 🧹 Stanford import specs We have plans to remove the Stanford import logic, but for now, this handles the specs that were failing, in an attempt to get CI to pass. * Make Rubocop happy * Fix typo * Update Hyrax to pull in most recent changes * More spec fixes Menu presenter: removed html_options Application Helper: override for missing_translations now receives `false` when translation wasn't found rather than a string. * Fix missing translation logic * Fix missing_translation override Simplify and fix override. A missing translation now returns `false` instead of the text `translation missing`. The super method requires the `option` parameter, so we opted to keep the override. * 🧹 Remove copied code The overrides are no longer necessary. In reviewing the diff between what I'm removing and what is in the `double_combo` branch; the major change was adding an operator for the `reduce` function. The breaking problem was that the method signature of one of the copied methods has changed in Hyrax 5.0. The removal considers that Hyrax::Group is of two different classes: - In Hyrax, it is a plain old Ruby object - In Hyku, it is descended from ActiveRecord::Base The two classes have different instantiation parameters. Hence the introduction of `Hyrax::Group.new` in this code. * 🧹 Fix missing method name * 🐛 Ensure :maxFileSize is integer Prior to this commit, we were casting the value to string. However Hyrax attempts to do division on that string. See https://github.com/samvera/hyrax/blob/b7891b758411c59f71ff54212e0d250fcc47e35f/app/views/hyrax/base/_form_files.html.erb#L6-L15 * Skip taking a picture on failure * 🧹 Fix issue regarding Valkyrie::Identifier The encountered error was: ``` NoMethodError: undefined method `split' for # ``` * 🧹 Favor Sipity::Entity function over to_sipity_entity The Sipity conversion methods are now explicit functions (much like the `Array()` function). - https://github.com/samvera/hyrax/blob/5aaa568d348a180add2a1337d9d794b740703df8/app/models/sipity.rb#L20 * Update Hyrax IIIF AV gem * 🧹 Review controller overrides Many of the controllers just needed to be updated with the decorator pattern. We're creating a new concern for all the Hyku specific overrides in Hyku::WorksControllerBehavior. This has to be included in the Controller classes after Hyrax::WorksControllerBehavior to override the relevant methods. * clean up from pr review * Appease rubocop * Update IIIF Print gem * 🤖 Specify Cache Root Prior to this commit, we were seeing the following failure: > Failure/Error: Site.application\_name || super > > ActionView::Template::Error: > Permission denied @ dir\_s\_mkdir - /app The `Account#setup_tenant_cache` sets the cache\_store to be `ENV.fetch('HYKU_CACHE_ROOT', '/app/samvera/file_cache')`. When I locally chanced it to `/blorg/samvera/file_cache` I could repeat the error. With this commit, we're setting the cache value for the CircleCI run. Why was it previously working? Perhaps because `Account#setup_tenant_cache` never got called due to other configurations? There is a guard clause around the method call. * 🧹 Update spec to reflect AdminSet default behavior In the following commit, we removed the deprecated `AdminSet.find_or_create_default_admin_set_id`: - https://github.com/samvera/hyrax/commit/863c4bc177702b3aa00ad01f93e7a3d214bb2d73 This commit follows the advice of the deprecation warning and now favors the `Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id` method call. Related to: - https://github.com/samvera/hyrax/pull/6203 * Remove obsolete site roles and routes (#2080) The site/roles route is obsolete. This is an attempt to remove as much of the obsolete logic as possible, as it has been replaced by the groups with roles feature. * Remove stanford importer Importer was deprecated in prior release version. * 🤖 Fix spec/features/collection_type_spec.rb There are three major changes: 1. Favor setting `collection_type` attribute instead of `collection_type_gid` 2. Change `#collections?` to `#collections#any?` 3. Change CSS selector based on Bootstrap upgrade The above changes are related to work done in: - https://github.com/samvera/hyrax/pull/5730 - https://github.com/samvera/hyrax/pull/5742 - https://github.com/samvera/hyrax/pull/4701 See: - https://github.com/samvera/hyrax/commit/eb6e04e312b039a65ef58b1781fa112fd93f2c0c - https://github.com/samvera/hyrax/commit/237c0c63297df05b9d483a15b212900c2323a455 - https://github.com/samvera/hyrax/commit/280664bd6b0e59c2e10795151f0afcec91b9c15b * 🤖 Fix stub methods to pass tests These tests weren't verifying that we could reach Redis; they were testing the interface of the `RedisEndpoint#ping` by mocking the instance. This commit changes the mocking by avoiding a call to `Hyrax::RedisEventStore.instance` which was raising a Redis connection error. Again, this test is not is redis connecting but "assume we are trying to connect to redis now demonstrate ping." * Lock chromedriver version (#2086) Refs https://github.com/CircleCI-Public/browser-tools-orb/pull/96 https://github.com/CircleCI-Public/browser-tools-orb/issues/75#issuecomment-169811589 * 🧹 Clean up model overrides Most of what's in this commit is just updating the Hyrax version in the override comments. There was also an opportunity to switch the `ContactForm` model to a decorator. * 🤖 Favor general spec over specific With the CircleCI alternate place for the file_store cache, we need to fallback to a more general test assertion. * 🤖 Bring over Hyrax spec changes * 🤖 Extract constant to ease testing We're not concerned with where the cached file is for testing purposes; so instead of hard-coding a value that can change in the ENV, let's compare the constant that we use in the code. tl;dr - Don't rely on magic strings * 🤖 Re-arrange CleanupAccountJob specs The `CleanupAccountJob` was stubbing very nosily; needing to know too much about implementation details of the end-points. Instead this preserves the over-view spec (e.g. what all the cleanup spec actually cleans up) while moving that nosy logic to the constituent endpoint. Most of these specs are testing that the method chains work; which is perhaps adequate as the other option is far more expensive tests (e.g. make a new Fedora node only to then immediately destroy it) I'm also leveraging the new `Redis::Namespace#clear` method. Related to: - https://github.com/resque/redis-namespace/pull/202 * 🧹 Restore btn-sign-up on splash page This change was part of the bulk upgrade of Bootstrap 3 to 4. * 🧹 Fix `./spec/requests/admin_dashboard_spec.rb` spec Prior to this commit, the specs failed because of the introduction of the WorkflowResponsibilityFormDecorator. The decorator extracted prior logic from Hyrax::Admin::WorkflowRolesController. In copying that logic we introduced a subtle bug. Namely, we favored the original `.new` behavior if and only if you provided a `:user_id`. This broke places where we instantiated the form in a view (e.g. `./app/views/hyrax/admin/workflow_roles/index.html.erb`). With this change, we make the behavior of `.new` fail towards its "normal" implementation and instead rely on the presence of an attribute to switch to a different form instantatior. See Commit: - https://github.com/samvera/hyku/commit/095edca9a50e647eb1b3ee31a34711c32151912e Related to: - https://github.com/samvera/hyku/pull/2079 Co-authored-by: LaRita Robinson Co-authored-by: Kirk Wang * 🎁 Upgrade Redis initializer to Hyrax 5's gen version Prior to this commit, when looking at the `Hyrax::RedisEventStore.instance` we saw it's connection information as: `#` We were expecting the connection to be the following: `#` What we were seeing in tests is when we hit the redis connection we were getting an error about not being able to connect to 127.0.0.1:6379 (e.g. localhost). With this commit, we have a clear connection to Redis. See Redis Config initializer: - https://github.com/samvera/hyrax/blob/966951ffaa72524e4a775f8a198bd51a47ece7d9/lib/generators/hyrax/templates/config/initializers/redis_config.rb#L1-L10 Co-authored-by: Kirk Wang * 🧹 Clean up actors Just found one actor that was overriding Hyrax. This commit will switch it to a decorator. * 🐛 Handle missing labels Adjust paths and keys to look for labels. * Appease the cops * 🧹 Assigning collection_type The partials rendered in the tested view assume that we've set a collection type. * Appease rubocop * Fixing nav link selectors to new structure * Fixing nav link selectors to new structure * 🧹 Clean up forms The only big thing in this commit is the appearance_decorator. I've added to the spec to show that the decorator should be working as intended. * 🧹 Fix spec based on HTML class changes * 🧹 Narrow specificity of CSS selector The selector was *very* specific, and with the HTML class changes for Bootstrap 3 to 4, this almost certainly broke. Note, there are still underlying issues with two other specs; there errors are listed below: ``` 1) Admin can select home page theme when a search results theme is selected updates the search results page with the selected layout view Failure/Error: super ActionView::Template::Error: undefined method `with_collection' for nil:NilClass ``` * 🧹 Remove specificity of CSS selectors in test The hyper specific selectors are not durable for CSS framework upgrades. * 🐛 Adding document_component to blacklight's config Blacklight 7.35.0 's default document_component is `nil`, see: - https://github.com/projectblacklight/blacklight/blob/ac5fa8b300c5ad5c35b1663ef0f15372ffa2be0f/lib/blacklight/configuration.rb#L213 - https://github.com/projectblacklight/blacklight/blob/ac5fa8b300c5ad5c35b1663ef0f15372ffa2be0f/lib/blacklight/configuration.rb#L186 Digging around in the wiki, you might find (only found because I cloned the repo): - https://github.com/projectblacklight/blacklight/wiki/Configuration---Results-View Related to: - https://github.com/projectblacklight/blacklight/pull/2317 * 🧹 Allow :clean or :clean_repo to work for the cleaners Hyrax has :clean_repo and Hyku has :clean Sometimes folks copy over specs from Hyrax, and bring along the :clean_repo; which looks like it should work. With this commit, we bring that logic along! * 🧹 Remove skip CI Perhaps it's flappy; but let's see. * 🧹 Fix button class for Bootstrap 3 to 4 * 🧹 Favor Layout/LineLength over Metrics/LineLength Rubocop seams to prefer the Layout namespace for LineLength. * 🧹 The selector is not working Checking the HTML (on the CircleCI SSH environment), it appears that the selector should work in test. But it is not. So I'm removing the specificity. * Cleaning a spec that should start clean * 🧹 Ensure feature specs run clean Prior to this commit, we did not automatically clean the features. The below ripgrep (and output) shows that there were some features which did not start from a clean state. ``` rg "(clean|clean_repo):" spec/features --files-without-match `` ``` spec/features/accounts_spec.rb spec/features/proprietor_spec.rb spec/features/featured_collections_spec.rb spec/features/user_roles_spec.rb spec/features/oai_pmh_spec.rb ``` * Stab at trying to find a problem * 🧹 Clean up services This commit will reconcile the services that are overrides for Hyrax with the Hyrax 5.0.0rc2 version. There were a number of overrides that were changed to the decorator pattern. * Moving rescue and documenting the why * 🧹 Rearrange order of filter chain This is not tested in the UI, but the `show_works_or_works_that_contain_files` remains after the troublesome advanced query filters. * 🧹 Address PR comments This commit addresses comments from the review but one thing that is of note is loading the I18n translations in the application.rb file. We needed this because our decorators load prior to I18n loads the locales in our config/locales directory for them to use so we were getting missing translations. * 🧹 Attempting to find and squash bug * ♻️ Account for observed customizations Dear reviewer, put on your reading glasses. This commit looks at the newly refactored `Hyrax::IiifAv::DisplaysContentDecorator` as well as the current state of [PALS's Hyrax::IiifAv::DisplaysContentDecorator][1] and attempts to account for the variances between the two by introducing configurations. Yes, we could port this to the hyrax-iiif_av gem, but for now that would not solve the underlying issue of how we've been handling things. Why the Hyku::Application class attribute? Because the decorator is being mixed into a module, which does not response to `.class_attribute` methods. [1]: https://github.com/scientist-softserv/palni-palci/blob/8754556c0225ce9f04674c1ffac6403586fd65f4/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb * 🧹 Clean up other overrides This commit is aiming to clean up the rest of the found overrides. * 🧹 Change locale file load order so that our local files are prioritized We add our local config locale directory to the I18n.load_path so that our local files take precendence over the ones found in our gems. Related issue: - https://github.com/scientist-softserv/hykuup_knapsack/issues/55 * 🧹 Update IIIF Print and AV gems We were not seeing images loading correctly in the UV because we introduced the `Hyku::WorksControllerBehavior` and IIIF Print did not know about it. - https://github.com/scientist-softserv/iiif_print/commit/cad2cf33fc07ec2abb5b6834a858b5ed74903c12 We were seeing a respon to mismatch error in the Hyrax IIIF AV. - https://github.com/samvera-labs/hyrax-iiif_av/commit/4723b8fd0961fa179caf787532bd7922a44957ec Also, the image that I tested was a phone image that apparently had different ExifImageHeight/ExifImageWidth values than the ImageHeight/ImageWidth values. This was probably because modern phones typically generate multiple images so the user can choose the best shot. The exiftool_image_to_fits.xslt was proritizing the Exif values over the regular values and would cause our image characterization to have weird height and width values. The change in this PR prioritizes the regular over the Exif values. Ref: - https://github.com/scientist-softserv/hykuup_knapsack/issues/68 * 🐛 Fix video thumbnail generation In a previous commit we created the `Hyrax::FileSetDerivativesServiceDecorator` to override the default thumbnails that Hyrax uses but I forgot to add the video thumbnail, which is fixed here. Ref - https://github.com/scientist-softserv/hykuup_knapsack/issues/69 * 🧹 Add space between thumbnail and title * 🧹 Update Hyku version This commit bumps the hyku version from 5.1.0 to 6.0.0. * :lipstick: ruboco fix * 🧹 Fixing a few styles This commit will change the maximized size to match Bootstrap 4 break points. Also, added btn-secondary to hyku.css so it doesn't get overriden by appearance styles. Finally, restructured the homepage to make it look more like Hyrax 5. * sort properties * adjust screen size * 🧹 Update `rails` to `6-1-stable` for GoodJob There is a compatibility issue with Rails 6.1.7.6 and Ruby 3.2 that was affecting GoodJob. There was a backport to the 6-1-stable branch that fixes this issue. See: - https://github.com/rails/rails/pull/46895 Ref: - https://github.com/scientist-softserv/hykuup_knapsack/issues/67 * 🎁 only add Good Job classes if it's enabled ref: - https://github.com/samvera/hyku/pull/2047#discussion_r1441130543 * ♻️ Removing file in favor of comments found in docker-compose.yml Remove file in favor of comments found on lines #138 - 139, which uses the sleep infinity command to accomplish the same thing. ref: - https://devopscube.com/keep-docker-container-running/ * 🧹 Remove disabling of Rubocop Naming/PredicateName (#2126) This cop was originally disabled in order to get specs to run during CI without code changes. Now that specs are running, the changes can be safely made to appease rubocop rather than disabling it. * 🎁 Add Hyrax version in the footer Often we forget what version of Hyrax we're running so this should help. --------- Co-authored-by: Rob Kaufman Co-authored-by: Shana Moore Co-authored-by: LaRita Robinson Co-authored-by: Jeremy Friesen Co-authored-by: Benjamin Kiah Stroud <32469930+bkiahstroud@users.noreply.github.com> Co-authored-by: LaRita Robinson --- .circleci/config.yml | 15 +- .dockerignore | 1 - .env | 11 +- .rubocop.yml | 180 +-- .rubocop_fixme.yml | 201 +++ .rubocop_todo.yml | 235 ++- Dockerfile | 3 +- Gemfile | 54 +- Gemfile.lock | 1290 +++++++++-------- README.md | 2 +- Rakefile | 4 + ...nvironment.rb => environment_decorator.rb} | 18 +- app/assets/javascripts/admin_color_select.js | 20 + app/assets/javascripts/application.js | 29 +- .../range_limit_distro_facets.js | 348 +++++ .../range_limit_shared.js | 24 + .../range_limit_slider.js | 130 ++ app/assets/javascripts/hyrax/app.js.erb | 10 +- .../javascripts/hyrax/featured_works.js | 2 +- .../javascripts/hyrax/thumbnail_select.es6 | 2 +- app/assets/stylesheets/application.css | 5 +- app/assets/stylesheets/hyku.scss | 26 +- app/assets/stylesheets/hyrax.scss | 11 +- .../themes/cultural_repository.scss | 10 +- .../themes/neutral_repository.scss | 4 +- app/controllers/account_sign_up_controller.rb | 32 +- app/controllers/admin/accounts_controller.rb | 12 +- .../admin/group_roles_controller.rb | 30 +- .../admin/group_users_controller.rb | 32 +- app/controllers/admin/groups_controller.rb | 18 +- app/controllers/admin/users_controller.rb | 10 +- .../admin/work_types_controller.rb | 6 +- app/controllers/admin_controller.rb | 12 +- app/controllers/application_controller.rb | 162 +-- app/controllers/catalog_controller.rb | 107 +- .../hyku/works_controller_behavior.rb | 111 ++ .../hyrax/admin/users_controller_behavior.rb | 16 +- .../hyrax/users_controller_decorator.rb | 40 +- .../hyrax/works_controller_behavior.rb | 522 ------- .../hyku/invitations_controller.rb | 12 +- .../hyku/registrations_controller.rb | 6 +- .../hyrax/admin/appearances_controller.rb | 90 -- .../admin/appearances_controller_decorator.rb | 79 + .../hyrax/admin/workflow_roles_controller.rb | 51 - .../hyrax/contact_form_controller.rb | 131 -- .../contact_form_controller_decorator.rb | 117 ++ .../hyrax/content_blocks_controller.rb | 44 - .../content_blocks_controller_decorator.rb | 20 + .../hyrax/dashboard/collections_controller.rb | 737 ---------- .../collections_controller_decorator.rb | 182 +++ .../featured_collection_lists_controller.rb | 6 +- .../hyrax/generic_works_controller.rb | 1 + app/controllers/hyrax/homepage_controller.rb | 150 +- app/controllers/hyrax/images_controller.rb | 1 + .../hyrax/my/works_controller_decorator.rb | 18 - app/controllers/hyrax/pages_controller.rb | 116 -- .../hyrax/pages_controller_decorator.rb | 81 ++ .../identity_providers_controller.rb | 43 +- app/controllers/labels_controller.rb | 12 +- .../proprietor/accounts_controller.rb | 64 +- .../proprietor/users_controller.rb | 74 +- app/controllers/roles_controller.rb | 34 - app/controllers/saved_searches_controller.rb | 7 + app/controllers/search_history_controller.rb | 7 + app/controllers/sites_controller.rb | 56 +- .../users/omniauth_callbacks_controller.rb | 2 +- ...{appearance.rb => appearance_decorator.rb} | 347 +++-- .../permission_template_form_decorator.rb | 2 +- .../workflow_responsibility_form_decorator.rb | 31 + .../workflow_responsibility_group_form.rb | 5 + app/helpers/admin_stats_helper.rb | 2 +- app/helpers/application_helper.rb | 8 +- .../blacklight/blacklight_helper_behavior.rb | 423 ------ app/helpers/google_tag_manager_helper.rb | 4 +- app/helpers/group_navigation_helper.rb | 2 +- .../hyku/blacklight_helper_behavior.rb | 63 + app/helpers/hyrax_helper.rb | 3 +- app/helpers/shared_search_helper.rb | 24 +- app/jobs/cleanup_account_job.rb | 28 +- app/jobs/create_default_admin_set_job.rb | 2 +- app/jobs/create_solr_collection_job.rb | 124 +- app/jobs/delete_old_guests_job.rb | 8 +- app/jobs/embargo_auto_expiry_job.rb | 6 +- app/jobs/import_work_from_purl_job.rb | 97 -- app/jobs/lease_auto_expiry_job.rb | 6 +- app/jobs/remove_solr_collection_job.rb | 8 +- app/mailers/hyku_mailer.rb | 6 +- app/models/ability.rb | 2 +- app/models/account.rb | 6 +- app/models/collection.rb | 1 + app/models/concerns/account_cname.rb | 8 +- app/models/concerns/account_settings.rb | 130 +- app/models/concerns/account_switch.rb | 4 +- .../hyrax/ability/collection_ability.rb | 8 +- .../hyrax/ability/solr_document_ability.rb | 4 +- .../concerns/hyrax/ability/work_ability.rb | 2 +- app/models/content_block.rb | 47 +- app/models/fcrepo_endpoint.rb | 2 +- app/models/featured_collection_list.rb | 36 +- app/models/generic_work.rb | 2 + .../access_controls/embargo_decorator.rb | 2 +- .../hydra/access_controls/lease_decorator.rb | 2 +- .../collection_type_participant_decorator.rb | 2 +- app/models/hyrax/contact_form.rb | 40 - app/models/hyrax/contact_form_decorator.rb | 25 + app/models/hyrax/group.rb | 54 +- .../permission_template_access_decorator.rb | 2 +- app/models/image.rb | 2 + app/models/nil_fcrepo_endpoint.rb | 6 +- app/models/nil_solr_endpoint.rb | 22 +- app/models/redis_endpoint.rb | 16 +- app/models/role.rb | 10 +- app/models/search_builder.rb | 4 + app/models/site.rb | 38 +- app/models/solr_endpoint.rb | 3 + app/models/user.rb | 23 +- .../iiif_av/displays_content_decorator.rb | 154 +- .../hyku/admin/group/navigation_presenter.rb | 118 +- .../hyku/collapsable_section_presenter.rb | 35 +- app/presenters/hyku/menu_presenter.rb | 16 +- app/presenters/hyku/work_show_presenter.rb | 34 +- .../hyrax/admin/dashboard_presenter.rb | 28 - .../admin/dashboard_presenter_decorator.rb | 18 + app/presenters/hyrax/admin/users_presenter.rb | 8 +- .../hyrax/admin/workflow_roles_presenter.rb | 58 - .../workflow_roles_presenter_decorator.rb | 24 + .../hyrax/collection_presenter_decorator.rb | 44 +- app/presenters/hyrax/homepage_presenter.rb | 74 - .../hyrax/homepage_presenter_decorator.rb | 29 + .../iiif_manifest_presenter_decorator.rb | 4 +- app/search_builders/adv_search_builder.rb | 24 + ...lection_member_search_builder_decorator.rb | 2 +- app/services/create_account.rb | 12 +- app/services/group_aware_role_checker.rb | 22 +- .../admin_set_create_service_decorator.rb | 18 +- ...service.rb => create_service_decorator.rb} | 152 +- .../permissions_create_service_decorator.rb | 8 +- .../permissions_service_decorator.rb | 2 +- .../file_set_derivatives_service_decorator.rb | 56 + app/services/hyrax/indexes_thumbnails.rb | 37 - .../hyrax/indexes_thumbnails_decorator.rb | 18 + .../manifest_builder_service_decorator.rb | 32 +- .../hyrax/permission_manager_decorator.rb | 4 +- .../hyrax/quick_classification_query.rb | 26 - .../quick_classification_query_decorator.rb | 25 + app/services/hyrax/thumbnail_path_service.rb | 71 - .../hyrax/thumbnail_path_service_decorator.rb | 13 + .../hyrax/work_thumbnail_path_service.rb | 12 - .../work_thumbnail_path_service_decorator.rb | 13 + .../hyrax/workflow/permission_grantor.rb | 92 +- .../hyrax/workflow/permission_query.rb | 450 ------ app/services/roles_service.rb | 32 +- ...oaded_collection_thumbnail_path_service.rb | 6 +- app/views/_admin_util_links.html.erb | 16 +- app/views/_logo.html.erb | 20 +- app/views/_masthead.html.erb | 26 +- app/views/_user_util_links.html.erb | 36 +- app/views/account_sign_up/new.html.erb | 10 +- app/views/admin/accounts/edit.html.erb | 10 +- app/views/admin/groups/_form.html.erb | 8 +- app/views/admin/groups/_nav.html.erb | 8 +- app/views/admin/groups/_per_page.html.erb | 4 +- app/views/admin/groups/_search.html.erb | 8 +- app/views/admin/groups/index.html.erb | 20 +- app/views/admin/groups/new.html.erb | 18 +- app/views/admin/groups/remove.html.erb | 10 +- app/views/admin/groups/roles.html.erb | 119 +- app/views/admin/groups/users.html.erb | 28 +- app/views/admin/work_types/edit.html.erb | 12 +- .../advanced/_advanced_search_help.html.erb | 20 + .../_range_limit_panel.html.erb | 125 ++ ..._index_gallery_collection_wrapper.html.erb | 4 +- ...efault.html.erb => _index_header.html.erb} | 12 +- .../_index_header_list_collection.html.erb | 8 +- .../_index_header_list_default.html.erb | 16 +- .../catalog/_index_masonry_default.html.erb | 2 - .../_thumbnail_list_collection.html.erb | 2 +- app/views/devise/passwords/new.html.erb | 10 +- app/views/devise/registrations/new.html.erb | 28 +- .../registrations/registrations/new.html.erb | 36 +- app/views/devise/sessions/new.html.erb | 49 +- .../_form_participant_table.html.erb | 74 +- .../admin_sets/_form_participants.html.erb | 24 +- .../appearances/_banner_image_form.html.erb | 13 +- .../admin/appearances/_color_input.html.erb | 2 +- .../appearances/_custom_css_form.html.erb | 8 +- .../appearances/_default_colors_form.html.erb | 12 +- .../appearances/_default_fonts_form.html.erb | 16 +- .../appearances/_default_images_form.html.erb | 14 +- .../_directory_image_form.html.erb | 12 +- .../admin/appearances/_favicon_form.html.erb | 12 +- .../appearances/_logo_image_form.html.erb | 12 +- .../admin/appearances/_theme_form.html.erb | 16 +- .../hyrax/admin/appearances/show.html.erb | 56 +- .../_form_participants.html.erb | 10 +- app/views/hyrax/admin/stats/show.html.erb | 41 +- app/views/hyrax/admin/users/index.html.erb | 14 +- .../hyrax/admin/workflow_roles/index.html.erb | 42 +- app/views/hyrax/base/_form_share.html.erb | 8 +- app/views/hyrax/base/_show_actions.html.erb | 18 +- app/views/hyrax/base/show.html.erb | 24 +- app/views/hyrax/contact_form/new.html.erb | 36 +- app/views/hyrax/content_blocks/_form.html.erb | 116 +- app/views/hyrax/dashboard/_sidebar.html.erb | 12 +- .../collections/_current_thumbnail.html.erb | 2 +- .../dashboard/collections/_form.html.erb | 54 +- .../collections/_form_branding.html.erb | 45 +- .../collections/_form_discovery.html.erb | 35 +- .../collections/_form_share.html.erb | 141 +- .../collections/_form_share_table.html.erb | 75 +- .../collections/_list_collections.html.erb | 4 +- .../_show_add_items_actions.html.erb | 4 +- .../_show_document_list_row.html.erb | 28 +- .../_show_parent_collection_row.html.erb | 6 +- .../collections/_subcollection_list.html.erb | 6 +- .../dashboard/sidebar/_activity.html.erb | 142 +- .../dashboard/sidebar/_configuration.html.erb | 27 +- .../sidebar/_repository_content.html.erb | 6 +- .../hyrax/dashboard/sidebar/_tasks.html.erb | 9 +- .../dashboard/works/_list_works.html.erb | 11 +- .../homepage/_explore_collections.html.erb | 5 +- .../_featured_collection_fields.html.erb | 24 +- .../_featured_collection_section.html.erb | 21 +- .../hyrax/homepage/_featured_fields.html.erb | 4 +- .../hyrax/homepage/_home_content.html.erb | 41 +- app/views/hyrax/homepage/_home_text.html.erb | 12 +- .../_sortable_featured_collections.html.erb | 10 +- .../hyrax/my/_collection_action_menu.html.erb | 15 +- app/views/hyrax/my/_work_action_menu.html.erb | 26 +- .../my/collections/_list_collections.html.erb | 4 +- app/views/hyrax/my/works/_list_works.html.erb | 14 +- .../uploads/_js_templates_branding.html.erb | 24 +- app/views/hyrax/users/_vitals.html.erb | 21 - app/views/identity_providers/_form.html.erb | 23 +- app/views/identity_providers/index.html.erb | 23 +- app/views/labels/edit.html.erb | 41 +- app/views/layouts/application.html.erb | 2 +- app/views/layouts/hyrax.html.erb | 8 +- app/views/proprietor/accounts/edit.html.erb | 22 +- app/views/proprietor/accounts/index.html.erb | 12 +- app/views/proprietor/accounts/new.html.erb | 50 +- app/views/proprietor/accounts/show.html.erb | 30 +- app/views/proprietor/users/_form.html.erb | 2 +- app/views/proprietor/users/edit.html.erb | 4 +- app/views/proprietor/users/index.html.erb | 14 +- app/views/proprietor/users/new.html.erb | 34 +- app/views/proprietor/users/show.html.erb | 8 +- .../records/show_fields/_license.html.erb | 4 + app/views/roles/index.html.erb | 36 - app/views/shared/_footer.html.erb | 12 +- app/views/single_signon/index.html.erb | 36 +- app/views/splash/index.html.erb | 8 +- app/views/status/index.html.erb | 16 +- .../_collections_section.html.erb | 6 +- .../cultural_repository/_facets.html.erb | 14 +- ..._featured_works_home_text_section.html.erb | 18 +- .../cultural_repository/_home_text.html.erb | 2 +- .../themes/cultural_repository/_logo.html.erb | 22 +- .../cultural_repository/_masthead.html.erb | 47 +- .../_recent_works_section.html.erb | 8 +- .../_user_util_links.html.erb | 51 +- .../catalog/_search_form.html.erb | 30 +- .../homepage/_explore_collections.html.erb | 22 +- .../hyrax/homepage/_facet_layout.html.erb | 13 +- .../hyrax/homepage/_featured.html.erb | 16 +- .../hyrax/homepage/_featured_fields.html.erb | 4 +- .../hyrax/homepage/_featured_works.html.erb | 8 +- .../hyrax/homepage/_marketing.html.erb | 8 +- .../hyrax/homepage/_recent_document.html.erb | 11 +- .../homepage/_recently_uploaded.html.erb | 6 +- .../layouts/_share_your_work_row.html.erb | 16 +- .../layouts/homepage.html.erb | 10 +- .../layouts/hyrax.html.erb | 22 +- .../hyrax/base/_work_title.html.erb | 2 +- .../cultural_show/hyrax/base/show.html.erb | 26 +- .../_controls.html.erb | 42 +- .../_home_text.html.erb | 6 +- .../_ir_below_stats_layout.html.erb | 8 +- .../institutional_repository/_logo.html.erb | 22 +- .../_masthead.html.erb | 45 +- .../_user_util_links.html.erb | 32 +- .../catalog/_search_form.html.erb | 28 +- .../homepage/_explore_collections.html.erb | 22 +- .../homepage/_featured_researcher.html.erb | 4 +- .../homepage/_ir_homepage_stats.html.erb | 6 +- .../hyrax/homepage/_marketing.html.erb | 4 +- .../hyrax/homepage/_recent_document.html.erb | 6 +- .../homepage/_recently_uploaded.html.erb | 4 +- .../homepage/_resource_type_slider.html.erb | 28 +- .../homepage/_resource_type_stats.html.erb | 8 +- .../hyrax/homepage/all_collections.html.erb | 8 +- .../layouts/homepage.html.erb | 12 +- .../layouts/hyrax.html.erb | 18 +- .../_featured_carousel.html.erb | 28 +- .../neutral_repository/_home_text.html.erb | 6 +- .../_theme_container.html.erb | 56 +- .../homepage/_explore_collections.html.erb | 8 +- .../homepage/_featured_researcher.html.erb | 4 +- .../hyrax/homepage/_featured_works.html.erb | 10 +- .../hyrax/homepage/_recent_document.html.erb | 11 +- .../homepage/_recently_uploaded.html.erb | 2 +- .../layouts/homepage.html.erb | 4 +- .../hyrax/base/_work_title.html.erb | 4 +- .../scholarly_show/hyrax/base/show.html.erb | 28 +- bin/bundle | 2 + bin/extract_mods_to_spreadsheet | 71 - bin/git-cleanup | 7 +- bin/graph | 14 +- bin/import_from_csv | 49 - bin/import_from_purl | 64 - bin/import_mods_files | 60 - bin/rails | 1 + bin/rake | 1 + bin/setup | 1 + bin/spring | 1 + bin/update | 1 + bin/web | 5 +- bin/worker | 19 +- config/analytics.yml | 4 + config/application.rb | 133 +- config/boot.rb | 1 + config/environment.rb | 1 + config/environments/development.rb | 17 +- config/environments/production.rb | 12 +- config/environments/test.rb | 1 + config/initializers/0db_created.rb | 5 +- config/initializers/active_fedora_override.rb | 11 - config/initializers/apartment.rb | 15 +- config/initializers/apartment_activejob.rb | 1 + .../application_controller_renderer.rb | 1 + config/initializers/assets.rb | 1 + config/initializers/backtrace_silencers.rb | 1 + config/initializers/bulkrax.rb | 25 +- config/initializers/clamav.rb | 1 + config/initializers/cookies_serializer.rb | 1 + config/initializers/devise.rb | 11 +- .../file_set_derivatives_overrides.rb | 59 - .../initializers/filter_parameter_logging.rb | 1 + config/initializers/good_job.rb | 37 + config/initializers/hydra_config.rb | 1 + config/initializers/hyrax.rb | 24 +- config/initializers/iiif_print.rb | 1 + config/initializers/inflections.rb | 1 + config/initializers/mailboxer.rb | 16 +- config/initializers/mime_types.rb | 3 +- config/initializers/mini_magick.rb | 1 + config/initializers/new_framework_defaults.rb | 1 + config/initializers/redis_config.rb | 17 +- config/initializers/riiif.rb | 3 +- config/initializers/rolify.rb | 3 +- config/initializers/secure_headers.rb | 1 + config/initializers/session_store.rb | 1 + config/initializers/sidekiq.rb | 18 +- config/initializers/simple_form.rb | 2 +- config/initializers/simple_form_bootstrap.rb | 4 + config/initializers/stats_admin.rb | 2 + config/initializers/version.rb | 4 +- config/initializers/wrap_parameters.rb | 2 + config/locales/de.yml | 2 +- config/locales/en.yml | 7 +- config/locales/es.yml | 2 +- config/locales/fr.yml | 2 +- config/locales/hyrax.de.yml | 2 - config/locales/hyrax.en.yml | 2 - config/locales/hyrax.es.yml | 2 - config/locales/hyrax.fr.yml | 2 - config/locales/hyrax.it.yml | 2 - config/locales/hyrax.pt-BR.yml | 2 - config/locales/hyrax.zh.yml | 2 - config/locales/it.yml | 2 +- config/locales/pt-BR.yml | 2 +- config/locales/zh.yml | 2 +- config/puma.rb | 1 + config/routes.rb | 27 +- config/spring.rb | 5 +- config/uv/uv.html | 27 +- .../20230406183810_setup_good_jobs_schema.rb | 16 + db/migrate/20230406183814_create_good_jobs.rb | 38 + ...5705_create_hyrax_counter_metrics.hyrax.rb | 14 + ...15215706_change_work_id_to_string.hyrax.rb | 5 + ..._indices_to_hyrax_counter_metrics.hyrax.rb | 8 + ...5708_add_fields_to_counter_metric.hyrax.rb | 8 + db/schema.rb | 62 +- db/seeds.rb | 2 +- docker-compose.yml | 5 +- lib/active_fedora/solr_service_decorator.rb | 2 +- lib/active_job_tenant.rb | 22 +- .../derivatives/processors/image_decorator.rb | 28 - .../canvas_builder_decorator.rb | 4 +- .../canvas_builder_decorator.rb | 21 + lib/importer.rb | 11 - lib/importer/csv_importer.rb | 53 - lib/importer/csv_parser.rb | 153 -- lib/importer/factory.rb | 21 - lib/importer/factory/collection_factory.rb | 25 - lib/importer/factory/etd_factory.rb | 18 - lib/importer/factory/image_factory.rb | 18 - lib/importer/factory/object_factory.rb | 157 -- .../factory/string_literal_processor.rb | 26 - .../factory/with_associated_collection.rb | 29 - lib/importer/log_subscriber.rb | 39 - lib/importer/mods_importer.rb | 46 - lib/importer/mods_parser.rb | 311 ---- .../metadata_format/hyku_dublin_core.rb | 6 + lib/oai/provider/model_decorator.rb | 4 +- .../list_metadata_formats_decorator.rb | 4 +- lib/omni_auth/strategies/saml_decorator.rb | 2 + lib/stanford.rb | 6 - lib/stanford/importer.rb | 10 - lib/stanford/importer/mods_parser.rb | 43 - lib/stanford/importer/purl_parser.rb | 176 --- lib/stanford/importer/purl_retriever.rb | 32 - lib/tasks/rubocop.rake | 3 + lib/tasks/tenantize_task.rake | 4 +- .../find_ids_by_model_decorator.rb | 17 +- ops/exiftool_image_to_fits.xslt | 100 +- spec/abilities/collection_ability_spec.rb | 24 +- spec/abilities/solr_document_ability_spec.rb | 4 +- .../hyrax/environment_decorator_spec.rb | 26 + spec/config/application_spec.rb | 42 + .../admin/accounts_controller_spec.rb | 2 +- spec/controllers/catalog_controller_spec.rb | 2 +- .../hyrax/generic_works_controller_spec.rb | 6 +- .../hyrax/homepage_controller_spec.rb | 129 +- .../hyrax/images_controller_spec.rb | 4 +- .../proprietor/accounts_controller_spec.rb | 4 +- .../proprietor/users_controller_spec.rb | 2 +- spec/controllers/roles_controller_spec.rb | 69 - .../search_history_controller_spec.rb | 34 + spec/factories/accounts.rb | 4 +- spec/factories/admin_sets.rb | 4 +- spec/factories/collection_types.rb | 8 +- spec/factories/collections.rb | 136 +- spec/factories/permission_templates.rb | 38 +- spec/features/admin_dashboard_spec.rb | 8 +- .../admin_set_form_participants_tab_spec.rb | 27 +- spec/features/appearance_theme_spec.rb | 12 +- .../features/assign_workflow_to_group_spec.rb | 4 +- spec/features/collection_editor_role_spec.rb | 2 +- spec/features/collection_manager_role_spec.rb | 6 +- spec/features/collection_reader_role_spec.rb | 2 +- spec/features/collection_spec.rb | 22 +- spec/features/collection_type_spec.rb | 15 +- spec/features/create_generic_work_spec.rb | 10 +- spec/features/create_image_spec.rb | 10 +- spec/features/create_work_spec.rb | 2 +- .../cultural_repository_theme_spec.rb | 11 +- spec/features/featured_collections_spec.rb | 6 +- .../institutional_repository_theme_spec.rb | 10 +- spec/features/oai_pmh_spec.rb | 4 +- spec/features/roles_spec.rb | 31 - spec/features/show_page_theme_spec.rb | 6 +- .../work_approval_permissions_spec.rb | 2 +- spec/features/work_editor_role_spec.rb | 6 +- ...work_search_institution_visibility_spec.rb | 2 +- .../work_show_institution_visibility_spec.rb | 2 +- .../work_show_open_visibility_spec.rb | 2 +- .../work_show_private_visibility_spec.rb | 2 +- .../fixtures/mods/shpc/druid_xv169dn4538.mods | 46 - spec/fixtures/mods/shpc/kx532cb7981.mods | 94 -- spec/fixtures/purl/bc390xk2647.xml | 62 - .../forms/admin/appearance_decorator_spec.rb | 33 + .../forms/permission_template_form_spec.rb | 31 +- ...flow_responsibility_form_decorator_spec.rb | 21 + spec/helpers/shared_search_helper_spec.rb | 2 +- spec/indexers/app_indexer_spec.rb | 2 +- spec/jobs/cleanup_account_job_spec.rb | 22 +- .../jobs/create_default_admin_set_job_spec.rb | 5 +- spec/jobs/import_work_from_purl_job_spec.rb | 153 -- spec/lib/importer/csv_importer_spec.rb | 46 - spec/lib/importer/csv_parser_spec.rb | 85 -- spec/lib/importer/factory/etd_factory_spec.rb | 36 - .../importer/factory/image_factory_spec.rb | 36 - .../factory/string_literal_processor_spec.rb | 38 - spec/lib/importer/mods_importer_spec.rb | 79 - spec/lib/importer/mods_parser_spec.rb | 238 --- .../lib/stanford/importer/mods_parser_spec.rb | 29 - .../lib/stanford/importer/purl_parser_spec.rb | 22 - spec/models/account_spec.rb | 10 +- spec/models/collection_spec.rb | 4 +- spec/models/fcrepo_endpoint_spec.rb | 21 +- spec/models/featured_collection_list_spec.rb | 8 +- spec/models/hyrax/group_spec.rb | 28 +- spec/models/redis_endpoint_spec.rb | 20 +- spec/models/solr_endpoint_spec.rb | 9 + spec/models/uploaded_file_spec.rb | 6 +- .../admin/group/navigation_presenter_spec.rb | 24 +- spec/presenters/hyku/menu_presenter_spec.rb | 14 +- .../dashboard_presenter_decorator_spec.rb | 21 + ...workflow_roles_presenter_decorator_spec.rb | 24 + .../collection_presenter_decorator_spec.rb | 10 + spec/rails_helper.rb | 31 +- spec/requests/catalog_controller_spec.rb | 18 +- spec/requests/institution_visibility_spec.rb | 2 +- .../work_approval_permissions_spec.rb | 18 +- spec/requests/work_depositor_role_spec.rb | 6 +- spec/requests/work_editor_role_spec.rb | 4 +- .../work_show_institution_visibility_spec.rb | 4 +- .../work_show_open_visibility_spec.rb | 2 +- .../work_show_private_visibility_spec.rb | 2 +- .../featured_collections_route_spec.rb | 4 +- spec/routing/roles_routing_spec.rb | 16 - .../adv_search_builder_spec.rb | 63 + spec/services/create_account_spec.rb | 5 +- .../create_service_decorator_spec.rb | 29 + .../indexes_thumbnails_decorator_spec.rb | 25 + ...ick_classification_query_decorator_spec.rb | 25 + .../hyrax/thumbnail_path_service_spec.rb | 21 + ...k_thumbnail_path_service_decorator_spec.rb | 21 + .../hyrax/workflow/permission_grantor_spec.rb | 6 +- .../hyrax/workflow/permission_query_spec.rb | 257 ---- spec/services/roles_service_spec.rb | 12 +- spec/support/fixtures.rb | 13 - .../shared_examples_for_csv_importer.rb | 67 - spec/tasks/rake_spec.rb | 2 +- .../account_sign_up/new.html.erb_spec.rb | 2 +- spec/views/admin/groups/edit.html.erb_spec.rb | 6 +- spec/views/admin/groups/new.html.erb_spec.rb | 6 +- .../admin/groups/remove.html.erb_spec.rb | 6 +- .../views/admin/groups/users.html.erb_spec.rb | 6 +- .../hyrax/admin/users/index.html.erb_spec.rb | 12 +- .../content_blocks/edit.html.erb_spec.rb | 10 +- .../collections/_form_share.erb_spec.rb | 4 +- .../_form_share_table.html.erb_spec.rb | 8 +- .../proprietor/accounts/new.html.erb_spec.rb | 2 +- .../proprietor/users/new.html.erb_spec.rb | 2 +- .../proprietor/users/show.html.erb_spec.rb | 2 +- 527 files changed, 7313 insertions(+), 10452 deletions(-) create mode 100644 .rubocop_fixme.yml rename app/actors/hyrax/{environment.rb => environment_decorator.rb} (51%) create mode 100644 app/assets/javascripts/admin_color_select.js create mode 100644 app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js create mode 100644 app/assets/javascripts/blacklight_range_limit/range_limit_shared.js create mode 100644 app/assets/javascripts/blacklight_range_limit/range_limit_slider.js create mode 100644 app/controllers/concerns/hyku/works_controller_behavior.rb delete mode 100644 app/controllers/concerns/hyrax/works_controller_behavior.rb delete mode 100644 app/controllers/hyrax/admin/appearances_controller.rb create mode 100644 app/controllers/hyrax/admin/appearances_controller_decorator.rb delete mode 100644 app/controllers/hyrax/admin/workflow_roles_controller.rb delete mode 100644 app/controllers/hyrax/contact_form_controller.rb create mode 100644 app/controllers/hyrax/contact_form_controller_decorator.rb delete mode 100644 app/controllers/hyrax/content_blocks_controller.rb create mode 100644 app/controllers/hyrax/content_blocks_controller_decorator.rb delete mode 100644 app/controllers/hyrax/dashboard/collections_controller.rb create mode 100644 app/controllers/hyrax/dashboard/collections_controller_decorator.rb delete mode 100644 app/controllers/hyrax/my/works_controller_decorator.rb delete mode 100644 app/controllers/hyrax/pages_controller.rb create mode 100644 app/controllers/hyrax/pages_controller_decorator.rb delete mode 100644 app/controllers/roles_controller.rb create mode 100644 app/controllers/saved_searches_controller.rb create mode 100644 app/controllers/search_history_controller.rb rename app/forms/hyrax/forms/admin/{appearance.rb => appearance_decorator.rb} (55%) create mode 100644 app/forms/hyrax/forms/workflow_responsibility_form_decorator.rb delete mode 100644 app/helpers/blacklight/blacklight_helper_behavior.rb create mode 100644 app/helpers/hyku/blacklight_helper_behavior.rb delete mode 100644 app/jobs/import_work_from_purl_job.rb delete mode 100644 app/models/hyrax/contact_form.rb create mode 100644 app/models/hyrax/contact_form_decorator.rb delete mode 100644 app/presenters/hyrax/admin/dashboard_presenter.rb create mode 100644 app/presenters/hyrax/admin/dashboard_presenter_decorator.rb delete mode 100644 app/presenters/hyrax/admin/workflow_roles_presenter.rb create mode 100644 app/presenters/hyrax/admin/workflow_roles_presenter_decorator.rb delete mode 100644 app/presenters/hyrax/homepage_presenter.rb create mode 100644 app/presenters/hyrax/homepage_presenter_decorator.rb create mode 100644 app/search_builders/adv_search_builder.rb rename app/services/hyrax/collection_types/{create_service.rb => create_service_decorator.rb} (52%) create mode 100644 app/services/hyrax/file_set_derivatives_service_decorator.rb delete mode 100644 app/services/hyrax/indexes_thumbnails.rb create mode 100644 app/services/hyrax/indexes_thumbnails_decorator.rb delete mode 100644 app/services/hyrax/quick_classification_query.rb create mode 100644 app/services/hyrax/quick_classification_query_decorator.rb delete mode 100644 app/services/hyrax/thumbnail_path_service.rb create mode 100644 app/services/hyrax/thumbnail_path_service_decorator.rb delete mode 100644 app/services/hyrax/work_thumbnail_path_service.rb create mode 100644 app/services/hyrax/work_thumbnail_path_service_decorator.rb delete mode 100644 app/services/hyrax/workflow/permission_query.rb create mode 100644 app/views/advanced/_advanced_search_help.html.erb create mode 100644 app/views/blacklight_range_limit/_range_limit_panel.html.erb rename app/views/catalog/{_index_header_default.html.erb => _index_header.html.erb} (83%) delete mode 100644 app/views/catalog/_index_masonry_default.html.erb delete mode 100644 app/views/hyrax/users/_vitals.html.erb create mode 100644 app/views/records/show_fields/_license.html.erb delete mode 100644 app/views/roles/index.html.erb delete mode 100755 bin/extract_mods_to_spreadsheet delete mode 100755 bin/import_from_csv delete mode 100755 bin/import_from_purl delete mode 100755 bin/import_mods_files delete mode 100644 config/initializers/active_fedora_override.rb delete mode 100644 config/initializers/file_set_derivatives_overrides.rb create mode 100644 config/initializers/good_job.rb create mode 100644 db/migrate/20230406183810_setup_good_jobs_schema.rb create mode 100644 db/migrate/20230406183814_create_good_jobs.rb create mode 100644 db/migrate/20231215215705_create_hyrax_counter_metrics.hyrax.rb create mode 100644 db/migrate/20231215215706_change_work_id_to_string.hyrax.rb create mode 100644 db/migrate/20231215215707_add_indices_to_hyrax_counter_metrics.hyrax.rb create mode 100644 db/migrate/20231215215708_add_fields_to_counter_metric.hyrax.rb delete mode 100644 lib/hydra/derivatives/processors/image_decorator.rb create mode 100644 lib/iiif_manifest/v3/manifest_builder/canvas_builder_decorator.rb delete mode 100644 lib/importer.rb delete mode 100644 lib/importer/csv_importer.rb delete mode 100644 lib/importer/csv_parser.rb delete mode 100644 lib/importer/factory.rb delete mode 100644 lib/importer/factory/collection_factory.rb delete mode 100644 lib/importer/factory/etd_factory.rb delete mode 100644 lib/importer/factory/image_factory.rb delete mode 100644 lib/importer/factory/object_factory.rb delete mode 100644 lib/importer/factory/string_literal_processor.rb delete mode 100644 lib/importer/factory/with_associated_collection.rb delete mode 100644 lib/importer/log_subscriber.rb delete mode 100644 lib/importer/mods_importer.rb delete mode 100644 lib/importer/mods_parser.rb delete mode 100644 lib/stanford.rb delete mode 100644 lib/stanford/importer.rb delete mode 100644 lib/stanford/importer/mods_parser.rb delete mode 100644 lib/stanford/importer/purl_parser.rb delete mode 100644 lib/stanford/importer/purl_retriever.rb create mode 100644 spec/actors/hyrax/environment_decorator_spec.rb create mode 100644 spec/config/application_spec.rb delete mode 100644 spec/controllers/roles_controller_spec.rb create mode 100644 spec/controllers/search_history_controller_spec.rb delete mode 100644 spec/features/roles_spec.rb delete mode 100644 spec/fixtures/mods/shpc/druid_xv169dn4538.mods delete mode 100644 spec/fixtures/mods/shpc/kx532cb7981.mods delete mode 100644 spec/fixtures/purl/bc390xk2647.xml create mode 100644 spec/forms/hyrax/forms/admin/appearance_decorator_spec.rb create mode 100644 spec/forms/hyrax/forms/workflow_responsibility_form_decorator_spec.rb delete mode 100644 spec/jobs/import_work_from_purl_job_spec.rb delete mode 100644 spec/lib/importer/csv_importer_spec.rb delete mode 100644 spec/lib/importer/csv_parser_spec.rb delete mode 100644 spec/lib/importer/factory/etd_factory_spec.rb delete mode 100644 spec/lib/importer/factory/image_factory_spec.rb delete mode 100644 spec/lib/importer/factory/string_literal_processor_spec.rb delete mode 100644 spec/lib/importer/mods_importer_spec.rb delete mode 100644 spec/lib/importer/mods_parser_spec.rb delete mode 100644 spec/lib/stanford/importer/mods_parser_spec.rb delete mode 100644 spec/lib/stanford/importer/purl_parser_spec.rb create mode 100644 spec/presenters/hyrax/admin/dashboard_presenter_decorator_spec.rb create mode 100644 spec/presenters/hyrax/admin/workflow_roles_presenter_decorator_spec.rb create mode 100644 spec/presenters/hyrax/collection_presenter_decorator_spec.rb delete mode 100644 spec/routing/roles_routing_spec.rb create mode 100644 spec/search_builders/adv_search_builder_spec.rb create mode 100644 spec/services/hyrax/collection_types/create_service_decorator_spec.rb create mode 100644 spec/services/hyrax/indexes_thumbnails_decorator_spec.rb create mode 100644 spec/services/hyrax/quick_classification_query_decorator_spec.rb create mode 100644 spec/services/hyrax/thumbnail_path_service_spec.rb create mode 100644 spec/services/hyrax/work_thumbnail_path_service_decorator_spec.rb delete mode 100644 spec/services/hyrax/workflow/permission_query_spec.rb delete mode 100644 spec/support/fixtures.rb delete mode 100644 spec/support/shared_examples_for_csv_importer.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 5f08b5735c..21ed03a193 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: samvera: samvera/circleci-orb@1 - browser-tools: circleci/browser-tools@1.3 + browser-tools: circleci/browser-tools@1.4.6 ruby: circleci/ruby@2 node: circleci/node@5 @@ -10,13 +10,13 @@ jobs: parameters: ruby_version: type: string - default: 2.7.8 + default: 3.2.2 bundler_version: type: string default: 2.4.8 rails_version: type: string - default: '5.1.6' + default: '6.1.7.6' solr_config_path: type: string fcrepo_version: @@ -61,6 +61,7 @@ jobs: BUNDLE_PATH: vendor/bundle BUNDLE_JOBS: 4 BUNDLE_RETRY: 3 + HYKU_CACHE_ROOT: /tmp/hyku-cache HYKU_RESTRICT_CREATE_AND_DESTROY_PERMISSIONS: 'true' SOLR_ADMIN_USER: admin SOLR_ADMIN_PASSWORD: admin @@ -79,7 +80,9 @@ jobs: ruby_version: << parameters.ruby_version >> bundler_version: << parameters.bundler_version >> - samvera/rubocop - - browser-tools/install-chrome + - browser-tools/install-chrome: + chrome-version: 114.0.5735.90 # see https://github.com/CircleCI-Public/browser-tools-orb/pull/96 + replace-existing: true - browser-tools/install-chromedriver - run: name: Check Chrome install @@ -95,6 +98,6 @@ workflows: ci: jobs: - bundle: - ruby_version: "2.7.8" - name: "ruby2-7-8" + ruby_version: "3.2.2" + name: "ruby3-2-2" solr_config_path: 'solr/conf' diff --git a/.dockerignore b/.dockerignore index 45c1f98e18..e07a10c042 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,6 @@ ./coverage ./docker ./fcrepo -./importer ./log ./pg ./public/assets diff --git a/.env b/.env index d915fdd7a1..8c4081a828 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ CHROME_HOSTNAME=chrome COMPOSE_DOCKER_CLI_BUILD=1 +DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true DB_ADAPTER=postgresql DB_HOST=db DB_HOST=db @@ -13,18 +14,18 @@ FCREPO_BASE_PATH=/hykudemo FCREPO_HOST=fcrepo FCREPO_PORT=8080 FCREPO_REST_PATH=rest +HYRAX_ACTIVE_JOB_QUEUE=sidekiq +HYRAX_FITS_PATH=/app/fits/fits.sh INITIAL_ADMIN_EMAIL=admin@example.com INITIAL_ADMIN_PASSWORD=testing123 -JAVA_OPTS=-Xmx4g -Xms1g IN_DOCKER=true +JAVA_OPTS=-Xmx4g -Xms1g LD_LIBRARY_PATH=/opt/fits/tools/mediainfo/linux +NEGATIVE_CAPTCHA_SECRET=default-value-change-me PASSENGER_APP_ENV=development RAILS_LOG_TO_STDOUT=true REDIS_HOST=redis SECRET_KEY_BASE=asdf -HYRAX_ACTIVE_JOB_QUEUE=sidekiq -HYRAX_FITS_PATH=/app/fits/fits.sh -NEGATIVE_CAPTCHA_SECRET=default-value-change-me SOLR_ADMIN_PASSWORD=SolrRocks SOLR_ADMIN_USER=solr SOLR_COLLECTION_NAME=hydra-development @@ -32,6 +33,8 @@ SOLR_CONFIGSET_NAME=hyku SOLR_HOST=solr SOLR_PORT=8983 SOLR_URL=http://solr:SolrRocks@solr:8983/solr/ +TB_RSPEC_FORMATTER=progress +TB_RSPEC_OPTIONS="--format RspecJunitFormatter --out rspec.xml" # Comment out these 5 for single tenancy / Uncomment for multi HYKU_ADMIN_HOST=hyku.test diff --git a/.rubocop.yml b/.rubocop.yml index 90961c9599..fc1044e12b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,129 +1,147 @@ -# Turn on RSpec cops -require: rubocop-rspec -inherit_from: .rubocop_todo.yml +inherit_from: + - .rubocop_fixme.yml -# With the rubocop 0.47.0 and rubocop-rspec 1.8.0 the following stderr message was sent: -# An error occurred while RSpec/DescribedClass cop was inspecting path/to/file -RSpec/DescribedClass: - Enabled: false +inherit_gem: + bixby: bixby_default.yml AllCops: + NewCops: disable + TargetRubyVersion: 3.2.2 DisplayCopNames: true - TargetRubyVersion: 2.3 Exclude: - - 'bin/**/*' - 'db/**/*' - - 'config/**/*' - - 'vendor/**/*' - - '.internal_test_app/**/*' - - 'spec/fixtures/**/*' - - 'spec/internal/**/*' + - 'script/**/*' - 'spec/test_app_templates/**/*' - - 'Rakefile' - - 'lib/tasks/rubocop.rake' - # disabling collections controller as most of the rubocop errors are in hyrax - - 'app/controllers/hyrax/dashboard/collections_controller.rb' - -Rails: - Enabled: true + - 'vendor/**/*' + - 'lib/hyrax/specs/**/*' + - 'bin/graph' + - 'bin/import_from_purl' -Rails/DynamicFindBy: - Whitelist: - - find_by_user_key +Lint/ImplicitStringConcatenation: Exclude: - - 'lib/importer/factory/object_factory.rb' + - 'lib/generators/hyrax/**/*' -Rails/UnknownEnv: - Environments: - - development - - production - - staging - - test +Rails/Output: + Exclude: + - 'lib/generators/**/*' -Metrics/LineLength: - Max: 120 +Metrics/BlockLength: + IgnoredMethods: ['included'] Exclude: - - 'app/controllers/catalog_controller.rb' - - 'spec/controllers/curation_concerns/generic_works_controller_spec.rb' - - 'spec/services/iiif_collection_thumbnail_path_service_spec.rb' - - 'spec/services/iiif_work_thumbnail_path_service_spec.rb' - - 'spec/routing/proprietor/accounts_routing_spec.rb' + - 'hyrax.gemspec' + - 'app/models/concerns/hyrax/content_block_behavior.rb' + - 'app/services/hyrax/workflow/workflow_schema.rb' + - 'config/initializers/simple_form.rb' + - 'config/features.rb' + - 'config/routes.rb' + - 'lib/generators/hyrax/templates/catalog_controller.rb' + - 'lib/generators/hyrax/templates/config/initializers/simple_form_bootstrap.rb' + - 'lib/hyrax/rails/routes.rb' + - 'lib/tasks/*.rake' + - 'spec/**/*.rb' + - 'tasks/benchmark.rake' -Layout/IndentationConsistency: - EnforcedStyle: rails +Style/AsciiComments: + Enabled: false -Layout/DotPosition: - EnforcedStyle: leading +# rubocop suggests !thing.nil? instead, but that is NOT equivalent +Style/DoubleNegation: + Enabled: false -Style/Documentation: +Style/CollectionMethods: + PreferredMethods: + collect: 'map' + collect!: 'map!' + inject: 'reduce' + detect: 'find' + find_all: 'select' + +Style/SymbolArray: Enabled: false -Style/StringLiterals: +Style/ClassAndModuleChildren: Enabled: false -Style/WordArray: +Style/SingleLineBlockParams: Enabled: false -Metrics/ClassLength: - Exclude: - - 'app/controllers/catalog_controller.rb' +Rails/ApplicationJob: + Enabled: false + +Rails/ApplicationMailer: + Enabled: false -Metrics/ModuleLength: - Max: 200 +Rails/ApplicationRecord: + Enabled: false Rails/HasAndBelongsToMany: Exclude: - 'app/models/role.rb' -RSpec/AnyInstance: +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/endpoint.rb' + +Rails/RakeEnvironment: Enabled: false -RSpec/InstanceVariable: +# We define custom methods like `find_by_user_key`, +# `find_by_created_date`, etc +Rails/DynamicFindBy: + Enabled: false + +Rails/FilePath: Exclude: - - 'spec/controllers/hyku/registrations_controller_spec.rb' + - 'spec/abilities/**/*' -RSpec/NamedSubject: - Enabled: false +Rails/OutputSafety: + Exclude: + - 'app/builders/hyrax/form_builder.rb' + - 'app/helpers/hyrax/citations_behaviors/formatters/apa_formatter.rb' + - 'app/helpers/hyrax/citations_behaviors/formatters/chicago_formatter.rb' + - 'app/helpers/hyrax/citations_behaviors/formatters/mla_formatter.rb' + - 'app/helpers/hyrax/collections_helper.rb' + - 'app/helpers/hyrax/content_block_helper_behavior.rb' + - 'app/helpers/hyrax/hyrax_helper_behavior.rb' + - 'app/presenters/hyrax/fixity_status_presenter.rb' + - 'app/presenters/hyrax/presents_attributes.rb' + - 'app/renderers/hyrax/renderers/attribute_renderer.rb' + - 'spec/views/hyrax/my/works/_list_works.html.erb_spec.rb' + +Rails/UniqueValidationWithoutIndex: + Exclude: + - 'app/models/account.rb' + - 'app/models/domain_name.rb' + - 'app/models/hyrax/group.rb' RSpec/DescribeClass: Exclude: - - 'spec/requests/**/*' + - 'spec/abilities/**/*' + - 'spec/config/hyrax_events_spec.rb' + - 'spec/conversions/**/*' - 'spec/features/**/*' + - 'spec/inputs/**/*' + - 'spec/javascripts/jasmine_spec.rb' + - 'spec/tasks/rake_spec.rb' - 'spec/views/**/*' - - 'spec/routing/**/*' - - 'spec/tasks/**/*' -Rails/FilePath: - Exclude: - - 'spec/routing/**/*' +# # By default RSpec/MessageSpies has the following: +# # Prefer have_received for setting message expectations. Setup form as a spy using allow or instance_spy. +# # The default assumes EnforcedStyle is 'have_received'. Most of our specs are 'receive' +RSpec/MessageSpies: + Enabled: false RSpec/ExpectActual: - Exclude: - - 'spec/routing/**/*' + Enabled: false -RSpec/VerifiedDoubles: +RSpec/LetSetup: Enabled: false RSpec/MessageExpectation: Enabled: false -# By default RSpec/MessageSpies has the following: -# Prefer have_received for setting message expectations. Setup form as a spy using allow or instance_spy. -RSpec/MessageSpies: - Enabled: true - EnforcedStyle: receive - -RSpec/ExampleLength: - Max: 20 - RSpec/NestedGroups: - Max: 4 - -RSpec/MultipleExpectations: Enabled: false -Metrics/BlockLength: - Exclude: - - 'spec/**/*.rb' - - 'lib/tasks/*.rake' - - 'app/controllers/catalog_controller.rb' +RSpec/LeadingSubject: + Enabled: false diff --git a/.rubocop_fixme.yml b/.rubocop_fixme.yml new file mode 100644 index 0000000000..964bae6dc1 --- /dev/null +++ b/.rubocop_fixme.yml @@ -0,0 +1,201 @@ +Security/MarshalLoad: + Exclude: + - 'app/models/concerns/hyrax/user.rb' + +Metrics/ClassLength: + Exclude: + - 'app/controllers/hyrax/dashboard/collections_controller.rb' + - 'app/controllers/hyrax/admin/admin_sets_controller.rb' + - 'app/controllers/hyrax/batch_edits_controller.rb' + - 'app/controllers/hyrax/downloads_controller.rb' + - 'app/controllers/hyrax/file_sets_controller.rb' + - 'app/forms/hyrax/forms/permission_template_form.rb' + - 'app/presenters/hyrax/work_show_presenter.rb' + - 'app/presenters/hyrax/collection_presenter.rb' + - 'app/services/hyrax/user_stat_importer.rb' + - 'lib/generators/hyrax/templates/catalog_controller.rb' + - 'lib/generators/hyrax/install_generator.rb' + - 'lib/hyrax/configuration.rb' + +Metrics/ParameterLists: + Exclude: + - 'app/jobs/batch_create_job.rb' + +Metrics/ModuleLength: + Exclude: + - 'app/controllers/concerns/hyrax/works_controller_behavior.rb' + - 'app/helpers/hyrax/hyrax_helper_behavior.rb' + - 'app/models/concerns/hyrax/ability.rb' + - 'app/services/hyrax/workflow/permission_query.rb' + - 'spec/services/hyrax/workflow/permission_query_spec.rb' + # TODO: extract CollectionAccessControls or something, so we don't have to skip this check? + - 'app/models/concerns/hyrax/collection_behavior.rb' + +RSpec/NamedSubject: + Enabled: false + +RSpec/ExampleLength: + Max: 9 + Exclude: + - 'spec/actors/hyrax/actors/file_set_actor_spec.rb' + - 'spec/actors/hyrax/actors/generic_work_actor_spec.rb' + - 'spec/controllers/hyrax/api/items_controller_spec.rb' + - 'spec/controllers/hyrax/batch_edits_controller_spec.rb' + - 'spec/controllers/hyrax/batch_uploads_controller_spec.rb' + - 'spec/controllers/hyrax/file_sets_controller_spec.rb' + - 'spec/controllers/hyrax/generic_works_controller_spec.rb' + - 'spec/controllers/hyrax/my/highlights_controller_spec.rb' + - 'spec/controllers/hyrax/transfers_controller_spec.rb' + - 'spec/forms/hyrax/forms/collection_form_spec.rb' + - 'spec/forms/hyrax/forms/batch_edit_form_spec.rb' + - 'spec/forms/hyrax/forms/batch_upload_form_spec.rb' + - 'spec/forms/hyrax/forms/file_set_edit_form_spec.rb' + - 'spec/features/**/*' + - 'spec/helpers/hyrax/charts_helper_spec.rb' + - 'spec/helpers/dashboard_helper_spec.rb' + - 'spec/helpers/hyrax_helper_spec.rb' + - 'spec/indexers/hyrax/file_set_indexer_spec.rb' + - 'spec/javascripts/jasmine_spec.rb' + - 'spec/jobs/file_set_attached_event_job_spec.rb' + - 'spec/jobs/batch_create_job_spec.rb' + - 'spec/jobs/create_work_job_spec.rb' + - 'spec/jobs/content_update_event_job_spec.rb' + - 'spec/jobs/content_restored_version_event_job_spec.rb' + - 'spec/jobs/content_new_version_event_job_spec.rb' + - 'spec/jobs/content_depositor_change_event_job_spec.rb' + - 'spec/jobs/change_depositor_event_job_spec.rb' + - 'spec/jobs/content_deposit_event_job_spec.rb' + - 'spec/jobs/content_delete_event_job_spec.rb' + - 'spec/jobs/ingest_file_job_spec.rb' + - 'spec/lib/hyrax/arkivo/actor_spec.rb' + - 'spec/lib/hyrax/resource_sync/capability_list_writer_spec.rb' + - 'spec/models/checksum_audit_log_spec.rb' + - 'spec/models/featured_work_spec.rb' + - 'spec/models/file_set_spec.rb' + - 'spec/models/generic_work_spec.rb' + - 'spec/presenters/hyrax/inspect_work_presenter_spec.rb' + - 'spec/services/hyrax/actor_factory_spec.rb' + - 'spec/services/hyrax/admin_set_create_service_spec.rb' + - 'spec/services/hyrax/default_middleware_stack_spec.rb' + - 'spec/services/hyrax/graph_exporter_spec.rb' + - 'spec/services/hyrax/user_stat_importer_spec.rb' + - 'spec/services/hyrax/workflow/activate_object_spec.rb' + - 'spec/services/hyrax/workflow/deactivate_object_spec.rb' + - 'spec/services/hyrax/workflow/permission_generator_spec.rb' + - 'spec/services/hyrax/workflow/permission_query_spec.rb' + - 'spec/services/hyrax/workflow/state_machine_generator_spec.rb' + - 'spec/services/hyrax/workflow/workflow_importer_spec.rb' + - 'spec/views/**/*' + - 'spec/wings/valkyrie/persister_spec.rb' + +RSpec/VerifiedDoubles: + Enabled: false + +RSpec/SubjectStub: + Exclude: + - 'spec/actors/hyrax/actors/generic_work_actor_spec.rb' + - 'spec/controllers/hyrax/file_sets_controller_spec.rb' + - 'spec/models/file_set_spec.rb' + - 'spec/models/hyrax/work_behavior_spec.rb' + - 'spec/search_builders/hyrax/file_set_search_builder_spec.rb' + - 'spec/models/hyrax/operation_spec.rb' + - 'spec/controllers/hyrax/accepts_batches_controller_spec.rb' + - 'spec/indexers/hyrax/repository_reindexer_spec.rb' + - 'spec/lib/hyrax/analytics_spec.rb' + - 'spec/models/job_io_wrapper_spec.rb' + - 'spec/search_builders/hyrax/abstract_type_relation_spec.rb' + - 'spec/services/hyrax/database_migrator_spec.rb' + +RSpec/AnyInstance: + Exclude: + - 'spec/actors/hyrax/actors/generic_work_actor_spec.rb' + - 'spec/controllers/hyrax/api/items_controller_spec.rb' + - 'spec/controllers/hyrax/api/zotero_controller_spec.rb' + - 'spec/controllers/hyrax/batch_edits_controller_spec.rb' + - 'spec/controllers/hyrax/stats_controller_spec.rb' + - 'spec/controllers/hyrax/users_controller_spec.rb' + - 'spec/hyrax/transactions/steps/delete_access_control_spec.rb' + - 'spec/hyrax/transactions/steps/save_access_control_spec.rb' + - 'spec/jobs/content_restored_version_event_job_spec.rb' + - 'spec/jobs/file_set_attached_event_job_spec.rb' + - 'spec/jobs/hyrax/grant_edit_to_members_job_spec.rb' + - 'spec/jobs/hyrax/grant_read_to_members_job_spec.rb' + - 'spec/jobs/hyrax/revoke_edit_from_members_job_spec.rb' + - 'spec/lib/hyrax/arkivo/create_subscription_job_spec.rb' + - 'spec/presenters/hyrax/file_usage_spec.rb' + - 'spec/presenters/hyrax/work_usage_spec.rb' + - 'spec/services/hyrax/repository_fixity_check_service_spec.rb' + - 'spec/services/hyrax/workflow/permission_generator_spec.rb' + - 'spec/services/hyrax/workflow/sipity_actions_generator_spec.rb' + - 'spec/services/hyrax/workflow/state_machine_generator_spec.rb' + - 'spec/services/hyrax/workflow/workflow_permissions_generator_spec.rb' + - 'spec/controllers/hyrax/homepage_controller_spec.rb' + - 'spec/controllers/hyrax/my/collections_controller_spec.rb' + - 'spec/controllers/hyrax/my/works_controller_spec.rb' + - 'spec/presenters/hyrax/admin/repository_object_presenter_spec.rb' + +# Offense count: 51 +RSpec/ExpectInHook: + Enabled: false + +# Offense count: 27 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: and_return, block +RSpec/ReturnFromStub: + Exclude: + - 'spec/controllers/hyrax/api/items_controller_spec.rb' + - 'spec/controllers/hyrax/file_sets_controller_spec.rb' + - 'spec/lib/hyrax/arkivo/create_subscription_job_spec.rb' + - 'spec/models/file_set_spec.rb' + - 'spec/presenters/hyrax/admin_set_options_presenter_spec.rb' + - 'spec/routing/api_route_spec.rb' + - 'spec/views/_user_util_links.html.erb_spec.rb' + - 'spec/views/hyrax/base/_attributes.html.erb_spec.rb' + - 'spec/views/hyrax/base/_form.html.erb_spec.rb' + - 'spec/views/hyrax/base/file_manager.html.erb_spec.rb' + - 'spec/views/hyrax/dashboard/profiles/edit.html.erb_spec.rb' + - 'spec/views/hyrax/users/_user_info.html.erb_spec.rb' + +# Offense count: 26 +RSpec/RepeatedDescription: + Exclude: + - 'spec/models/sipity/agent_spec.rb' + - 'spec/models/sipity/comment_spec.rb' + - 'spec/models/sipity/entity_spec.rb' + - 'spec/models/sipity/entity_specific_responsibility_spec.rb' + - 'spec/models/sipity/role_spec.rb' + - 'spec/models/sipity/workflow_action_spec.rb' + - 'spec/models/sipity/workflow_responsibility_spec.rb' + - 'spec/models/sipity/workflow_role_spec.rb' + - 'spec/models/sipity/workflow_state_action_permission_spec.rb' + - 'spec/models/sipity/workflow_state_action_spec.rb' + - 'spec/models/sipity/workflow_state_spec.rb' + +# Offense count: 2 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/admin_set.rb' + - 'app/models/hyrax/permission_template.rb' + +# Offense count: 1 +Rails/SkipsModelValidations: + Exclude: + - 'app/services/hyrax/works/migration_service.rb' + +# Offense count: 12 +Lint/MissingSuper: + Exclude: + - 'app/actors/hyrax/actors/interpret_visibility_actor.rb' + - 'app/actors/hyrax/actors/ordered_members_actor.rb' + - 'app/models/concerns/hyrax/file_set/characterization.rb' + - 'app/presenters/hyrax/file_usage.rb' + - 'app/presenters/hyrax/work_usage.rb' + - 'app/services/hyrax/batch_create_failure_service.rb' + - 'app/services/hyrax/batch_create_success_service.rb' + - 'app/services/hyrax/collection_types/create_service.rb' + - 'app/services/hyrax/solr_query_service.rb' + - 'lib/hyrax/form_fields.rb' + - 'lib/hyrax/health_checks/solr_check.rb' + - 'lib/hyrax/schema.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 902a164717..ad19a7fd2c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,186 +1,139 @@ # This configuration was generated by -# `rubocop --auto-gen-config` -# on 2020-05-15 17:53:23 +0000 using RuboCop version 0.52.1. +# `rubocop --auto-gen-config --no-auto-gen-timestamp` +# using RuboCop version 1.28.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -Lint/AmbiguousBlockAssociation: - Exclude: - - 'app/controllers/catalog_controller.rb' - -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Lint/EndAlignment: +# Offense count: 2 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines. +Layout/EmptyLineBetweenDefs: Exclude: - - 'app/helpers/blacklight/catalog_helper_behavior.rb' + - 'app/jobs/bulkrax/import_file_set_job.rb' + - 'app/models/bulkrax/entry.rb' # Offense count: 2 -Lint/UselessAssignment: +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, no_empty_lines +Layout/EmptyLinesAroundBlockBody: Exclude: - - 'app/forms/hyrax/forms/admin/appearance.rb' - - 'spec/controllers/proprietor/users_controller_spec.rb' - -# Offense count: 10 -Metrics/AbcSize: - Max: 49 + - 'spec/rails_helper.rb' # Offense count: 3 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 112 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'app/parsers/bulkrax/csv_parser.rb' + - 'spec/models/bulkrax/rdf_entry_spec.rb' + - 'spec/models/bulkrax/xml_entry_spec.rb' -# Offense count: 2 -Metrics/CyclomaticComplexity: - Max: 12 +# Offense count: 1 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: Width, AllowedPatterns, IgnoredPatterns. +Layout/IndentationWidth: + Exclude: + - 'spec/rails_helper.rb' -# Offense count: 23 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# Offense count: 8 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. # URISchemes: http, https -Metrics/LineLength: - Max: 376 - -# Offense count: 7 -# Configuration parameters: CountComments. -Metrics/MethodLength: - Max: 35 +Layout/LineLength: + Max: 301 # Offense count: 1 -# Configuration parameters: CountComments. -Metrics/ModuleLength: - Max: 168 - -# Offense count: 2 -Metrics/PerceivedComplexity: - Max: 14 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'app/parsers/bulkrax/csv_parser.rb' # Offense count: 1 -# Configuration parameters: Blacklist. -# Blacklist: END, (?-mix:EO[A-Z]{1}) -Naming/HeredocDelimiterNaming: +# This cop supports safe auto-correction (--auto-correct). +Layout/RescueEnsureAlignment: Exclude: - - 'spec/jobs/import_work_from_purl_job_spec.rb' - -# Offense count: 6 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist, MethodDefinitionMacros. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -# MethodDefinitionMacros: define_method, define_singleton_method -Naming/PredicateName: - Exclude: - - 'spec/**/*' - - 'app/controllers/application_controller.rb' - - 'app/helpers/blacklight/catalog_helper_behavior.rb' - - 'app/models/user.rb' + - 'spec/rails_helper.rb' -# Offense count: 6 -RSpec/BeforeAfterAll: - Exclude: - - 'spec/spec_helper.rb' +# Offense count: 7 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'app/models/bulkrax/csv_entry.rb' + - 'app/parsers/bulkrax/csv_parser.rb' + - 'spec/models/bulkrax/rdf_entry_spec.rb' + - 'spec/models/bulkrax/xml_entry_spec.rb' - 'spec/rails_helper.rb' - - 'spec/support/**/*.rb' - - 'spec/models/hyku/group_spec.rb' - - 'spec/models/uploaded_file_spec.rb' - - 'spec/tasks/rake_spec.rb' -# Offense count: 85 -# Configuration parameters: Prefixes. -# Prefixes: when, with, without -RSpec/ContextWording: - Enabled: false +# Offense count: 16 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 42 -# Offense count: 3 -RSpec/ExpectInHook: - Exclude: - - 'spec/controllers/sites_controller_spec.rb' - - 'spec/models/uploaded_file_spec.rb' +# Offense count: 4 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 201 -# Offense count: 1 -RSpec/IteratedExpectation: - Exclude: - - 'spec/tasks/rake_spec.rb' +# Offense count: 13 +# Configuration parameters: IgnoredMethods. +Metrics/CyclomaticComplexity: + Max: 19 -# Offense count: 4 -RSpec/LetSetup: - Exclude: - - 'spec/models/account_spec.rb' - - 'spec/models/role_spec.rb' - - 'spec/models/user_spec.rb' +# Offense count: 32 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +Metrics/MethodLength: + Max: 26 # Offense count: 2 -RSpec/MessageChain: - Exclude: - - 'spec/views/shared/select_work_type_modal.html.erb_spec.rb' +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 131 -# Offense count: 8 -RSpec/ScatteredLet: - Exclude: - - 'spec/controllers/account_sign_up_controller_spec.rb' - - 'spec/controllers/admin/accounts_controller_spec.rb' - - 'spec/controllers/proprietor/accounts_controller_spec.rb' - - 'spec/jobs/import_work_from_purl_job_spec.rb' +# Offense count: 9 +# Configuration parameters: IgnoredMethods. +Metrics/PerceivedComplexity: + Max: 19 # Offense count: 1 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: Exclude: - - 'app/models/endpoint.rb' + - 'app/models/concerns/bulkrax/status_info.rb' -# Offense count: 5 -Rails/OutputSafety: - Exclude: - - 'app/forms/hyrax/forms/admin/appearance.rb' - - 'app/helpers/blacklight/catalog_helper_behavior.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Exclude: - - 'app/helpers/blacklight/catalog_helper_behavior.rb' - -# Offense count: 3 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: annotated, template, unannotated -Style/FormatStringToken: +# Offense count: 2 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: Keywords, RequireColon. +# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE +Style/CommentAnnotation: Exclude: - - 'app/models/account.rb' - - 'spec/models/account_spec.rb' + - 'app/models/bulkrax/xml_entry.rb' + - 'spec/models/bulkrax/oai_entry_spec.rb' -# Offense count: 4 -# Configuration parameters: MinBodyLength. -Style/GuardClause: +# Offense count: 2 +# This cop supports safe auto-correction (--auto-correct). +Style/IfUnlessModifier: Exclude: - - 'app/controllers/application_controller.rb' - - 'app/controllers/hyrax/downloads_controller.rb' - - 'app/helpers/blacklight/catalog_helper_behavior.rb' - - 'app/models/bulkrax/entry.rb' + - 'app/models/bulkrax/csv_entry.rb' + - 'lib/generators/bulkrax/templates/config/initializers/bulkrax.rb' # Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: +# This cop supports safe auto-correction (--auto-correct). +Style/MultilineIfModifier: Exclude: - - 'app/models/user.rb' + - 'app/models/bulkrax/csv_entry.rb' # Offense count: 1 -Style/MixinUsage: +# This cop supports safe auto-correction (--auto-correct). +Style/RedundantBegin: Exclude: - - 'spec/support/devise.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'app/helpers/blacklight/catalog_helper_behavior.rb' + - 'spec/rails_helper.rb' diff --git a/Dockerfile b/Dockerfile index 8916c378ef..325d61452b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -ARG HYRAX_IMAGE_VERSION=hyrax-v4.0.0.rc1 -ARG RUBY_VERSION=2.7.7 +ARG HYRAX_IMAGE_VERSION=hyrax-v5.0.0.rc1 FROM ghcr.io/samvera/hyrax/hyrax-base:$HYRAX_IMAGE_VERSION as hyku-base USER root diff --git a/Gemfile b/Gemfile index 3fd4a64535..c12f7d1001 100644 --- a/Gemfile +++ b/Gemfile @@ -1,22 +1,26 @@ # frozen_string_literal: true -# rubocop:disable Metrics/LineLength +# rubocop:disable Layout/LineLength source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 5.2.5' +gem 'rails', '~> 6.0', github: 'rails/rails', branch: '6-1-stable' -gem 'active-fedora', '>= 11.1.4' gem 'active_elastic_job', github: 'active-elastic-job/active-elastic-job', ref: 'ec51c5d9dedc4a1b47f2db41f26d5fceb251e979', group: %i[aws] +gem 'active-fedora', '~> 14.0' gem 'activerecord-nulldb-adapter' gem 'addressable', '2.8.1' # remove once https://github.com/postrank-labs/postrank-uri/issues/49 is fixed -gem 'apartment' +gem 'apartment', github: 'scientist-softserv/apartment', branch: 'development' gem 'aws-sdk-sqs', group: %i[aws] -gem 'blacklight', '~> 6.7' -gem 'blacklight_oai_provider', '~> 6.1', '>= 6.1.1' +gem 'bixby', '~> 5.0', '>= 5.0.2', group: %i[development test] +gem 'blacklight', '~> 7.29' +gem 'blacklight_advanced_search' +gem 'blacklight_oai_provider', '~> 7.0' +gem 'blacklight_range_limit' gem 'bolognese', '>= 1.9.10' +gem 'bootstrap', '~> 4.6' gem 'bootstrap-datepicker-rails' -gem 'bulkrax', '~> 5.3' +gem 'bulkrax', '~> 5.4' gem 'byebug', group: %i[development test] gem 'capybara', group: %i[test] gem 'capybara-screenshot', '~> 1.0', group: %i[test] @@ -28,19 +32,20 @@ gem 'database_cleaner', group: %i[test] gem 'devise' gem 'devise-guests', '~> 0.3' gem 'devise-i18n' -gem 'devise_invitable', '~> 1.6' -gem 'dry-monads', '~> 1.4.0' # Locked because 1.5.0 was not compatible with Hyrax v.3.4.2 +gem 'devise_invitable', '~> 2.0' +gem 'dry-monads', '~> 1.5' gem 'easy_translate', group: %i[development] gem 'factory_bot_rails', group: %i[test] gem 'fcrepo_wrapper', '~> 0.4', group: %i[development test] -gem 'flipflop', '~> 2.6.0' # waiting for hyrax 4 upgrade gem 'flutie' -gem 'hyrax', '~> 3.5.0' -gem 'hyrax-doi', github: 'samvera-labs/hyrax-doi', branch: 'main' -gem 'hyrax-iiif_av', github: 'samvera-labs/hyrax-iiif_av', branch: 'main' +gem 'good_job', '~> 2.99' +gem 'googleauth', '= 1.8.1' # 1.9.0 got yanked from rubygems, hard pinning until we can upgrade +gem 'hyrax', github: 'samvera/hyrax', branch: 'double_combo' +gem 'hyrax-doi', github: 'samvera-labs/hyrax-doi', branch: 'rails_hyrax_upgrade' +gem 'hyrax-iiif_av', github: 'samvera-labs/hyrax-iiif_av', branch: 'rails_hyrax_upgrade' gem 'i18n-debug', require: false, group: %i[development test] gem 'i18n-tasks', group: %i[development test] -gem 'iiif_print', github: 'scientist-softserv/iiif_print', branch: 'main' +gem 'iiif_print', github: 'scientist-softserv/iiif_print', branch: 'rails_hyrax_upgrade' gem 'jbuilder', '~> 2.5' gem 'jquery-rails' # Use jquery as the JavaScript library # The maintainers yanked 0.3.2 version (see https://github.com/dryruby/json-canonicalization/issues/2) @@ -53,42 +58,47 @@ gem 'negative_captcha' gem 'okcomputer' gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271' gem 'omniauth-multi-provider' +gem 'omniauth_openid_connect' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-saml', '~> 2.1' -gem 'omniauth_openid_connect' -gem 'parser', '~> 2.5.3' +gem 'order_already' +gem 'parser', '>= 3.1.0.0' gem 'pg' gem 'postrank-uri', '>= 1.0.24' gem 'pry-byebug', group: %i[development test] gem 'puma', '~> 5.6' # Use Puma as the app server gem 'rack-test', '0.7.0', group: %i[test] # rack-test >= 0.71 does not work with older Capybara versions (< 2.17). See #214 for more details gem 'rails-controller-testing', group: %i[test] -gem 'rdf', '~> 3.1.15' # rdf 3.2.0 removed SerializedTransaction which ldp requires +gem 'rdf', '~> 3.2' +gem 'redis-namespace', '~> 1.10' # Hyrax v5 relies on 1.5; but we'd like to have the #clear method so we need 1.10 or greater. gem 'redlock', '>= 0.1.2', '< 2.0' # lock redlock per https://github.com/samvera/hyrax/pull/5961 -gem 'riiif', '~> 1.1' +gem 'riiif', '~> 2.0' gem 'rolify' gem 'rsolr', '~> 2.0' gem 'rspec', group: %i[development test] gem 'rspec-activemodel-mocks', group: %i[test] gem 'rspec-its', group: %i[test] +gem 'rspec_junit_formatter', group: %i[test] gem 'rspec-rails', '>= 3.6.0', group: %i[development test] gem 'rspec-retry', group: %i[test] -gem 'rspec_junit_formatter', group: %i[test] -gem 'rubocop', '~> 0.50', '<= 0.52.1', group: %i[development test] +gem 'rubocop', '1.28.2', group: %i[development test] +gem 'rubocop-rails', '~> 2.15', group: %i[development test] gem 'rubocop-rspec', '~> 1.22', '<= 1.22.2', group: %i[development test] -gem 'sass-rails', '~> 5.0' # Use SCSS for stylesheets +gem 'sass-rails', '~> 6.0' # Use SCSS for stylesheets gem 'scss_lint', require: false, group: %i[development] gem 'secure_headers' gem 'selenium-webdriver', '4.8.1', group: %i[test] gem 'shoulda-matchers', '~> 4.0', group: %i[test] gem 'sidekiq', "< 7.0" # sidekiq 7 requires upgrade to redis 6 gem 'simplecov', require: false, group: %i[development test] +gem 'solargraph', group: %i[development] gem 'solr_wrapper', '~> 2.0', group: %i[development test] gem 'spring', '~> 1.7', group: %i[development] gem 'spring-watcher-listen', '~> 2.0.0', group: %i[development] gem 'terser' # to support the Safe Navigation / Optional Chaining operator (?.) and avoid uglifier precompile issue gem 'tether-rails' gem 'turbolinks', '~> 5' +gem 'twitter-typeahead-rails', '0.11.1.pre.corejavascript' gem 'web-console', '>= 3.3.0', group: %i[development] # <%= console %> in views gem 'webdrivers', '~> 4.7.0', group: %i[test] gem 'webmock', group: %i[test] @@ -97,4 +107,4 @@ gem 'webmock', group: %i[test] # and place overrides, themes and deployment code. gem 'hyku_knapsack', github: 'samvera-labs/hyku_knapsack', branch: 'upstream_main' -# rubocop:enable Metrics/LineLength +# rubocop:enable Layout/LineLength diff --git a/Gemfile.lock b/Gemfile.lock index 651139ee28..f340413ca6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,49 +7,204 @@ GIT aws-sdk-sqs (~> 1) rails (>= 4.2) +GIT + remote: https://github.com/rails/rails.git + revision: d16199e507086e3d54d94253b7e1d87ead394d9f + branch: 6-1-stable + specs: + actioncable (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) + mail (>= 2.7.1) + actionmailer (6.1.7.6) + actionpack (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activesupport (= 6.1.7.6) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.7.6) + actionview (= 6.1.7.6) + activesupport (= 6.1.7.6) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.6) + actionpack (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) + nokogiri (>= 1.8.5) + actionview (6.1.7.6) + activesupport (= 6.1.7.6) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.7.6) + activesupport (= 6.1.7.6) + globalid (>= 0.3.6) + activemodel (6.1.7.6) + activesupport (= 6.1.7.6) + activerecord (6.1.7.6) + activemodel (= 6.1.7.6) + activesupport (= 6.1.7.6) + activestorage (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activesupport (= 6.1.7.6) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + rails (6.1.7.6) + actioncable (= 6.1.7.6) + actionmailbox (= 6.1.7.6) + actionmailer (= 6.1.7.6) + actionpack (= 6.1.7.6) + actiontext (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activemodel (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) + bundler (>= 1.15.0) + railties (= 6.1.7.6) + sprockets-rails (>= 2.0.0) + railties (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) + method_source + rake (>= 12.2) + thor (~> 1.0) + GIT remote: https://github.com/samvera-labs/hyku_knapsack.git - revision: 446f16b928f7f12125fdaaf6feb08dcdacf1bd38 + revision: 14f688b61afab9ee886f5f7fa26a76a37bced597 branch: upstream_main specs: hyku_knapsack (0.0.1) rails (>= 5.2.0) - sentry-raven GIT remote: https://github.com/samvera-labs/hyrax-doi.git - revision: d494a50ef8ce3eae594c7ed7148c33b3c977d4a7 - branch: main + revision: 9b7ab7f8054a14385b838de1118f55ad2e0f5da8 + branch: rails_hyrax_upgrade specs: hyrax-doi (0.2.0) - bolognese (~> 1.8, >= 1.8.6) + bolognese (>= 1.8.6, < 2.0) flipflop (~> 2.3) - hyrax (>= 2.9, < 4.0) - rails (~> 5.2.4, >= 5.2.4.3) + hyrax (>= 2.9, < 6.0) + rails (>= 5.2.4.3, < 8.0) GIT remote: https://github.com/samvera-labs/hyrax-iiif_av.git - revision: 6273f90016c153d2add33f85cc24285d51a25382 - branch: main + revision: 4723b8fd0961fa179caf787532bd7922a44957ec + branch: rails_hyrax_upgrade specs: hyrax-iiif_av (0.2.0) blacklight - hyrax (>= 2.9, < 4.0) + hyrax (>= 3.5, < 6.0) iiif_manifest (> 0.5) - rails (~> 5.1) + rails (>= 5.1, < 8.0) + +GIT + remote: https://github.com/samvera/hyrax.git + revision: b7891b758411c59f71ff54212e0d250fcc47e35f + branch: double_combo + specs: + hyrax (5.0.0.rc2) + active-fedora (~> 14.0) + almond-rails (~> 0.1) + awesome_nested_set (~> 3.1) + blacklight (~> 7.29) + blacklight-gallery (~> 4.0) + breadcrumbs_on_rails (~> 3.0) + browse-everything (>= 0.16, < 2.0) + carrierwave (~> 1.0) + clipboard-rails (~> 1.5) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.4) + draper (~> 4.0) + dry-container (~> 0.11) + dry-events (~> 1.0, >= 1.0.1) + dry-logic (~> 1.5) + dry-monads (~> 1.6) + dry-validation (~> 1.10) + flipflop (~> 2.3) + flot-rails (~> 0.0.6) + font-awesome-rails (~> 4.2) + hydra-derivatives (~> 3.3) + hydra-editor (~> 6.0) + hydra-file_characterization (~> 1.1) + hydra-head (~> 12.0) + hydra-works (>= 0.16) + iiif_manifest (>= 0.3, < 2.0) + json-schema + legato (~> 0.3) + linkeddata + mailboxer (~> 0.12) + nest (~> 3.1) + noid-rails (~> 3.0) + oauth + oauth2 (~> 1.2) + openseadragon + posix-spawn + qa (~> 5.5, >= 5.5.1) + rails (~> 6.1) + rails_autolink (~> 1.1) + rdf-rdfxml + rdf-vocab (~> 3.0) + redis (~> 4.0) + redis-namespace (~> 1.5) + redlock (>= 0.1.2, < 2.0) + reform (~> 2.3) + reform-rails (~> 0.2.0) + retriable (>= 2.9, < 4.0) + sass-rails (~> 6.0) + select2-rails (~> 3.5) + signet + sprockets (~> 3.7) + tinymce-rails (~> 5.10) + valkyrie (~> 3.1.1) + view_component (~> 2.74.1) + +GIT + remote: https://github.com/scientist-softserv/apartment.git + revision: 9ad4b0ef2ee6debb956e86ef9e7095c8358fef5e + branch: development + specs: + apartment (2.2.1) + activerecord (>= 3.1.2, < 7.0) + parallel (>= 0.7.1) + public_suffix (>= 2) + rack (>= 1.3.6) GIT remote: https://github.com/scientist-softserv/iiif_print.git - revision: 9e7837ce4bd08bf8fff9126455d0e0e2602f6018 - branch: main + revision: cad2cf33fc07ec2abb5b6834a858b5ed74903c12 + branch: rails_hyrax_upgrade specs: iiif_print (1.0.0) - blacklight_iiif_search (~> 1.0) + blacklight_iiif_search (>= 1.0, < 3.0) derivative-rodeo (~> 0.5) - dry-monads (~> 1.4.0) - hyrax (>= 2.5, < 4) + hyrax (>= 2.5, < 6) nokogiri (>= 1.13.2) - rails (~> 5.0) rdf-vocab (~> 3.0) GIT @@ -65,35 +220,12 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - active-fedora (13.3.0) + active-fedora (14.0.1) active-triples (>= 0.11.0, < 2.0.0) activemodel (>= 5.1) activesupport (>= 5.1) deprecation - faraday (~> 0.12) + faraday (>= 1.0) faraday-encoding (>= 0.0.5) ldp (>= 0.7.0, < 2) rsolr (>= 1.1.2, < 3) @@ -106,119 +238,110 @@ GEM active_encode (0.8.2) rails sprockets (< 4) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) - globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activerecord-import (1.4.1) + activerecord-import (1.5.1) activerecord (>= 4.2) - activerecord-nulldb-adapter (0.9.0) - activerecord (>= 5.2.0, < 7.1) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + activerecord-nulldb-adapter (1.0.1) + activerecord (>= 5.2.0, < 7.2) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) almond-rails (0.3.0) rails (>= 4.2) - amazing_print (1.4.0) - apartment (2.2.1) - activerecord (>= 3.1.2, < 6.0) - parallel (>= 0.7.1) - public_suffix (>= 2) - rack (>= 1.3.6) - arel (9.0.0) ast (2.4.2) attr_required (1.0.1) - autoprefixer-rails (10.4.13.0) + autoprefixer-rails (10.4.16.0) execjs (~> 2) - awesome_nested_set (3.5.0) - activerecord (>= 4.0.0, < 7.1) - aws-eventstream (1.2.0) - aws-partitions (1.785.0) - aws-sdk-core (3.178.0) - aws-eventstream (~> 1, >= 1.0.2) + awesome_nested_set (3.6.0) + activerecord (>= 4.0.0, < 7.2) + aws-eventstream (1.3.0) + aws-partitions (1.865.0) + aws-sdk-core (3.190.0) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.64.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.74.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.121.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.141.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sdk-sqs (1.61.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sigv4 (~> 1.8) + aws-sdk-sqs (1.69.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.6.0) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) + backport (1.2.0) bagit (0.4.6) docopt (~> 0.5.0) validatable (~> 1.6) + base64 (0.2.0) bcp47 (0.3.3) i18n - bcrypt (3.1.18) + bcp47_spec (0.2.1) + bcrypt (3.1.20) + benchmark (0.3.0) benchmark_methods (0.7) - better_html (1.0.16) - actionview (>= 4.0) - activesupport (>= 4.0) + better_html (2.0.2) + actionview (>= 6.0) + activesupport (>= 6.0) ast (~> 2.0) erubi (~> 1.4) - html_tokenizer (~> 0.0.6) parser (>= 2.4) smart_properties bibtex-ruby (6.0.0) latex-decode (~> 0.0) + bigdecimal (3.1.5) bindata (2.4.15) bindex (0.8.1) - blacklight (6.25.0) - bootstrap-sass (~> 3.2) + bixby (5.0.2) + rubocop (= 1.28.2) + rubocop-ast + rubocop-performance + rubocop-rails + rubocop-rspec + blacklight (7.35.0) deprecation globalid + hashdiff + i18n (>= 1.7.0) jbuilder (~> 2.7) kaminari (>= 0.15) - nokogiri (~> 1.6) - rails (>= 4.2, < 6) - rsolr (>= 1.0.6, < 3) - twitter-typeahead-rails (= 0.11.1.pre.corejavascript) - blacklight-access_controls (0.6.2) - blacklight (~> 6.0) - cancancan (~> 1.8) + ostruct (>= 0.3.2) + rails (>= 5.1, < 7.2) + view_component (>= 2.66, < 4) + blacklight-access_controls (6.0.1) + blacklight (> 6.0, < 8) + cancancan (>= 1.8) deprecation (~> 1.0) - blacklight-gallery (0.12.0) - blacklight (~> 6.3) - bootstrap-sass (~> 3.0) - openseadragon (>= 0.2.0) - rails - blacklight_iiif_search (1.0.0) - blacklight (~> 6.0) + blacklight-gallery (4.4.0) + blacklight (>= 7.17, < 9) + rails (>= 6.1, < 8) + blacklight_advanced_search (7.0.0) + blacklight (~> 7.0) + parslet + blacklight_iiif_search (2.0.0) + blacklight (~> 7.0) iiif-presentation - rails (>= 4.2, < 6) - blacklight_oai_provider (6.1.1) - blacklight (~> 6.0) - oai (~> 1.0) - bolognese (1.11.0) + rails (>= 5.1, < 7) + blacklight_oai_provider (7.0.2) + blacklight (~> 7.0) + oai (~> 1.2) + rexml + blacklight_range_limit (8.4.0) + blacklight (>= 7.25.2, < 9) + deprecation + view_component (>= 2.54, < 4) + bolognese (1.11.5) activesupport (>= 4.2.5) benchmark_methods (~> 0.7) bibtex-ruby (>= 5.1.0) @@ -242,27 +365,27 @@ GEM rdf-rdfxml (~> 3.1) rdf-turtle (~> 3.1) thor (>= 0.19) - bootstrap-datepicker-rails (1.9.0.1) + bootstrap (4.6.2) + autoprefixer-rails (>= 9.1.0) + popper_js (>= 1.16.1, < 2) + sassc-rails (>= 2.0.0) + bootstrap-datepicker-rails (1.10.0.1) railties (>= 3.0) - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) breadcrumbs_on_rails (3.0.1) - browse-everything (1.2.0) + browse-everything (1.3.0) addressable (~> 2.5) aws-sdk-s3 dropbox_api (>= 0.1.20) google-apis-drive_v3 googleauth (>= 0.6.6, < 2.0) - rails (>= 4.2, < 7.1) + rails (>= 4.2, < 7.2) ruby-box signet (~> 0.8) typhoeus builder (3.2.4) - bulkrax (5.4.0) + bulkrax (5.5.0) bagit (~> 0.4) coderay - dry-monads (~> 1.4.0) iso8601 (~> 0.9.0) kaminari language_list (~> 1.2, >= 1.2.1) @@ -275,8 +398,8 @@ GEM rubyzip simple_form byebug (11.1.3) - cancancan (1.17.0) - capybara (3.39.0) + cancancan (3.5.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -288,7 +411,7 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - carrierwave (1.3.3) + carrierwave (1.3.4) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) @@ -303,8 +426,7 @@ GEM csl (~> 1.6) clipboard-rails (1.7.1) cocoon (1.2.15) - codemirror-rails (5.16.0) - railties (>= 3.0, < 6.0) + codemirror-rails (0.3.1) coderay (1.1.3) coffee-rails (4.2.2) coffee-script (>= 2.2.0) @@ -315,7 +437,7 @@ GEM coffee-script-source (1.12.2) colorize (0.8.1) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) @@ -330,14 +452,11 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3) + date (3.3.4) declarative (0.0.20) - declarative-builder (0.1.0) - declarative-option (< 0.2.0) - declarative-option (0.1.0) deprecation (1.1.0) activesupport - derivative-rodeo (0.5.0) + derivative-rodeo (0.5.3) activesupport (>= 5) aws-sdk-s3 aws-sdk-sqs @@ -346,7 +465,7 @@ GEM mime-types mini_magick nokogiri - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -354,18 +473,15 @@ GEM warden (~> 1.2.3) devise-guests (0.8.1) devise - devise-i18n (1.11.0) + devise-i18n (1.12.0) devise (>= 4.9.0) - devise_invitable (1.7.5) - actionmailer (>= 4.1.0) - devise (>= 4.0.0) + devise_invitable (2.0.9) + actionmailer (>= 5.0) + devise (>= 4.6) diff-lcs (1.5.0) - disposable (0.4.7) + disposable (0.6.3) declarative (>= 0.0.9, < 1.0.0) - declarative-builder (< 0.2.0) - declarative-option (< 0.2.0) - representable (>= 2.4.0, <= 3.1.0) - uber (< 0.2.0) + representable (>= 3.1.1, < 4) docile (1.4.0) docopt (0.5.0) draper (4.0.2) @@ -378,108 +494,123 @@ GEM dropbox_api (0.1.21) faraday (< 3.0) oauth2 (~> 1.1) - dry-configurable (0.16.1) - dry-core (~> 0.6) + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) dry-container (0.11.0) concurrent-ruby (~> 1.0) - dry-core (0.9.1) + dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) - dry-equalizer (0.3.0) - dry-events (0.2.0) + dry-events (1.0.1) concurrent-ruby (~> 1.0) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) - dry-inflector (0.3.0) + dry-core (~> 1.0, < 2) + dry-inflector (1.0.0) dry-initializer (3.1.1) - dry-logic (1.3.0) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.9, >= 0.9) + dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-matcher (0.9.0) - dry-core (~> 0.4, >= 0.4.8) - dry-monads (1.4.0) + dry-monads (1.6.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.7) - dry-schema (1.11.3) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-schema (1.13.3) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.16, >= 0.16) - dry-core (~> 0.9, >= 0.9) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-logic (~> 1.3) - dry-types (~> 1.6) + dry-logic (>= 1.4, < 2) + dry-types (>= 1.7, < 2) zeitwerk (~> 2.6) - dry-struct (1.5.2) - dry-core (~> 0.9, >= 0.9) - dry-types (~> 1.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) ice_nine (~> 0.11) zeitwerk (~> 2.6) - dry-transaction (0.13.3) - dry-container (>= 0.2.8) - dry-events (>= 0.1.0) - dry-matcher (>= 0.7.0) - dry-monads (>= 0.4.0) - dry-types (1.6.1) + dry-types (1.7.1) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.9, >= 0.9) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.3, >= 1.3) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) zeitwerk (~> 2.6) - dry-validation (1.9.0) + dry-validation (1.10.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.9, >= 0.9) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-schema (~> 1.11, >= 1.11.0) + dry-schema (>= 1.12, < 2) zeitwerk (~> 2.6) + e2mmap (0.1.0) easy_translate (0.5.1) thread thread_safe - ebnf (2.2.1) - amazing_print (~> 1.2) + ebnf (2.4.0) htmlentities (~> 4.3) - rdf (~> 3.1) + rdf (~> 3.3) scanf (~> 1.0) - sxp (~> 1.1) - unicode-types (~> 1.6) + sxp (~> 1.3) + unicode-types (~> 1.8) edtf (3.1.1) activesupport (>= 3.0, < 8.0) - equivalent-xml (0.6.0) - nokogiri (>= 1.4.3) erubi (1.12.0) + et-orbi (1.2.7) + tzinfo ethon (0.16.0) ffi (>= 1.15.0) excon (0.71.1) - execjs (2.8.1) - factory_bot (6.2.1) + execjs (2.9.1) + factory_bot (6.4.2) activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) + factory_bot_rails (6.4.2) + factory_bot (~> 6.4) railties (>= 5.0.0) - faraday (0.17.6) - multipart-post (>= 1.2, < 3) + faraday (2.7.12) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) faraday-encoding (0.0.5) faraday - faraday_middleware (0.14.0) - faraday (>= 0.7.4, < 1.0) + faraday-excon (2.1.0) + excon (>= 0.27.4) + faraday (~> 2.0) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-gzip (0.1.0) + faraday (>= 1.0) + zlib (~> 2.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (3.0.2) fcrepo_wrapper (0.9.0) ruby-progressbar - ffi (1.15.5) - flipflop (2.6.0) + ffi (1.16.3) + flipflop (2.7.1) activesupport (>= 4.0) + terminal-table (>= 1.8) flot-rails (0.0.7) jquery-rails flutie (2.2.0) font-awesome-rails (4.7.0.8) railties (>= 3.2, < 8.0) + fugit (1.9.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) gender_detector (0.1.2) unicode_utils (>= 1.3.0) - geocoder (1.8.1) - globalid (1.1.0) - activesupport (>= 5.0) - google-apis-core (0.11.0) + geo_coord (0.2.0) + geocoder (1.8.2) + globalid (1.2.1) + activesupport (>= 6.1) + good_job (2.99.0) + activejob (>= 5.2.0) + activerecord (>= 5.2.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 5.2.0) + thor (>= 0.14.1) + webrick (>= 1.3) + zeitwerk (>= 2.0) + google-apis-core (0.11.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -488,17 +619,17 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-drive_v3 (0.39.0) + google-apis-drive_v3 (0.46.0) google-apis-core (>= 0.11.0, < 2.a) - googleauth (1.5.2) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - haml (5.2.2) - temple (>= 0.8.0) + haml (6.3.0) + temple (>= 0.8.2) + thor tilt hamster (3.0.0) concurrent-ruby (~> 1.0) @@ -506,46 +637,46 @@ GEM hashie (5.0.0) highline (2.1.0) hiredis (0.6.3) - html_tokenizer (0.0.7) htmlentities (4.3.4) http_logger (0.7.0) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - hydra-access-controls (11.0.7) + hydra-access-controls (12.1.0) active-fedora (>= 10.0.0) - activesupport (>= 4, < 6) - blacklight (>= 5.16) - blacklight-access_controls (~> 0.6.0) - cancancan (~> 1.8) + activesupport (>= 5.2, < 7.1) + blacklight-access_controls (~> 6.0) + cancancan (>= 1.8, < 4) deprecation (~> 1.0) - hydra-core (11.0.7) - hydra-access-controls (= 11.0.7) - railties (>= 4.0.0, < 6) - hydra-derivatives (3.7.0) - active-fedora (>= 11.5.6, != 13.2.1, != 13.2.0, != 13.1.3, != 13.1.2, != 13.1.1, != 13.1.0, != 13.0.0, != 12.2.1, != 12.2.0, != 12.1.1, != 12.1.0, != 12.0.3, != 12.0.2, != 12.0.1, != 12.0.0) + hydra-core (12.1.0) + hydra-access-controls (= 12.1.0) + railties (>= 5.2, < 7.1) + hydra-derivatives (3.8.0) + active-fedora (>= 14.0) + active-triples (>= 1.2) active_encode (~> 0.1) - activesupport (>= 4.0, < 7) + activesupport (>= 4.0, < 7.1) addressable (~> 2.5) deprecation mime-types (> 2.0, < 4.0) mini_magick (>= 3.2, < 5) - hydra-editor (5.0.5) + hydra-editor (6.2.0) active-fedora (>= 9.0.0) - activerecord (~> 5.0) + activerecord (>= 5.2, < 7.1) almond-rails (~> 0.1) - cancancan (~> 1.8) - rails (>= 5, < 6) - simple_form (>= 4.1.0, < 6.0) - sprockets (~> 3.7) + cancancan + psych (~> 3.3, < 4) + rails (>= 5.2, < 7.1) + simple_form (>= 4.1.0, < 5.2) + sprockets (>= 3.7) sprockets-es6 - hydra-file_characterization (1.1.2) + hydra-file_characterization (1.2.0) activesupport (>= 3.0.0) - hydra-head (11.0.7) - hydra-access-controls (= 11.0.7) - hydra-core (= 11.0.7) - rails (>= 5.2, < 6.1) + hydra-head (12.1.0) + hydra-access-controls (= 12.1.0) + hydra-core (= 12.1.0) + rails (>= 5.2, < 7.1) hydra-pcdm (1.3.0) active-fedora (>= 10, < 15) mime-types (>= 1) @@ -555,64 +686,6 @@ GEM hydra-derivatives (~> 3.6) hydra-file_characterization (~> 1.0) hydra-pcdm (>= 0.9) - hyrax (3.5.0) - active-fedora (~> 13.1, >= 13.1.2) - almond-rails (~> 0.1) - awesome_nested_set (~> 3.1) - blacklight (~> 6.14) - blacklight-gallery (~> 0.7) - breadcrumbs_on_rails (~> 3.0) - browse-everything (>= 0.16, < 2.0) - carrierwave (~> 1.0) - clipboard-rails (~> 1.5) - draper (~> 4.0) - dry-equalizer (~> 0.2) - dry-events (~> 0.2.0) - dry-monads (< 1.5) - dry-struct (~> 1.0) - dry-transaction (~> 0.11) - dry-validation (~> 1.3) - flipflop (~> 2.3) - flot-rails (~> 0.0.6) - font-awesome-rails (~> 4.2) - hydra-derivatives (~> 3.3) - hydra-editor (~> 5.0, >= 5.0.4) - hydra-file_characterization (~> 1.1.2) - hydra-head (~> 11.0, >= 11.0.1) - hydra-works (>= 0.16) - iiif_manifest (>= 0.3, < 2.0) - jquery-datatables-rails (~> 3.4) - jquery-ui-rails (~> 6.0) - json-ld (< 3.2) - json-schema - kaminari_route_prefix (~> 0.1.1) - legato (~> 0.3) - linkeddata - mailboxer (~> 0.12) - nest (~> 3.1) - noid-rails (~> 3.0.0) - oauth - oauth2 (~> 1.2) - posix-spawn - power_converter (~> 0.1, >= 0.1.2) - psych (~> 3.3) - qa (~> 5.5, >= 5.5.1) - rails (~> 5.0) - rails_autolink (~> 1.1) - rdf-rdfxml - rdf-vocab (~> 3.0) - redis (~> 4.0) - redis-namespace (~> 1.5) - redlock (>= 0.1.2) - reform (~> 2.3) - reform-rails (~> 0.2.0) - retriable (>= 2.9, < 4.0) - samvera-nesting_indexer (~> 2.0) - sass-rails (~> 5.0) - select2-rails (~> 3.5) - signet - tinymce-rails (~> 5.10) - valkyrie (~> 2, >= 2.1.1) i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-debug (1.2.0) @@ -629,52 +702,50 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) ice_nine (0.11.2) - iiif-presentation (1.1.0) + iiif-image-api (0.2.0) + activesupport + iiif-presentation (1.3.0) activesupport (>= 3.2.18) - faraday (>= 0.9) + faraday (~> 2.7) + geo_coord json iiif_manifest (1.3.1) activesupport (>= 4) iso-639 (0.3.6) iso8601 (0.9.1) + jaro_winkler (1.5.6) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-datatables-rails (3.4.0) - actionpack (>= 3.1) - jquery-rails - railties (>= 3.1) - sass-rails - jquery-rails (4.5.1) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) - railties (>= 3.2.16) - json (2.6.3) + json (2.7.1) json-canonicalization (0.3.1) - json-jwt (1.15.3) + json-jwt (1.16.3) activesupport (>= 4.2) aes_key_wrap bindata - httpclient - json-ld (3.1.10) + faraday (~> 2.0) + faraday-follow_redirects + json-ld (3.2.4) htmlentities (~> 4.3) - json-canonicalization (~> 0.2) + json-canonicalization (~> 0.3) link_header (~> 0.0, >= 0.0.8) - multi_json (~> 1.14) - rack (~> 2.0) - rdf (~> 3.1) - json-ld-preloaded (3.1.6) - json-ld (~> 3.1) - rdf (~> 3.1) - json-schema (4.0.0) + multi_json (~> 1.15) + rack (>= 2.2, < 4) + rdf (~> 3.2, >= 3.2.10) + json-ld-preloaded (3.2.2) + json-ld (~> 3.2) + rdf (~> 3.2) + json-schema (4.1.1) addressable (>= 2.8) jsonlint (0.3.0) oj (~> 3) optimist (~> 3) - jwt (2.7.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -687,25 +758,28 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kaminari_route_prefix (0.1.1) - kaminari (~> 1.0) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) language_list (1.2.1) latex-decode (0.4.0) launchy (2.5.2) addressable (~> 2.8) - ld-patch (3.1.3) - ebnf (~> 2.1) - rdf (~> 3.1) - rdf-xsd (~> 3.1) - sparql (~> 3.1) - sxp (~> 1.1) - ldp (1.0.3) + ld-patch (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.3) + ldp (1.2.0) deprecation - faraday + faraday (>= 1) http_logger - json-ld - rdf (>= 1.1) + json-ld (~> 3.2) + rdf (~> 3.2) rdf-isomorphic + rdf-ldp rdf-turtle rdf-vocab (>= 0.8) slop @@ -718,44 +792,44 @@ GEM multi_json libxml-ruby (3.2.4) link_header (0.0.8) - linkeddata (3.1.6) - equivalent-xml (~> 0.6) - json-ld (~> 3.1, >= 3.1.10) - json-ld-preloaded (~> 3.1, >= 3.1.6) - ld-patch (~> 3.1, >= 3.1.3) - nokogiri (~> 1.12) - rdf (~> 3.1, >= 3.1.15) - rdf-aggregate-repo (~> 3.1) - rdf-isomorphic (~> 3.1, >= 3.1.1) - rdf-json (~> 3.1) - rdf-microdata (~> 3.1, >= 3.1.4) - rdf-n3 (~> 3.1, >= 3.1.2) - rdf-normalize (~> 0.4) - rdf-ordered-repo (~> 3.1, >= 3.1.1) - rdf-rdfa (~> 3.1, >= 3.1.3) - rdf-rdfxml (~> 3.1, >= 3.1.1) - rdf-reasoner (~> 0.7, >= 0.7.2) - rdf-tabular (~> 3.1, >= 3.1.1) - rdf-trig (~> 3.1, >= 3.1.2) - rdf-trix (~> 3.1, >= 3.1.1) - rdf-turtle (~> 3.1, >= 3.1.3) - rdf-vocab (~> 3.1, >= 3.1.14) - rdf-xsd (~> 3.1, >= 3.1.1) - shacl (~> 0.1, >= 0.1.1) - shex (~> 0.6, >= 0.6.4) - sparql (~> 3.1, >= 3.1.8) - sparql-client (~> 3.1, >= 3.1.2) - listen (3.1.5) + linkeddata (3.2.1) + json-ld (~> 3.2, >= 3.2.3) + json-ld-preloaded (~> 3.2) + ld-patch (~> 3.2) + nokogiri (~> 1.13, >= 1.13.8) + rdf (~> 3.2, >= 3.2.9) + rdf-aggregate-repo (~> 3.2, >= 3.2.1) + rdf-hamster-repo (~> 3.2) + rdf-isomorphic (~> 3.2, >= 3.2.1) + rdf-json (~> 3.2) + rdf-microdata (~> 3.2, >= 3.2.1) + rdf-n3 (~> 3.2, >= 3.2.1) + rdf-normalize (~> 0.5) + rdf-ordered-repo (~> 3.2, >= 3.2.1) + rdf-rdfa (~> 3.2) + rdf-rdfxml (~> 3.2) + rdf-reasoner (~> 0.8) + rdf-tabular (~> 3.2, >= 3.2.1) + rdf-trig (~> 3.2) + rdf-trix (~> 3.2) + rdf-turtle (~> 3.2, >= 3.2.1) + rdf-vocab (~> 3.2, >= 3.2.1) + rdf-xsd (~> 3.2, >= 3.2.1) + shacl (~> 0.2, >= 0.2.1) + shex (~> 0.7, >= 0.7.1) + sparql (~> 3.2, >= 3.2.4) + sparql-client (~> 3.2, >= 3.2.1) + yaml-ld (~> 0.0) + listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - logger (1.5.3) - lograge (0.12.0) + logger (1.6.0) + lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -767,27 +841,29 @@ GEM carrierwave (>= 0.5.8) rails (>= 5.0.0) marcel (1.0.2) - maremma (4.9.8) + maremma (4.9.9) activesupport (>= 4.2.5) addressable (>= 2.3.6) builder (~> 3.2, >= 3.2.2) excon (~> 0.71.0) - faraday (~> 0.17.3) - faraday-encoding (~> 0.0.4) - faraday_middleware (~> 0.14.0) - nokogiri (>= 1.11.2, < 1.14.0) + faraday (>= 2.0) + faraday-encoding (~> 0.0.5) + faraday-excon (~> 2.1.0) + faraday-follow_redirects (~> 0.3.0) + faraday-gzip (~> 0.1.0) + faraday-multipart (~> 1.0.4) + nokogiri (>= 1.13.1, < 1.14.0) oj (>= 2.8.3) oj_mimic_json (~> 1.0, >= 1.0.1) matrix (0.4.2) - memoist (0.16.2) method_source (1.0.0) - mime-types (3.4.1) + mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) + mime-types-data (3.2023.1205) mini_magick (4.12.0) - mini_mime (1.1.2) - mini_portile2 (2.8.4) - minitest (5.18.1) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + minitest (5.20.0) mods (2.4.1) edtf iso-639 @@ -804,19 +880,19 @@ GEM redic net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.3.6) + net-imap (0.4.9) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol - nio4r (2.5.9) + nio4r (2.7.0) noid (0.9.0) - noid-rails (3.0.3) - actionpack (>= 5.0.0, < 7) + noid-rails (3.1.0) + actionpack (>= 5.0.0, < 7.1) noid (~> 0.9) nokogiri (1.13.10) mini_portile2 (~> 2.8.0) @@ -824,10 +900,10 @@ GEM nom-xml (1.2.0) i18n nokogiri - oai (1.1.0) + oai (1.2.1) builder (>= 3.1.0) - faraday - faraday_middleware + faraday (< 3) + faraday-follow_redirects (>= 0.3.0, < 2) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) snaky_hash (~> 2.0) @@ -840,9 +916,10 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 4) - oj (3.14.3) + oj (3.16.3) + bigdecimal (>= 3.0) oj_mimic_json (1.0.1) - okcomputer (1.18.4) + okcomputer (1.18.5) omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) @@ -855,37 +932,42 @@ GEM omniauth-saml (2.1.0) omniauth (~> 2.0) ruby-saml (~> 1.12) - omniauth_openid_connect (0.6.1) + omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) - openid_connect (~> 1.1) - openid_connect (1.4.2) + openid_connect (~> 2.2) + openid_connect (2.2.0) activemodel attr_required (>= 1.0.0) - json-jwt (>= 1.15.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) net-smtp - rack-oauth2 (~> 1.21) - swd (~> 1.3) + rack-oauth2 (~> 2.2) + swd (~> 2.0) tzinfo validate_email validate_url - webfinger (~> 1.2) + webfinger (~> 2.0) openseadragon (0.6.0) rails (> 3.2.0) - optimist (3.0.1) + optimist (3.1.0) + order_already (0.3.1) + rails-html-sanitizer (~> 1.4) orm_adapter (0.5.0) os (1.1.4) + ostruct (0.6.0) parallel (1.23.0) - parser (2.5.3.0) - ast (~> 2.4.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc parslet (2.0.0) - pg (1.5.3) + pg (1.5.4) + popper_js (1.16.1) posix-spawn (0.3.15) postrank-uri (1.1) addressable (>= 2.4.0) nokogiri (>= 1.8.0) public_suffix (>= 4.0.0, < 5) - power_converter (0.1.2) - powerpack (0.1.3) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -896,158 +978,150 @@ GEM public_suffix (4.0.7) puma (5.6.7) nio4r (~> 2.0) - qa (5.10.0) + qa (5.11.0) activerecord-import deprecation faraday (< 3.0, != 2.0.0) geocoder ldpath nokogiri (~> 1.6) - rails (>= 5.0, < 7.1) + rails (>= 5.0, < 7.2) rdf - racc (1.7.1) + raabro (1.4.0) + racc (1.7.3) rack (2.2.8) - rack-oauth2 (1.21.3) + rack-oauth2 (2.2.0) activesupport attr_required - httpclient + faraday (~> 2.0) + faraday-follow_redirects json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.0.6) - rack + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (0.7.0) rack (>= 1.0, < 3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) - sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.1.1) + rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) - rails-i18n (5.1.3) + rails-i18n (7.0.8) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) + railties (>= 6.0.0, < 8) rails_autolink (1.1.8) actionview (> 3.1) activesupport (> 3.1) railties (> 3.1) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdf (3.1.15) - hamster (~> 3.0) + rdf (3.3.1) + bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) - rdf-aggregate-repo (3.1.0) - rdf (~> 3.1) - rdf-isomorphic (3.1.1) - rdf (~> 3.1) - rdf-json (3.1.0) - rdf (~> 3.1) - rdf-microdata (3.1.4) + rdf-aggregate-repo (3.3.0) + rdf (~> 3.3) + rdf-hamster-repo (3.3.0) + hamster (~> 3.0) + rdf (~> 3.3) + rdf-isomorphic (3.3.0) + rdf (~> 3.3) + rdf-json (3.3.0) + rdf (~> 3.3) + rdf-ldp (0.1.0) + deprecation + rdf + rdf-microdata (3.2.1) htmlentities (~> 4.3) - nokogiri (~> 1.12) - rdf (~> 3.1, >= 3.1.13) - rdf-rdfa (~> 3.1, >= 3.1.3) - rdf-xsd (~> 3.1) - rdf-n3 (3.1.2) - ebnf (~> 2.1) - rdf (~> 3.1, >= 3.1.8) - sparql (~> 3.1, >= 3.1.4) - sxp (~> 1.1) - rdf-normalize (0.4.0) - rdf (~> 3.1) - rdf-ordered-repo (3.1.1) - rdf (~> 3.1) - rdf-rdfa (3.1.3) - haml (~> 5.2) + nokogiri (~> 1.13) + rdf (~> 3.2) + rdf-rdfa (~> 3.2) + rdf-xsd (~> 3.2) + rdf-n3 (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.3) + rdf-normalize (0.7.0) + rdf (~> 3.3) + rdf-ordered-repo (3.3.0) + rdf (~> 3.3) + rdf-rdfa (3.3.0) + haml (~> 6.1) htmlentities (~> 4.3) - rdf (~> 3.1, >= 3.1.13) - rdf-aggregate-repo (~> 3.1) - rdf-vocab (~> 3.1, >= 3.1.11) - rdf-xsd (~> 3.1) - rdf-rdfxml (3.1.1) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) + rdf-rdfxml (3.3.0) + builder (~> 3.2, >= 3.2.4) htmlentities (~> 4.3) - rdf (~> 3.1) - rdf-rdfa (~> 3.1) - rdf-xsd (~> 3.1) - rdf-reasoner (0.7.2) - rdf (~> 3.1, >= 3.1.12) - rdf-xsd (~> 3.1) - rdf-tabular (3.1.1) - addressable (~> 2.3) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-reasoner (0.9.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-tabular (3.2.1) + addressable (~> 2.8) bcp47 (~> 0.3, >= 0.3.3) - json-ld (~> 3.1) - rdf (~> 3.1) - rdf-vocab (~> 3.1) - rdf-xsd (~> 3.1) - rdf-trig (3.1.2) - ebnf (~> 2.1) - rdf (~> 3.1) - rdf-turtle (~> 3.1) - rdf-trix (3.1.1) - rdf (~> 3.1) - rdf-xsd (~> 3.1) - rdf-turtle (3.1.3) - ebnf (~> 2.1) - rdf (~> 3.1, >= 3.1.8) - rdf-vocab (3.1.14) - rdf (~> 3.1, >= 3.1.12) - rdf-xsd (3.1.1) - rdf (~> 3.1) + json-ld (~> 3.2) + rdf (~> 3.2, >= 3.2.7) + rdf-vocab (~> 3.2) + rdf-xsd (~> 3.2) + rdf-trig (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-turtle (~> 3.3) + rdf-trix (3.3.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-turtle (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-vocab (3.3.0) + rdf (~> 3.3) + rdf-xsd (3.3.0) + rdf (~> 3.3) rexml (~> 3.2) redic (1.5.3) hiredis redis (4.8.1) - redis-namespace (1.10.0) + redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - reform (2.5.0) - disposable (>= 0.4.2, < 0.5.0) - representable (>= 2.4.0, < 3.1.0) + reform (2.6.2) + disposable (>= 0.5.0, < 1.0.0) + representable (>= 3.1.1, < 4) uber (< 0.2.0) - reform-rails (0.2.3) + reform-rails (0.2.6) activemodel (>= 5.0) reform (>= 2.3.1, < 3.0.0) - regexp_parser (2.8.0) - representable (3.0.4) + regexp_parser (2.8.3) + representable (3.2.0) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) request_store (1.5.1) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) - rexml (3.2.5) - riiif (1.7.1) + reverse_markdown (2.1.1) + nokogiri + rexml (3.2.6) + riiif (2.4.0) deprecation (>= 1.0.0) - railties (>= 4.2, < 6) + iiif-image-api (>= 0.1.0) + railties (>= 4.2, < 8) rolify (6.0.1) rsolr (2.5.0) builder (>= 2.1.2) @@ -1056,7 +1130,7 @@ GEM rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-activemodel-mocks (1.1.0) + rspec-activemodel-mocks (1.2.0) activemodel (>= 3.0) activesupport (>= 3.0) rspec-mocks (>= 2.99, < 4.0) @@ -1068,29 +1142,40 @@ GEM rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.5) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-rails (6.1.0) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.12.0) + rspec-support (3.12.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.52.1) + rubocop (1.28.2) parallel (~> 1.10) - parser (>= 2.4.0.2, < 3.0) - powerpack (~> 0.1) + parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-performance (1.19.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.15.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.7.0, < 2.0) rubocop-rspec (1.22.2) rubocop (>= 0.52.1) ruby-box (1.15.0) @@ -1099,27 +1184,26 @@ GEM multipart-post oauth2 ruby-progressbar (1.13.0) - ruby-saml (1.15.0) + ruby-saml (1.16.0) nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) - ruby_dep (1.5.0) rubyzip (2.3.2) - samvera-nesting_indexer (2.0.0) - dry-equalizer sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt scanf (1.0.0) scss_lint (0.60.0) sass (~> 3.5, >= 3.5.5) @@ -1129,34 +1213,32 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-raven (2.13.0) - faraday (>= 0.7.6, < 1.0) - shacl (0.1.1) - json-ld (~> 3.1, >= 3.1.7) - rdf (~> 3.1, >= 3.1.8) - sparql (~> 3.1) - sxp (~> 1.1) - shex (0.6.4) - ebnf (~> 2.1, >= 2.2) + shacl (0.3.0) + json-ld (~> 3.2) + rdf (~> 3.2, >= 3.2.8) + sparql (~> 3.2, >= 3.2.4) + sxp (~> 1.2) + shex (0.7.1) + ebnf (~> 2.2) htmlentities (~> 4.3) - json-ld (~> 3.1) - json-ld-preloaded (~> 3.1) - rdf (~> 3.1) - rdf-xsd (~> 3.1) - sparql (~> 3.1) - sxp (~> 1.1) + json-ld (~> 3.2) + json-ld-preloaded (~> 3.2) + rdf (~> 3.2) + rdf-xsd (~> 3.2) + sparql (~> 3.2) + sxp (~> 1.2) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) - sidekiq (6.5.9) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) - signet (0.17.0) + signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simple_form (5.2.0) + simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) simplecov (0.22.0) @@ -1170,23 +1252,38 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) + solargraph (0.48.0) + backport (~> 1.2) + benchmark + bundler (>= 1.17.2) + diff-lcs (~> 1.4) + e2mmap + jaro_winkler (~> 1.5) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.1) + parser (~> 3.0) + reverse_markdown (>= 1.0.5, < 3) + rubocop (>= 0.52) + thor (~> 1.0) + tilt (~> 2.0) + yard (~> 0.9, >= 0.9.24) solr_wrapper (2.2.0) faraday retriable ruby-progressbar rubyzip - sparql (3.1.8) - builder (~> 3.2) - ebnf (~> 2.1) - logger (~> 1.4) - rdf (~> 3.1, >= 3.1.14) - rdf-aggregate-repo (~> 3.1) - rdf-xsd (~> 3.1) - sparql-client (~> 3.1, >= 3.1.2) - sxp (~> 1.1) - sparql-client (3.1.2) - net-http-persistent (~> 4.0, >= 4.0.1) - rdf (~> 3.1) + sparql (3.3.0) + builder (~> 3.2, >= 3.2.4) + ebnf (~> 2.4) + logger (~> 1.5) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-xsd (~> 3.3) + sparql-client (~> 3.3) + sxp (~> 1.3) + sparql-client (3.3.0) + net-http-persistent (~> 4.0, >= 4.0.2) + rdf (~> 3.3) spring (1.7.2) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -1203,26 +1300,29 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) ssrf_filter (1.0.8) - swd (1.3.0) + swd (2.0.2) activesupport (>= 3) attr_required (>= 0.0.5) - httpclient (>= 2.4) - sxp (1.1.0) - rdf (~> 3.1) - temple (0.10.0) + faraday (~> 2.0) + faraday-follow_redirects + sxp (1.3.0) + matrix (~> 0.4) + rdf (~> 3.3) + temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terser (1.1.14) + terser (1.1.20) execjs (>= 0.3.0, < 3) tether-rails (1.4.0) rails (>= 3.1) - thor (1.2.2) + thor (1.3.0) thread (0.2.2) thread_safe (0.3.6) - tilt (2.1.0) - timeout (0.4.0) - tinymce-rails (5.10.7) + tilt (2.3.0) + timeout (0.4.1) + tinymce-rails (5.10.9) railties (>= 3.1.1) + trailblazer-option (0.1.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -1230,13 +1330,13 @@ GEM actionpack (>= 3.1) jquery-rails railties (>= 3.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) - tzinfo (1.2.11) - thread_safe (~> 0.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) uber (0.1.0) unicode-display_width (1.8.0) - unicode-types (1.8.0) + unicode-types (1.9.0) unicode_utils (1.4.0) validatable (1.6.7) validate_email (0.1.6) @@ -1245,14 +1345,13 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix - valkyrie (2.2.0) + valkyrie (3.1.1) activemodel activesupport - disposable (~> 0.4.5) - draper dry-struct dry-types (~> 1.0) - faraday (< 1.0) + faraday (>= 0.9, < 3, != 2.0.0) + faraday-multipart json json-ld railties @@ -1260,49 +1359,66 @@ GEM rdf-vocab reform (~> 2.2) reform-rails - version_gem (1.1.2) + version_gem (1.1.3) + view_component (2.74.1) + activesupport (>= 5.0.0, < 8.0) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) webdrivers (4.7.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (> 3.141, < 5.0) - webfinger (1.2.0) + webfinger (2.1.2) activesupport - httpclient (>= 2.4) - webmock (3.18.1) + faraday (~> 2.0) + faraday-follow_redirects + webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) - websocket (1.2.9) - websocket-driver (0.7.5) + websocket (1.2.10) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + yaml-ld (0.0.2) + json-ld (~> 3.2, >= 3.2.3) + psych (>= 3.3) + rdf (~> 3.2, >= 3.2.9) + rdf-xsd (~> 3.2) + yard (0.9.34) + zeitwerk (2.6.12) + zlib (2.1.1) PLATFORMS - ruby + aarch64-linux-musl + x86_64-linux-musl DEPENDENCIES - active-fedora (>= 11.1.4) + active-fedora (~> 14.0) active_elastic_job! activerecord-nulldb-adapter addressable (= 2.8.1) - apartment + apartment! aws-sdk-sqs - blacklight (~> 6.7) - blacklight_oai_provider (~> 6.1, >= 6.1.1) + bixby (~> 5.0, >= 5.0.2) + blacklight (~> 7.29) + blacklight_advanced_search + blacklight_oai_provider (~> 7.0) + blacklight_range_limit bolognese (>= 1.9.10) + bootstrap (~> 4.6) bootstrap-datepicker-rails - bulkrax (~> 5.3) + bulkrax (~> 5.4) byebug capybara capybara-screenshot (~> 1.0) @@ -1314,15 +1430,16 @@ DEPENDENCIES devise devise-guests (~> 0.3) devise-i18n - devise_invitable (~> 1.6) - dry-monads (~> 1.4.0) + devise_invitable (~> 2.0) + dry-monads (~> 1.5) easy_translate factory_bot_rails fcrepo_wrapper (~> 0.4) - flipflop (~> 2.6.0) flutie + good_job (~> 2.99) + googleauth (= 1.8.1) hyku_knapsack! - hyrax (~> 3.5.0) + hyrax! hyrax-doi! hyrax-iiif_av! i18n-debug @@ -1342,17 +1459,19 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.1) omniauth_openid_connect - parser (~> 2.5.3) + order_already + parser (>= 3.1.0.0) pg postrank-uri (>= 1.0.24) pry-byebug puma (~> 5.6) rack-test (= 0.7.0) - rails (~> 5.2.5) + rails (~> 6.0)! rails-controller-testing - rdf (~> 3.1.15) + rdf (~> 3.2) + redis-namespace (~> 1.10) redlock (>= 0.1.2, < 2.0) - riiif (~> 1.1) + riiif (~> 2.0) rolify rsolr (~> 2.0) rspec @@ -1361,24 +1480,27 @@ DEPENDENCIES rspec-rails (>= 3.6.0) rspec-retry rspec_junit_formatter - rubocop (~> 0.50, <= 0.52.1) + rubocop (= 1.28.2) + rubocop-rails (~> 2.15) rubocop-rspec (~> 1.22, <= 1.22.2) - sass-rails (~> 5.0) + sass-rails (~> 6.0) scss_lint secure_headers selenium-webdriver (= 4.8.1) shoulda-matchers (~> 4.0) sidekiq (< 7.0) simplecov + solargraph solr_wrapper (~> 2.0) spring (~> 1.7) spring-watcher-listen (~> 2.0.0) terser tether-rails turbolinks (~> 5) + twitter-typeahead-rails (= 0.11.1.pre.corejavascript) web-console (>= 3.3.0) webdrivers (~> 4.7.0) webmock BUNDLED WITH - 2.4.14 + 2.4.21 diff --git a/README.md b/README.md index 1be23b504e..d666a9f050 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ More info about configuring and using bulkrax can be found [here](https://github ### Commandline Importers -Importing from CSV and PURL directly can be done via Bulkrax and the built in code in Hyku is slated for deletion in the next release. +Importing from CSV and PURL directly can be done via Bulkrax. ## Compatibility diff --git a/Rakefile b/Rakefile index f55260d5d4..ac6b3fbc94 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. @@ -15,7 +17,9 @@ Rails.application.load_tasks begin require 'solr_wrapper/rake_task' +# rubocop:disable Lint/SuppressedException rescue LoadError + # rubocop:enable Lint/SuppressedException end task :ci do diff --git a/app/actors/hyrax/environment.rb b/app/actors/hyrax/environment_decorator.rb similarity index 51% rename from app/actors/hyrax/environment.rb rename to app/actors/hyrax/environment_decorator.rb index 814895f87a..257f66b118 100644 --- a/app/actors/hyrax/environment.rb +++ b/app/actors/hyrax/environment_decorator.rb @@ -1,25 +1,21 @@ # frozen_string_literal: true -# OVERRIDE Hyrax 2.9 to add in import flag +# OVERRIDE Hyrax v5.0.0rc2 to add in import flag + module Hyrax module Actors - class Environment + module EnvironmentDecoractor # @param [ActiveFedora::Base] curation_concern work to operate on # @param [Ability] current_ability the authorizations of the acting user # @param [ActionController::Parameters] attributes user provided form attributes def initialize(curation_concern, current_ability, attributes, importing = false) - @curation_concern = curation_concern - @current_ability = current_ability - @attributes = attributes.to_h.with_indifferent_access @importing = importing + super(curation_concern, current_ability, attributes) end - attr_reader :curation_concern, :current_ability, :attributes, :importing - - # @return [User] the user from the current_ability - def user - current_ability.current_user - end + attr_reader :importing end end end + +Hyrax::Actors::Environment.prepend(Hyrax::Actors::EnvironmentDecoractor) diff --git a/app/assets/javascripts/admin_color_select.js b/app/assets/javascripts/admin_color_select.js new file mode 100644 index 0000000000..371fc0b427 --- /dev/null +++ b/app/assets/javascripts/admin_color_select.js @@ -0,0 +1,20 @@ +$(document).on('turbolinks:load', function() { + $('div.defaultable-colors a.restore-default-color').click(function(e) { + e.preventDefault() + + var defaultTarget = $(e.target).data('default-target') + var input = $("input[name='admin_appearance["+ defaultTarget +"]']") + + input.val(input.data('default-value')) + }) + + $('.panel-footer a.restore-all-default-colors').click(function(e) { + e.preventDefault() + + var allColorInputs = $("input[name*='color']") + + allColorInputs.each(function() { + $(this).val($(this).data('default-value')) + }) + }) +}); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index df9e83c18a..1f6185b0cf 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -10,14 +10,14 @@ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // -//= require codemirror -//= require codemirror-autorefresh -//= require codemirror/modes/css //= require jquery3 -//= require jquery_ujs +//= require rails-ujs +//= require popper +//= require twitter/typeahead +//= require bootstrap //= require jquery.fontselect -//= require dataTables/jquery.dataTables -//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap +//= require jquery.dataTables +//= require dataTables.bootstrap4 //= require stat_slider //= require turbolinks //= require cocoon @@ -25,6 +25,9 @@ //= require tether // Required by Blacklight //= require blacklight/blacklight +//= require blacklight_gallery +//= require admin_color_select +//= require blacklight_advanced_search // Moved the Hyku JS *above* the Hyrax JS to resolve #1187 (following // a pattern found in ScholarSphere) @@ -42,9 +45,23 @@ //= require bulkrax/application //= require hyrax + +//= require codemirror +//= require codemirror-autorefresh +//= require codemirror/modes/css + //= require iiif_print //= require jquery.flot.pie //= require flot_graph //= require statistics_tab_manager //= require blacklight_gallery/default + +// Required for blacklight range limit +//= require blacklight_range_limit/range_limit_distro_facets +//= require blacklight_range_limit/range_limit_shared +//= require blacklight_range_limit/range_limit_slider +//= require bootstrap-slider +//= require jquery.flot.js + +//= require tinymce diff --git a/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js b/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js new file mode 100644 index 0000000000..1133b8dec8 --- /dev/null +++ b/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js @@ -0,0 +1,348 @@ +// for Blacklight.onLoad: + +/* A custom event "plotDrawn.blacklight.rangeLimit" will be sent when flot plot + is (re-)drawn on screen possibly with a new size. target of event will be the DOM element + containing the plot. Used to resize slider to match. */ + +Blacklight.onLoad(function () { + // ratio of width to height for desired display, multiply width by this ratio + // to get height. hard-coded in for now. + var display_ratio = 1 / (1.618 * 2); // half a golden rectangle, why not + var redrawnEvent = "plotDrawn.blacklight.rangeLimit"; + + // Facets already on the page? Turn em into a chart. + $(".range_limit .profile .distribution.chart_js ul").each(function () { + turnIntoPlot($(this).parent()); + }); + + // Add AJAX fetched range facets if needed, and add a chart to em + $(".range_limit .profile .distribution a.load_distribution").each( + function () { + var container = $(this).parent("div.distribution"); + + $(container).load($(this).attr("href"), function (response, status) { + if ($(container).hasClass("chart_js") && status == "success") { + turnIntoPlot(container); + } + }); + } + ); + + // Listen for twitter bootstrap collapsible open events, to render flot + // in previously hidden divs on open, if needed. + $("body").on("show.bs.collapse", function (event) { + // Was the target a .facet-content including a .chart-js? + var container = $(event.target).filter(".facet-content").find(".chart_js"); + + // only if it doesn't already have a canvas, it isn't already drawn + if (container && container.find("canvas").length == 0) { + // be willing to wait up to 1100ms for container to + // have width -- right away on show.bs is too soon, but + // shown.bs is later than we want, we want to start rendering + // while animation is still in progress. + turnIntoPlot(container, 1100); + } + }); + + // after a collapsible facet contents is fully shown, + // resize the flot chart to current conditions. This way, if you change + // browser window size, you can get chart resized to fit by closing and opening + // again, if needed. + + function redrawPlot(container) { + if (container && container.width() > 0) { + // resize the container's height, since width may have changed. + container.height(container.width() * display_ratio); + + // redraw the chart. + var plot = container.data("plot"); + if (plot) { + // how to redraw after possible resize? + // Cribbed from https://github.com/flot/flot/blob/master/jquery.flot.resize.js + plot.resize(); + plot.setupGrid(); + plot.draw(); + // plus trigger redraw of the selection, which otherwise ain't always right + // we'll trigger a fake event on one of the boxes + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin").trigger("change"); + + // send our custom event to trigger redraw of slider + $(container).trigger(redrawnEvent); + } + } + } + + $("body").on("shown.bs.collapse", function (event) { + var container = $(event.target).filter(".facet-content").find(".chart_js"); + redrawPlot(container); + }); + + // debouce borrowed from underscore + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + debounce = function (func, wait, immediate) { + var timeout; + return function () { + var context = this, + args = arguments; + var later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + }; + + $(window).on( + "resize", + debounce(function () { + $(".chart_js").each(function (i, container) { + redrawPlot($(container)); + }); + }, 350) + ); + + // second arg, if provided, is a number of ms we're willing to + // wait for the container to have width before giving up -- we'll + // set 50ms timers to check back until timeout is expired or the + // container is finally visible. The timeout is used when we catch + // bootstrap show event, but the animation hasn't barely begun yet -- but + // we don't want to wait until it's finished, we want to start rendering + // as soon as we can. + // + // We also will + function turnIntoPlot(container, wait_for_visible) { + // flot can only render in a a div with a defined width. + // for instance, a hidden div can't generally be rendered in (although if you set + // an explicit width on it, it might work) + // + // We'll count on later code that catch bootstrap collapse open to render + // on show, for currently hidden divs. + + // for some reason width sometimes return negative, not sure + // why but it's some kind of hidden. + if (container.width() > 0) { + var height = container.width() * display_ratio; + + // Need an explicit height to make flot happy. + container.height(height); + + areaChart($(container)); + + $(container).trigger(redrawnEvent); + } else if (wait_for_visible > 0) { + setTimeout(function () { + turnIntoPlot(container, wait_for_visible - 50); + }, 50); + } + } + + // Takes a div holding a ul of distribution segments produced by + // blacklight_range_limit/_range_facets and makes it into + // a flot area chart. + function areaChart(container) { + //flot loaded? And canvas element supported. + if (domDependenciesMet()) { + // Grab the data from the ul div + var series_data = new Array(); + var pointer_lookup = new Array(); + var x_ticks = new Array(); + var min = BlacklightRangeLimit.parseNum( + $(container).find("ul li:first-child span.from").text() + ); + var max = BlacklightRangeLimit.parseNum( + $(container).find("ul li:last-child span.to").text() + ); + + $(container) + .find("ul li") + .each(function () { + var from = BlacklightRangeLimit.parseNum( + $(this).find("span.from").text() + ); + var to = BlacklightRangeLimit.parseNum( + $(this).find("span.to").text() + ); + var count = BlacklightRangeLimit.parseNum( + $(this).find("span.count").text() + ); + var avg = count / (to - from + 1); + + //We use the avg as the y-coord, to make the area of each + //segment proportional to how many documents it holds. + series_data.push([from, avg]); + series_data.push([to + 1, avg]); + + x_ticks.push(from); + + pointer_lookup.push({ + from: from, + to: to, + count: count, + label: $(this).find(".facet_select").text(), + }); + }); + var max_plus_one = + BlacklightRangeLimit.parseNum( + $(container).find("ul li:last-child span.to").text() + ) + 1; + x_ticks.push(max_plus_one); + + var plot; + var config = + $(container).closest(".facet_limit").data("plot-config") || {}; + + try { + plot = $.plot( + $(container), + [series_data], + $.extend(true, config, { + yaxis: { ticks: [], min: 0, autoscaleMargin: 0.1 }, + //xaxis: { ticks: x_ticks }, + xaxis: { tickDecimals: 0 }, // force integer ticks + series: { lines: { fill: true, steps: true } }, + grid: { clickable: true, hoverable: true, autoHighlight: false }, + selection: { mode: "x" }, + }) + ); + } catch (err) { + alert(err); + } + + find_segment_for = function_for_find_segment(pointer_lookup); + var last_segment = null; + $(container).tooltip({ + placement: "bottom", + trigger: "manual", + delay: { show: 0, hide: 100 }, + }); + + $(container).bind("plothover", function (event, pos, item) { + segment = find_segment_for(pos.x); + + if (segment != last_segment) { + var title = + find_segment_for(pos.x).label + + " (" + + BlacklightRangeLimit.parseNum(segment.count) + + ")"; + $(container) + .attr("title", title) + .tooltip("_fixTitle") + .tooltip("show"); + + last_segment = segment; + } + }); + + $(container).bind("mouseout", function () { + last_segment = null; + $(container).tooltip("hide"); + }); + $(container).bind("plotclick", function (event, pos, item) { + if (plot.getSelection() == null) { + segment = find_segment_for(pos.x); + plot.setSelection(normalized_selection(segment.from, segment.to)); + } + }); + $(container).bind("plotselected plotselecting", function (event, ranges) { + if (ranges != null) { + var from = Math.floor(ranges.xaxis.from); + var to = Math.floor(ranges.xaxis.to); + + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin").val(from); + form.find("input.range_end").val(to); + + var slider_placeholder = $(container) + .closest(".limit_content") + .find("[data-slider-placeholder]"); + if (slider_placeholder) { + slider_placeholder.slider("setValue", [from, to + 1]); + } + } + }); + + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin, input.range_end").change(function () { + plot.setSelection(form_selection(form, min, max), true); + }); + $(container) + .closest(".limit_content") + .find(".profile .range") + .on("slide", function (event, ui) { + var values = $(event.target).data("slider").getValue(); + form.find("input.range_begin").val(values[0]); + form.find("input.range_end").val(values[1]); + plot.setSelection( + normalized_selection(values[0], Math.max(values[0], values[1] - 1)), + true + ); + }); + + // initially entirely selected, to match slider + plot.setSelection({ xaxis: { from: min, to: max + 0.9999 } }); + } + } + + // Send endpoint to endpoint+0.99999 to have display + // more closely approximate limiting behavior esp + // at small resolutions. (Since we search on whole numbers, + // inclusive, but flot chart is decimal.) + function normalized_selection(min, max) { + max += 0.99999; + + return { xaxis: { from: min, to: max } }; + } + + function form_selection(form, min, max) { + var begin_val = BlacklightRangeLimit.parseNum( + $(form).find("input.range_begin").val() + ); + if (isNaN(begin_val) || begin_val < min) { + begin_val = min; + } + var end_val = BlacklightRangeLimit.parseNum( + $(form).find("input.range_end").val() + ); + if (isNaN(end_val) || end_val > max) { + end_val = max; + } + + return normalized_selection(begin_val, end_val); + } + + function function_for_find_segment(pointer_lookup_arr) { + return function (x_coord) { + for (var i = pointer_lookup_arr.length - 1; i >= 0; i--) { + var hash = pointer_lookup_arr[i]; + if (x_coord >= hash.from) return hash; + } + return pointer_lookup_arr[0]; + }; + } + + // Check if Flot is loaded, and if browser has support for + // canvas object, either natively or via IE excanvas. + function domDependenciesMet() { + var flotLoaded = typeof $.plot != "undefined"; + var canvasAvailable = + typeof document.createElement("canvas").getContext != "undefined" || + typeof window.CanvasRenderingContext2D != "undefined" || + typeof G_vmlCanvasManager != "undefined"; + + return flotLoaded && canvasAvailable; + } +}); diff --git a/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js b/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js new file mode 100644 index 0000000000..74aef9e9e4 --- /dev/null +++ b/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js @@ -0,0 +1,24 @@ + +// takes a string and parses into an integer, but throws away commas first, to avoid truncation when there is a comma +// use in place of javascript's native parseInt +!function(global) { + 'use strict'; + + var previousBlacklightRangeLimit = global.BlacklightRangeLimit; + + function BlacklightRangeLimit(options) { + this.options = options || {}; + } + + BlacklightRangeLimit.parseNum = function parseNum(str) { + str = String(str).replace(/[^0-9]/g, ''); + return parseInt(str, 10); + }; + + BlacklightRangeLimit.noConflict = function noConflict() { + global.BlacklightRangeLimit = previousBlacklightRangeLimit; + return BlacklightRangeLimit; + }; + + global.BlacklightRangeLimit = BlacklightRangeLimit; +}(this); diff --git a/app/assets/javascripts/blacklight_range_limit/range_limit_slider.js b/app/assets/javascripts/blacklight_range_limit/range_limit_slider.js new file mode 100644 index 0000000000..e294642299 --- /dev/null +++ b/app/assets/javascripts/blacklight_range_limit/range_limit_slider.js @@ -0,0 +1,130 @@ +// for Blacklight.onLoad: + +Blacklight.onLoad(function() { + + $(".range_limit .profile .range.slider_js").each(function() { + var range_element = $(this); + + var boundaries = min_max(this); + var min = boundaries[0]; + var max = boundaries[1]; + + if (isInt(min) && isInt(max)) { + $(this).contents().wrapAll('
    '); + + var range_element = $(this); + var form = $(range_element).closest(".range_limit").find("form.range_limit"); + var begin_el = form.find("input.range_begin"); + var end_el = form.find("input.range_end"); + + var placeholder_input = $('').appendTo(range_element); + + // make sure slider is loaded + if (placeholder_input.slider !== undefined) { + placeholder_input.slider({ + min: min, + max: max+1, + value: [min, max+1], + tooltip: "hide" + }); + + // try to make slider width/orientation match chart's + var container = range_element.closest(".range_limit"); + var plot = container.find(".chart_js").data("plot"); + var slider_el = container.find(".slider"); + + if (plot && slider_el) { + slider_el.width(plot.width()); + slider_el.css("display", "block") + slider_el.css('margin-right', 'auto'); + slider_el.css('margin-left', 'auto'); + } + else if (slider_el) { + slider_el.css("width", "100%"); + } + } + + // Slider change should update text input values. + var parent = $(this).parent(); + var form = $(parent).closest(".limit_content").find("form.range_limit"); + $(parent).closest(".limit_content").find(".profile .range").on("slide", function(event, ui) { + var values = $(event.target).data("slider").getValue(); + form.find("input.range_begin").val(values[0]); + form.find("input.range_end").val(values[1]); + }); + } + + begin_el.val(min); + end_el.val(max); + + begin_el.change( function() { + var val = BlacklightRangeLimit.parseNum($(this).val()); + if ( isNaN(val) || val < min) { + //for weird data, set slider at min + val = min; + } + var values = placeholder_input.data("slider").getValue(); + values[0] = val; + placeholder_input.slider("setValue", values); + }); + + end_el.change( function() { + var val = BlacklightRangeLimit.parseNum($(this).val()); + if ( isNaN(val) || val > max ) { + //weird entry, set slider to max + val = max; + } + var values = placeholder_input.data("slider").getValue(); + values[1] = val; + placeholder_input.slider("setValue", values); + }); + + }); + + // catch event for redrawing chart, to redraw slider to match width + $("body").on("plotDrawn.blacklight.rangeLimit", function(event) { + var area = $(event.target).closest(".limit_content.range_limit"); + var plot = area.find(".chart_js").data("plot"); + var slider_el = area.find(".slider"); + + if (plot && slider_el) { + slider_el.width(plot.width()); + slider_el.css("display", "block") + slider_el.css('margin-right', 'auto'); + slider_el.css('margin-left', 'auto'); + } + }); + + // returns two element array min/max as numbers. If there is a limit applied, + // it's boundaries are are limits. Otherwise, min/max in current result + // set as sniffed from HTML. Pass in a DOM element for a div.range + // Will return NaN as min or max in case of error or other weirdness. + function min_max(range_element) { + var current_limit = $(range_element).closest(".limit_content.range_limit").find(".current") + + + + var min = max = BlacklightRangeLimit.parseNum(current_limit.find(".single").text()) + if ( isNaN(min)) { + min = BlacklightRangeLimit.parseNum(current_limit.find(".from").first().text()); + max = BlacklightRangeLimit.parseNum(current_limit.find(".to").first().text()); + } + + if (isNaN(min) || isNaN(max)) { + //no current limit, take from results min max included in spans + min = BlacklightRangeLimit.parseNum($(range_element).find(".min").first().text()); + max = BlacklightRangeLimit.parseNum($(range_element).find(".max").first().text()); + } + + return [min, max] + } + + + // Check to see if a value is an Integer + // see: http://stackoverflow.com/questions/3885817/how-to-check-if-a-number-is-float-or-integer + function isInt(n) { + return n % 1 === 0; + } + + }); + \ No newline at end of file diff --git a/app/assets/javascripts/hyrax/app.js.erb b/app/assets/javascripts/hyrax/app.js.erb index 37372cdcf6..8caa18d466 100644 --- a/app/assets/javascripts/hyrax/app.js.erb +++ b/app/assets/javascripts/hyrax/app.js.erb @@ -1,4 +1,4 @@ -// OVERRIDE Hyrax v3.5.0 to have a sidebar maximize and minimize based on window size +// OVERRIDE Hyrax v5.0.0rc2 to have a sidebar maximize and minimize based on window size // Once, javascript is written in a modular format, all initialization // code should be called from here. @@ -167,25 +167,25 @@ Hyrax = { sidebar: function () { $('.sidebar-toggle').on('click', function() { $('.sidebar').toggleClass('maximized'); - if ($(window).width() >= 768) { + if ($(window).width() >= 992) { $('.main-content').toggleClass('maximized'); } }); $('.sidebar').on('mouseenter', function() { - if ($(window).width() < 768) { + if ($(window).width() < 992) { $('.sidebar').addClass('maximized'); } }); $('.sidebar').on('mouseleave', function() { - if ($(window).width() < 768) { + if ($(window).width() < 992) { $('.sidebar').removeClass('maximized'); } }); $(window).on('resize', function() { - if ($(window).width() >= 768) { + if ($(window).width() >= 992) { $('.sidebar, .main-content').addClass('maximized'); } else { $('.sidebar, .main-content').removeClass('maximized'); diff --git a/app/assets/javascripts/hyrax/featured_works.js b/app/assets/javascripts/hyrax/featured_works.js index f964a93f90..a4cc818974 100644 --- a/app/assets/javascripts/hyrax/featured_works.js +++ b/app/assets/javascripts/hyrax/featured_works.js @@ -1,4 +1,4 @@ -//OVERRIDE this file to add the selector for featured collections (ff) at the bottom in addition to the selector for featured works (dd) +//OVERRIDE Hyrax 5.0.0rc2 to add the selector for featured collections (ff) at the bottom in addition to the selector for featured works (dd) // /* find the input element with data-property="order" that is nested under the given node */ function setWeight(node, weight, property) { diff --git a/app/assets/javascripts/hyrax/thumbnail_select.es6 b/app/assets/javascripts/hyrax/thumbnail_select.es6 index 22464fa646..55ace54c44 100644 --- a/app/assets/javascripts/hyrax/thumbnail_select.es6 +++ b/app/assets/javascripts/hyrax/thumbnail_select.es6 @@ -1,4 +1,4 @@ -// OVERRIDE: Hyrax 2.9 to use work titles for collection thumbnail select +// OVERRIDE: Hyrax 5.0.0rc2 to use work titles for collection thumbnail select // Dynamically load the file options into the "Thumbnail" select field. export default class { diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 8b66c7d5da..280aa914cd 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -15,8 +15,11 @@ *= require codemirror *= require codemirror-theme *= require hyrax - *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap + *= require dataTables.bootstrap4 *= require bootstrap-datepicker *= require single_signon + *= require blacklight_advanced_search + *= require blacklight_range_limit *= require_self + *= require hyku_knapsack/application */ diff --git a/app/assets/stylesheets/hyku.scss b/app/assets/stylesheets/hyku.scss index 8979ddd16b..4c0a380bcf 100644 --- a/app/assets/stylesheets/hyku.scss +++ b/app/assets/stylesheets/hyku.scss @@ -16,7 +16,7 @@ } } -@media (min-width: $screen-sm-min) { +@media (min-width: 768px) { .logo-set { display: flex; flex-direction: column; @@ -61,6 +61,7 @@ margin-bottom: 20px; } + // TODO: Do we need this style or could we fallback on btn-primary .btn-sign-up { @include button-variant( $classic-white, @@ -131,7 +132,7 @@ footer.navbar { .sidebar { .h5 { - padding: $nav-link-padding; + padding: 10px 15px; } &.maximized { @@ -275,7 +276,7 @@ footer.navbar { } .callout-danger { - background-color: $well-bg; + background-color: #f5f5f5; border-color: $brand-danger; h1, @@ -374,7 +375,7 @@ a.btn.btn-default.restore-default-color.with-color-hint { height: auto; } -@media (min-width: $screen-lg-min) { +@media (min-width: 1200px) { a.restore-default-color { margin-top: 2em; } @@ -388,7 +389,7 @@ a.btn.btn-default.restore-default-color.with-color-hint { display: inline-block; } -@media (max-width: $screen-md-min) { +@media (max-width: 992px) { .product-features { .grid-item { width: 50%; @@ -396,7 +397,7 @@ a.btn.btn-default.restore-default-color.with-color-hint { } } -@media (max-width: $screen-sm-min) { +@media (max-width: 768px) { .product-features { .grid-item { width: 100%; @@ -422,6 +423,7 @@ nav.navbar.navbar-default.navbar-static-top .container-fluid .row { body.public-facing { .btn { &.btn-primary, + &.btn-secondary, &.btn-danger { color: #fff; } @@ -433,7 +435,7 @@ body.public-facing { } // FEATURED COLLECTIONS -@media (max-width: $screen-sm-min) { +@media (max-width: 768px) { .admin-show-page .collection-title-row-wrapper .collection-title-row-content @@ -486,3 +488,13 @@ body.public-facing { .mt-20 { margin-top: 20px; } + +.navbar-toggler-icon { + background: no-repeat center center; + background-size: 100% 100%; + content: ""; + display: inline-block; + height: 1.5em; + vertical-align: middle; + width: 1.5em; +} diff --git a/app/assets/stylesheets/hyrax.scss b/app/assets/stylesheets/hyrax.scss index 5efe46c158..411eca65f0 100644 --- a/app/assets/stylesheets/hyrax.scss +++ b/app/assets/stylesheets/hyrax.scss @@ -5,18 +5,17 @@ */ @import "variables"; @import "bootstrap-default-overrides"; -@import "bootstrap-sprockets"; @import "bootstrap"; @import "blacklight/blacklight"; +@import "blacklight_gallery/gallery"; +@import "blacklight_gallery/masonry"; +@import "blacklight_gallery/slideshow"; +@import "blacklight_gallery/osd_viewer"; +@import "hyrax/blacklight_gallery"; @import "font-awesome"; @import "hyrax/hyrax"; @import "hyku"; @import "accounts"; @import "viewer"; -@import "blacklight_gallery/gallery"; -@import "blacklight_gallery/masonry"; -@import "blacklight_gallery/slideshow"; -@import "blacklight_gallery/osd_viewer"; - @import "themes/*"; diff --git a/app/assets/stylesheets/themes/cultural_repository.scss b/app/assets/stylesheets/themes/cultural_repository.scss index fc451c0c74..811e8e7dc7 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: $screen-md-min) { + @media (max-width: 992px) { padding-top: 0 !important; } @@ -162,7 +162,7 @@ .facet-panel-background-color { background-color: #ffffff; - @media (max-width: $screen-md-min) { + @media (max-width: 992px) { background-color: transparent; } @@ -240,14 +240,14 @@ ////// Media Queries ////// - @media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { + @media (min-width: 480px) and (max-width: 1199px) { .mt-reverse-mb { margin-bottom: 40px; margin-top: 0; } } - @media (max-width: $screen-sm-max) { + @media (max-width: 991px) { div.home_page_text.homepage-text-container { height: 0; margin-bottom: 30px; @@ -255,7 +255,7 @@ } } - @media screen and (max-width: $screen-md-min) { + @media screen and (max-width: 992px) { 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 ec037fa896..26639ca41a 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: $screen-md-min) { + @media (max-width: 992px) { div.neutral-repository-collections:nth-of-type(2n+3) { clear: left; } } - @media (min-width: $screen-md-min) { + @media (min-width: 992px) { div.neutral-repository-collections:nth-of-type(3n+4) { clear: left; } diff --git a/app/controllers/account_sign_up_controller.rb b/app/controllers/account_sign_up_controller.rb index 89b06cb6e2..c0fe62fae9 100644 --- a/app/controllers/account_sign_up_controller.rb +++ b/app/controllers/account_sign_up_controller.rb @@ -27,24 +27,24 @@ def create private - def ensure_admin! - # Require Admin access to perform any actions - authorize! :read, :admin_dashboard - end + def ensure_admin! + # Require Admin access to perform any actions + authorize! :read, :admin_dashboard + end - def admin_only_tenant_creation? - ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_ADMIN_ONLY_TENANT_CREATION', false)) - end + def admin_only_tenant_creation? + ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_ADMIN_ONLY_TENANT_CREATION', false)) + end - def first_user_registration_url - if current_user - new_user_session_url(host: @account.cname) - else - new_user_registration_url(host: @account.cname) - end + def first_user_registration_url + if current_user + new_user_session_url(host: @account.cname) + else + new_user_registration_url(host: @account.cname) end + end - def create_params - params.require(:account).permit(:name) - end + def create_params + params.require(:account).permit(:name) + end end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index f27c0e2a7d..d5028b2171 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -28,12 +28,12 @@ def update private - def account_params - params.require(:account).permit(:name, :cname, :title, *@account.public_settings.keys) - end + def account_params + params.require(:account).permit(:name, :cname, :title, *@account.public_settings.keys) + end - def set_current_account - @account = Site.account - end + def set_current_account + @account = Site.account + end end end diff --git a/app/controllers/admin/group_roles_controller.rb b/app/controllers/admin/group_roles_controller.rb index d1966f2dd6..1818e85a63 100644 --- a/app/controllers/admin/group_roles_controller.rb +++ b/app/controllers/admin/group_roles_controller.rb @@ -44,23 +44,23 @@ def destroy private - def load_group - @group = Hyrax::Group.find_by(id: params[:group_id]) - end + def load_group + @group = Hyrax::Group.find_by(id: params[:group_id]) + end - def redirect_not_found - flash[:error] = 'Unable to find Group Role with that ID' - redirect_to admin_group_roles_path(@group) - end + def redirect_not_found + flash[:error] = 'Unable to find Group Role with that ID' + redirect_to admin_group_roles_path(@group) + end - def cannot_remove_admin_role_from_admin_group - role = Role.find_by(id: params[:role_id]) - return unless @group.name == ::Ability.admin_group_name && role.name == 'admin' + def cannot_remove_admin_role_from_admin_group + role = Role.find_by(id: params[:role_id]) + return unless @group.name == ::Ability.admin_group_name && role.name == 'admin' - redirect_back( - fallback_location: edit_admin_group_path(@group), - flash: { error: "Admin role cannot be removed from this group" } - ) - end + redirect_back( + fallback_location: edit_admin_group_path(@group), + flash: { error: "Admin role cannot be removed from this group" } + ) + end end end diff --git a/app/controllers/admin/group_users_controller.rb b/app/controllers/admin/group_users_controller.rb index 3488fda4c1..c7544ead0e 100644 --- a/app/controllers/admin/group_users_controller.rb +++ b/app/controllers/admin/group_users_controller.rb @@ -32,25 +32,25 @@ def destroy private - def load_group - @group = Hyrax::Group.find_by(id: params[:group_id]) - end + def load_group + @group = Hyrax::Group.find_by(id: params[:group_id]) + end - def page_number - params.fetch(:page, 1).to_i - end + def page_number + params.fetch(:page, 1).to_i + end - def page_size - params.fetch(:per, 10).to_i - end + def page_size + params.fetch(:per, 10).to_i + end - def cannot_remove_admin_users_from_admin_group - return unless @group.name == ::Ability.admin_group_name + def cannot_remove_admin_users_from_admin_group + return unless @group.name == ::Ability.admin_group_name - redirect_back( - fallback_location: edit_admin_group_path(@group), - flash: { error: "Admin users cannot be removed from this group" } - ) - end + redirect_back( + fallback_location: edit_admin_group_path(@group), + flash: { error: "Admin users cannot be removed from this group" } + ) + end end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index c939ff5de2..513ef190e7 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -83,16 +83,16 @@ def destroy private - def group_params - params.require(:group).permit(:name, :humanized_name, :description) - end + def group_params + params.require(:group).permit(:name, :humanized_name, :description) + end - def page_number - params.fetch(:page, 1).to_i - end + def page_number + params.fetch(:page, 1).to_i + end - def page_size - params.fetch(:per, 10).to_i - end + def page_size + params.fetch(:per, 10).to_i + end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index fa6740127c..66d17cc447 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -20,16 +20,16 @@ def activate user.password = ENV.fetch('HYKU_USER_DEFAULT_PASSWORD', 'password') if user.save && user.accept_invitation! - redirect_to hyrax.admin_users_path, notice: t('hyrax.admin.users.activate.success', user: user) + redirect_to hyrax.admin_users_path, notice: t('hyrax.admin.users.activate.success', user:) else - redirect_to hyrax.admin_users_path, flash: { error: t('hyrax.admin.users.activate.failure', user: user) } + redirect_to hyrax.admin_users_path, flash: { error: t('hyrax.admin.users.activate.failure', user:) } end end private - def load_user - @user = User.from_url_component(params[:id]) - end + def load_user + @user = User.from_url_component(params[:id]) + end end end diff --git a/app/controllers/admin/work_types_controller.rb b/app/controllers/admin/work_types_controller.rb index babaf515c9..1057457751 100644 --- a/app/controllers/admin/work_types_controller.rb +++ b/app/controllers/admin/work_types_controller.rb @@ -24,8 +24,8 @@ def update private - def site - @site ||= Site.first - end + def site + @site ||= Site.first + end end end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 2f8d6cb336..e9005f827e 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -7,11 +7,11 @@ class AdminController < ApplicationController private - def ensure_admin! - authorize! :read, :admin_dashboard - end + def ensure_admin! + authorize! :read, :admin_dashboard + end - def deny_access(_exception) - redirect_to main_app.root_url, alert: t('hyku.admin.flash.access_denied') - end + def deny_access(_exception) + redirect_to main_app.root_url, alert: t('hyku.admin.flash.access_denied') + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9449da3c03..c20cc377a5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception, prepend: true - force_ssl if: :ssl_configured? - helper Openseadragon::OpenseadragonHelper # Adds a few additional behaviors into the application controller include Blacklight::Controller @@ -24,7 +22,6 @@ class ApplicationController < ActionController::Base before_action :require_active_account!, if: :multitenant? before_action :set_account_specific_connections! before_action :elevate_single_tenant!, if: :singletenant? - skip_after_action :discard_flash_if_xhr rescue_from Apartment::TenantNotFound do raise ActionController::RoutingError, 'Not Found' @@ -32,107 +29,100 @@ class ApplicationController < ActionController::Base protected - def is_hidden - current_account.persisted? && !current_account.is_public? - end + def hidden? + current_account.persisted? && !current_account.is_public? + end - def is_api_or_pdf - request.format.to_s.match('json') || - params[:print] || - request.path.include?('api') || - request.path.include?('pdf') - end + def api_or_pdf? + request.format.to_s.match('json') || + params[:print] || + request.path.include?('api') || + request.path.include?('pdf') + end - def is_staging - ['staging'].include?(Rails.env) - end + def staging? + ['staging'].include?(Rails.env) + end - ## - # Extra authentication for palni-palci during development phase - def authenticate_if_needed - # Disable this extra authentication in test mode - return true if Rails.env.test? - if (is_hidden || is_staging) && !is_api_or_pdf - authenticate_or_request_with_http_basic do |username, password| - username == "samvera" && password == "hyku" - end - end + def authenticate_if_needed + # Disable this extra authentication in test mode + return true if Rails.env.test? + return unless (hidden? || staging?) && !api_or_pdf? + authenticate_or_request_with_http_basic do |username, password| + username == "samvera" && password == "hyku" end + end - def super_and_current_users - users = Role.find_by(name: 'superadmin')&.users.to_a - users << current_user if current_user && !users.include?(current_user) - users - end + def super_and_current_users + users = Role.find_by(name: 'superadmin')&.users.to_a + users << current_user if current_user && !users.include?(current_user) + users + end private - def require_active_account! - return if singletenant? - return if devise_controller? - raise Apartment::TenantNotFound, "No tenant for #{request.host}" unless current_account.persisted? - end + def require_active_account! + return if singletenant? + return if devise_controller? + raise Apartment::TenantNotFound, "No tenant for #{request.host}" unless current_account.persisted? + end - def set_account_specific_connections! - current_account&.switch! - end + def set_account_specific_connections! + current_account&.switch! + end - def multitenant? - @multitenant ||= ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_MULTITENANT', false)) - end + def multitenant? + @multitenant ||= ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_MULTITENANT', false)) + end - def singletenant? - !multitenant? - end + def singletenant? + !multitenant? + end - def elevate_single_tenant! - AccountElevator.switch!(current_account.cname) if current_account && root_host? - end + def elevate_single_tenant! + AccountElevator.switch!(current_account.cname) if current_account && root_host? + end - def root_host? - Account.canonical_cname(request.host) == Account.root_host - end + def root_host? + Account.canonical_cname(request.host) == Account.root_host + end - def admin_host? - return false if singletenant? - Account.canonical_cname(request.host) == Account.admin_host - end + def admin_host? + return false if singletenant? + Account.canonical_cname(request.host) == Account.admin_host + end - def current_account - @current_account ||= Account.from_request(request) - @current_account ||= if multitenant? - Account.new do |a| - a.build_solr_endpoint - a.build_fcrepo_endpoint - a.build_redis_endpoint - end - else - Account.single_tenant_default + def current_account + @current_account ||= Account.from_request(request) + @current_account ||= if multitenant? + Account.new do |a| + a.build_solr_endpoint + a.build_fcrepo_endpoint + a.build_redis_endpoint end - end - - # Find themes set on Site model, or return default - def home_page_theme - current_account.sites&.first&.home_theme || 'default_home' - end + else + Account.single_tenant_default + end + end - def show_page_theme - current_account.sites&.first&.show_theme || 'default_show' - end + # Find themes set on Site model, or return default + def home_page_theme + current_account.sites&.first&.home_theme || 'default_home' + end - def search_results_theme - current_account.sites&.first&.search_theme || 'list_view' - end + def show_page_theme + current_account.sites&.first&.show_theme || 'default_show' + end - # Add context information to the lograge entries - def append_info_to_payload(payload) - super - payload[:request_id] = request.uuid - payload[:user_id] = current_user.id if current_user - payload[:account_id] = current_account.cname if current_account - end + def search_results_theme + current_account.sites&.first&.search_theme || 'list_view' + end - def ssl_configured? - ActiveRecord::Type::Boolean.new.cast(current_account.ssl_configured) - end + # Add context information to the lograge entries + def append_info_to_payload(payload) + super + payload[:request_id] = request.uuid + payload[:user_id] = current_user.id if current_user + payload[:account_id] = current_account.cname if current_account + end end diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 72265252cd..148fc234b6 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength, Metrics/BlockLength class CatalogController < ApplicationController + include BlacklightAdvancedSearch::Controller + include BlacklightRangeLimit::ControllerOverride include Hydra::Catalog include Hydra::Controller::ControllerBehavior include BlacklightOaiProvider::Controller @@ -8,20 +11,45 @@ class CatalogController < ApplicationController # These before_action filters apply the hydra access controls before_action :enforce_show_permissions, only: :show - def self.uploaded_field - 'system_create_dtsi' + def self.created_field + 'date_created_ssim' + end + + def self.creator_field + 'creator_ssim' end def self.modified_field 'system_modified_dtsi' end + def self.title_field + 'title_ssim' + end + + def self.uploaded_field + 'system_create_dtsi' + end + # CatalogController-scope behavior and configuration for BlacklightIiifSearch include BlacklightIiifSearch::Controller configure_blacklight do |config| + ## + # Blacklight 7.35.0 's default document_component is `nil`, see: + # + # - https://github.com/projectblacklight/blacklight/blob/ac5fa8b300c5ad5c35b1663ef0f15372ffa2be0f/lib/blacklight/configuration.rb#L213 + # - https://github.com/projectblacklight/blacklight/blob/ac5fa8b300c5ad5c35b1663ef0f15372ffa2be0f/lib/blacklight/configuration.rb#L186 + # + # Digging around in the wiki, you might find (only found because I cloned the repo): + # + # - https://github.com/projectblacklight/blacklight/wiki/Configuration---Results-View + config.index.document_component = Blacklight::DocumentComponent + config.show.document_component = Blacklight::DocumentComponent + # IiifPrint index fields - config.add_index_field 'all_text_tsimv', highlight: true, helper_method: :render_ocr_snippets + config.add_index_field 'all_text_timv' + config.add_index_field 'file_set_text_tsimv', label: "Item contents", highlight: true, helper_method: :render_ocr_snippets # configuration for Blacklight IIIF Content Search config.iiif_search = { @@ -32,30 +60,42 @@ def self.modified_field suggester_name: 'iiifSuggester' } - config.view.gallery.partials = %i[index_header index] - config.view.masonry.partials = [:index] - config.view.slideshow.partials = [:index] - config.show.tile_source_field = :content_metadata_image_iiif_info_ssm config.show.partials.insert(1, :openseadragon) + # default advanced config values config.advanced_search ||= Blacklight::OpenStructWithHashAccess.new # config.advanced_search[:qt] ||= 'advanced' config.advanced_search[:url_key] ||= 'advanced' config.advanced_search[:query_parser] ||= 'dismax' config.advanced_search[:form_solr_parameters] ||= {} + config.advanced_search[:form_facet_partial] ||= "advanced_search_facets_as_select" config.search_builder_class = IiifPrint::CatalogSearchBuilder + # Use locally customized AdvSearchBuilder so we can enable blacklight_advanced_search + config.search_builder_class = AdvSearchBuilder + # Show gallery view config.view.gallery.partials = %i[index_header index] + config.view.masonry.partials = [:index] config.view.slideshow.partials = [:index] + # Because too many times on Samvera tech people raise a problem regarding a failed query to SOLR. + # Often, it's because they inadvertently exceeded the character limit of a GET request. + config.http_method = :post + ## Default parameters to send to solr for all search-like requests. See also SolrHelper#solr_search_params config.default_solr_params = { qt: "search", rows: 10, - qf: "title_tesim description_tesim creator_tesim keyword_tesim all_text_timv" + qf: IiifPrint.config.metadata_fields.keys.map { |attribute| "#{attribute}_tesim" } + .join(' ') << " title_tesim description_tesim all_text_timv file_set_text_tsimv", # the first space character is necessary! + "hl": true, + "hl.simple.pre": "", + "hl.simple.post": "", + "hl.snippets": 30, + "hl.fragsize": 100 } # Specify which field to use in the tag cloud on the homepage. @@ -67,6 +107,18 @@ def self.modified_field config.index.display_type_field = 'has_model_ssim' config.index.thumbnail_field = 'thumbnail_path_ss' + # Blacklight 7 additions + config.add_results_document_tool(:bookmark, partial: 'bookmark_control', if: :render_bookmarks_control?) + config.add_results_collection_tool(:sort_widget) + config.add_results_collection_tool(:per_page_widget) + config.add_results_collection_tool(:view_type_group) + config.add_show_tools_partial(:bookmark, partial: 'bookmark_control', if: :render_bookmarks_control?) + config.add_show_tools_partial(:email, callback: :email_action, validator: :validate_email_params) + config.add_show_tools_partial(:sms, if: :render_sms_action?, callback: :sms_action, validator: :validate_sms_params) + config.add_show_tools_partial(:citation) + config.add_nav_action(:bookmark, partial: 'blacklight/nav/bookmark', if: :render_bookmarks_control?) + config.add_nav_action(:search_history, partial: 'blacklight/nav/search_history') + # solr fields that will be treated as facets by the blacklight application # The ordering of the field names is the order of the display config.add_facet_field 'human_readable_type_sim', label: "Type", limit: 5 @@ -81,6 +133,10 @@ def self.modified_field config.add_facet_field 'file_format_sim', limit: 5 config.add_facet_field 'member_of_collections_ssim', limit: 5, label: 'Collections' + # TODO: deal with part of facet changes + # config.add_facet_field solr_name("part", :facetable), limit: 5, label: 'Part' + # config.add_facet_field solr_name("part_of", :facetable), limit: 5 + # Have BL send all facet field names to Solr, which has been the default # previously. Simply remove these lines if you'd rather use Solr request # handler defaults, or have no facets. @@ -220,14 +276,15 @@ def self.modified_field } end + date_fields = ['date_created_tesim', 'sorted_date_isi', 'sorted_month_isi'] + config.add_search_field('date_created') do |field| field.solr_parameters = { "spellcheck.dictionary": "date_created" } - solr_name = 'created_tesim' field.solr_local_parameters = { - qf: solr_name, - pf: solr_name + qf: date_fields.join(' '), + pf: date_fields.join(' ') } end @@ -343,16 +400,27 @@ def self.modified_field } end + config.add_search_field('source') do |field| + solr_name = solr_name("source", :stored_searchable) + field.solr_local_parameters = { + qf: solr_name, + pf: solr_name + } + end + # "sort results by" select (pulldown) # label in pulldown is followed by the name of the SOLR field to sort by and # whether the sort is ascending or descending (it must be asc or desc # except in the relevancy case). # label is key, solr field is value - config.add_sort_field "score desc, #{uploaded_field} desc", label: "relevance" - config.add_sort_field "#{uploaded_field} desc", label: "date uploaded \u25BC" - config.add_sort_field "#{uploaded_field} asc", label: "date uploaded \u25B2" - config.add_sort_field "#{modified_field} desc", label: "date modified \u25BC" - config.add_sort_field "#{modified_field} asc", label: "date modified \u25B2" + config.add_sort_field "score desc, #{uploaded_field} desc", label: "Relevance" + + config.add_sort_field "#{title_field} asc", label: "Title" + config.add_sort_field "#{creator_field} asc", label: "Author" + config.add_sort_field "#{created_field} asc", label: "Published Date (Ascending)" + config.add_sort_field "#{created_field} desc", label: "Published Date (Descending)" + config.add_sort_field "#{modified_field} asc", label: "Upload Date (Ascending)" + config.add_sort_field "#{modified_field} desc", label: "Upload Date (Descending)" # OAI Config fields config.oai = { @@ -360,8 +428,8 @@ def self.modified_field repository_name: ->(controller) { controller.send(:current_account)&.name.presence }, # repository_url: ->(controller) { controller.oai_catalog_url }, record_prefix: ->(controller) { controller.send(:current_account).oai_prefix }, - admin_email: ->(controller) { controller.send(:current_account).oai_admin_email }, - sample_id: ->(controller) { controller.send(:current_account).oai_sample_identifier } + admin_email: ->(controller) { controller.send(:current_account).oai_admin_email }, + sample_id: ->(controller) { controller.send(:current_account).oai_sample_identifier } }, document: { limit: 100, # number of records returned with each request, default: 15 @@ -378,7 +446,8 @@ def self.modified_field # This is overridden just to give us a JSON response for debugging. def show - _, @document = fetch params[:id] + _, @document = search_service.fetch(params[:id]) render json: @document.to_h end end +# rubocop:enable Metrics/ClassLength, Metrics/BlockLength diff --git a/app/controllers/concerns/hyku/works_controller_behavior.rb b/app/controllers/concerns/hyku/works_controller_behavior.rb new file mode 100644 index 0000000000..96b6f4e537 --- /dev/null +++ b/app/controllers/concerns/hyku/works_controller_behavior.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# OVERRIDE: Hyrax v5.0.0rc2 to add inject_show_theme_views - Hyku theming +# and correct hostname of manifests +# and to add Hyrax IIIF AV +module Hyku + # include this module after including Hyrax::WorksControllerBehavior to override + # Hyrax::WorksControllerBehavior methods with the ones defined here + module WorksControllerBehavior + extend ActiveSupport::Concern + + # Adds behaviors for hyrax-iiif_av plugin and provides #manifest and #iiif_manifest_builder + include Hyrax::IiifAv::ControllerBehavior + + included do + # add around action to load theme show page views + around_action :inject_show_theme_views, except: :delete + end + + def json_manifest + iiif_manifest_builder.manifest_for(presenter: iiif_manifest_presenter) + end + + private + + def iiif_manifest_presenter + Hyrax::IiifManifestPresenter.new(search_result_document(id: params[:id])).tap do |p| + p.hostname = request.hostname + p.ability = current_ability + end + end + + def format_error_messages(errors) + errors.messages.map do |field, messages| + field_name = field.to_s.humanize + messages.map { |message| "#{field_name} #{message.sub(/^./, &:downcase)}" } + end.flatten.join("\n") + end + + # Creating a form object that can re-render most of the submitted parameters. + # Required for ActiveFedora::Base objects only. + def rebuild_form(original_input_params_for_form) + build_form + @form = Hyrax::Forms::FailedSubmissionFormWrapper + .new(form: @form, + input_params: original_input_params_for_form) + end + + def after_update_error(errors) + respond_to do |wants| + wants.html do + flash[:error] = format_error_messages(errors) + build_form unless @form.is_a? Hyrax::ChangeSet + render 'edit', status: :unprocessable_entity + end + wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: }) } + end + end + + def available_admin_sets + # only returns admin sets in which the user can deposit + admin_set_results = Hyrax::AdminSetService.new(self).search_results(:deposit) + + # get all the templates at once, reducing query load + templates = Hyrax::PermissionTemplate.where(source_id: admin_set_results.map(&:id)).to_a + + admin_sets = admin_set_results.map do |admin_set_doc| + template = templates.find { |temp| temp.source_id == admin_set_doc.id.to_s } + + ## OVERRIDE Hyrax v5.0.0rc2 + # Removes a short-circuit that allowed users with manage access to + # the given permission_template to always be able to edit a record's sharing + # (i.e. the "Sharing" tab in forms). + # + # We remove this because there is currently a bug in Hyrax where, if the + # workflow does not allow access grants, changes to a record's sharing + # are not being persisted, leading to a confusing UX. + # @see https://github.com/samvera/hyrax/issues/5904 + # + # TEMPORARY: This override should be removed when the bug is resolved in + # upstream Hyrax and brought into this project. + # + # determine if sharing tab should be visible + sharing = !!template&.active_workflow&.allows_access_grant? + + Hyrax::AdminSetSelectionPresenter::OptionsEntry + .new(admin_set: admin_set_doc, permission_template: template, permit_sharing: sharing) + end + + Hyrax::AdminSetSelectionPresenter.new(admin_sets:) + end + + # added to prepend the show theme views into the view_paths + def inject_show_theme_views + if show_page_theme && show_page_theme != 'default_show' + original_paths = view_paths + Hyku::Application.theme_view_path_roots.each do |root| + show_theme_view_path = File.join(root, 'app', 'views', "themes", show_page_theme.to_s) + prepend_view_path(show_theme_view_path) + end + yield + # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. + view_paths=(original_paths) + # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + else + yield + end + end + end +end diff --git a/app/controllers/concerns/hyrax/admin/users_controller_behavior.rb b/app/controllers/concerns/hyrax/admin/users_controller_behavior.rb index cd4d075e9f..d58ad7ce06 100644 --- a/app/controllers/concerns/hyrax/admin/users_controller_behavior.rb +++ b/app/controllers/concerns/hyrax/admin/users_controller_behavior.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Authorize User access and set up roles dropdown +# OVERRIDE Hyrax v5.0.0rc2 Authorize User access and set up roles dropdown module Hyrax module Admin module UsersControllerBehavior extend ActiveSupport::Concern include Blacklight::SearchContext included do - # OVERRIDE Hyrax v3.4.2 Replace :ensure_admin! with :ensure_access! to leverage User abilities. + # OVERRIDE Hyrax v5.0.0rc2 Replace :ensure_admin! with :ensure_access! to leverage User abilities. # @see app/models/concerns/hyrax/ability/user_ability.rb before_action :ensure_access! with_themed_layout 'dashboard' @@ -19,7 +19,7 @@ def index add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path add_breadcrumb t(:'hyrax.admin.users.index.title'), hyrax.admin_users_path @presenter = Hyrax::Admin::UsersPresenter.new - # OVERRIDE Hyrax v3.4.2 Sets up a list of role names to be used by a dropdown input that + # OVERRIDE Hyrax v5.0.0rc2 Sets up a list of role names to be used by a dropdown input that # allows users to be invited with a specific role. @invite_roles_options = if current_ability.admin? ::RolesService::DEFAULT_ROLES @@ -30,11 +30,11 @@ def index private - # OVERRIDE Hyrax v3.4.2 Replace :ensure_admin! with :ensure_access! to leverage User abilities. - # @see app/models/concerns/hyrax/ability/user_ability.rb - def ensure_access! - authorize! :read, ::User - end + # OVERRIDE Hyrax v5.0.0rc2 Replace :ensure_admin! with :ensure_access! to leverage User abilities. + # @see app/models/concerns/hyrax/ability/user_ability.rb + def ensure_access! + authorize! :read, ::User + end end end end diff --git a/app/controllers/concerns/hyrax/users_controller_decorator.rb b/app/controllers/concerns/hyrax/users_controller_decorator.rb index 98f6ca53ee..bdbc358bc4 100644 --- a/app/controllers/concerns/hyrax/users_controller_decorator.rb +++ b/app/controllers/concerns/hyrax/users_controller_decorator.rb @@ -11,28 +11,28 @@ module UsersControllerDecorator private - def users_match! - # The #find_user method is a :before_action on the Hyrax::UsersController. It sets - # the @user variable, which we need in this method. - # - # However, because we are decorating Hyrax::UsersController with this module, - # this method's :before_action will fire before the one that calls #find_user. - # This means that #find_user will fire a second time after this method runs. - # - # This is not ideal, but after discussing with @jeremyf, we decided that this is - # the "lesser evil" compared to other override methods (i.e. using #class_eval, - # overriding the entire file, etc.); since User is an ActiveRecord object, the - # database query that sets @user in #find_user will be cached. - # - # Classes that inherit from Hyrax::UsersController do not have this problem - # (e.g. Hyrax::Dashboard::ProfilesController), which is why we check for presence. - find_user if @user.blank? + def users_match! + # The #find_user method is a :before_action on the Hyrax::UsersController. It sets + # the @user variable, which we need in this method. + # + # However, because we are decorating Hyrax::UsersController with this module, + # this method's :before_action will fire before the one that calls #find_user. + # This means that #find_user will fire a second time after this method runs. + # + # This is not ideal, but after discussing with @jeremyf, we decided that this is + # the "lesser evil" compared to other override methods (i.e. using #class_eval, + # overriding the entire file, etc.); since User is an ActiveRecord object, the + # database query that sets @user in #find_user will be cached. + # + # Classes that inherit from Hyrax::UsersController do not have this problem + # (e.g. Hyrax::Dashboard::ProfilesController), which is why we check for presence. + find_user if @user.blank? - return if can?(:read, @user) - return if current_user == @user + return if can?(:read, @user) + return if current_user == @user - raise CanCan::AccessDenied - end + raise CanCan::AccessDenied + end end end diff --git a/app/controllers/concerns/hyrax/works_controller_behavior.rb b/app/controllers/concerns/hyrax/works_controller_behavior.rb deleted file mode 100644 index f5b51f64f5..0000000000 --- a/app/controllers/concerns/hyrax/works_controller_behavior.rb +++ /dev/null @@ -1,522 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE: Hyrax 3.4.0 to add inject_show_theme_views - Hyku theming and correct hostname of manifests -# OVERRIDE: Hyrax 3.4.0 to add Hyrax IIIF AV -require 'iiif_manifest' - -# rubocop:disable Metrics/ModuleLength -# rubocop:disable Metrics/LineLength -module Hyrax - module WorksControllerBehavior - extend ActiveSupport::Concern - include Blacklight::Base - include Blacklight::AccessControls::Catalog - - # Adds behaviors for hyrax-iiif_av plugin and provides #manifest and #iiif_manifest_builder - include Hyrax::IiifAv::ControllerBehavior - - included do - with_themed_layout :decide_layout - copy_blacklight_config_from(::CatalogController) - - class_attribute :_curation_concern_type, :show_presenter, :work_form_service, :search_builder_class - class_attribute :iiif_manifest_builder, instance_accessor: false - self.show_presenter = Hyku::WorkShowPresenter - self.work_form_service = Hyrax::WorkFormService - self.search_builder_class = WorkSearchBuilder - # Set to nil for the #iiif_manifest_builder (provided by Hyrax::IiifAv) to work - self.iiif_manifest_builder = nil - - attr_accessor :curation_concern - helper_method :curation_concern, :contextual_path - - rescue_from WorkflowAuthorizationException, with: :render_unavailable - # add around action to load theme show page views - around_action :inject_show_theme_views, except: :delete # rubocop:disable Rails/LexicallyScopedActionFilter - end - - class_methods do - def curation_concern_type=(curation_concern_type) - load_and_authorize_resource class: curation_concern_type, - instance_name: :curation_concern, - except: %i[show file_manager inspect_work manifest] - - # Load the fedora resource to get the etag. - # No need to authorize for the file manager, because it does authorization via the presenter. - load_resource class: curation_concern_type, instance_name: :curation_concern, only: :file_manager - - self._curation_concern_type = curation_concern_type - # We don't want the breadcrumb action to occur until after the concern has - # been loaded and authorized - before_action :save_permissions, only: :update # rubocop:disable Rails/LexicallyScopedActionFilter - end - - def curation_concern_type - _curation_concern_type - end - - def cancan_resource_class - Hyrax::ControllerResource - end - end - - def new - @admin_set_options = available_admin_sets - # TODO: move these lines to the work form builder in Hyrax - curation_concern.depositor = current_user.user_key - curation_concern.admin_set_id = admin_set_id_for_new - build_form - end - - def create - case curation_concern - when ActiveFedora::Base - original_input_params_for_form = params[hash_key_for_curation_concern].deep_dup - actor.create(actor_environment) ? after_create_response : after_create_error(curation_concern.errors, original_input_params_for_form) - else - create_valkyrie_work - end - end - - # Finds a solr document matching the id and sets @presenter - # @raise CanCan::AccessDenied if the document is not found or the user doesn't have access to it. - def show - @user_collections = user_collections - - respond_to do |wants| - wants.html { presenter && parent_presenter } - wants.json do - # load @curation_concern manually because it's skipped for html - @curation_concern = Hyrax.query_service.find_by(alternate_identifier: params[:id]) - curation_concern # This is here for authorization checks (we could add authorize! but let's use the same method for CanCanCan) - render :show, status: :ok - end - additional_response_formats(wants) - wants.ttl { render body: presenter.export_as_ttl, mime_type: Mime[:ttl] } - wants.jsonld { render body: presenter.export_as_jsonld, mime_type: Mime[:jsonld] } - wants.nt { render body: presenter.export_as_nt, mime_type: Mime[:nt] } - end - end - # rubocop:enable Metrics/AbcSize - - def edit - @admin_set_options = available_admin_sets - build_form - end - - def update - case curation_concern - when ActiveFedora::Base - actor.update(actor_environment) ? after_update_response : after_update_error(curation_concern.errors) - else - update_valkyrie_work - end - end - - def destroy - case curation_concern - when ActiveFedora::Base - title = curation_concern.to_s - env = Actors::Environment.new(curation_concern, current_ability, {}) - return unless actor.destroy(env) - Hyrax.config.callback.run(:after_destroy, curation_concern.id, current_user, warn: false) - else - transactions['work_resource.destroy'] - .with_step_args('work_resource.delete' => { user: current_user }) - .call(curation_concern) - .value! - - title = Array(curation_concern.title).first - end - - after_destroy_response(title) - end - - def file_manager - @form = Forms::FileManagerForm.new(curation_concern, current_ability) - end - - def inspect_work - raise Hydra::AccessDenied unless current_ability.admin? - presenter - end - - def json_manifest - iiif_manifest_builder.manifest_for(presenter: iiif_manifest_presenter) - end - - private - - def iiif_manifest_presenter - IiifManifestPresenter.new(search_result_document(id: params[:id])).tap do |p| - p.hostname = request.hostname - p.ability = current_ability - end - end - - def user_collections - collections_service.search_results(:deposit) - end - - def collections_service - Hyrax::CollectionsService.new(self) - end - - def admin_set_id_for_new - Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id.to_s - end - - def build_form - @form = work_form_service.build(curation_concern, current_ability, self) - end - - def actor - @actor ||= Hyrax::CurationConcern.actor - end - - ## - # @return [#errors] - def create_valkyrie_work - form = build_form - return after_create_error(form_err_msg(form)) unless form.validate(params[hash_key_for_curation_concern]) - - result = - transactions['change_set.create_work'] - .with_step_args( - 'work_resource.add_to_parent' => { parent_id: params[:parent_id], user: current_user }, - 'work_resource.add_file_sets' => { uploaded_files: uploaded_files, file_set_params: params[hash_key_for_curation_concern][:file_set] }, - 'change_set.set_user_as_depositor' => { user: current_user }, - 'work_resource.change_depositor' => { user: ::User.find_by_user_key(form.on_behalf_of) } - ) - .call(form) - @curation_concern = result.value_or { return after_create_error(transaction_err_msg(result)) } - after_create_response - end - - def update_valkyrie_work - form = build_form - return after_update_error(form_err_msg(form)) unless form.validate(params[hash_key_for_curation_concern]) - result = - transactions['change_set.update_work'] - .with_step_args('work_resource.add_file_sets' => { uploaded_files: uploaded_files, file_set_params: params[hash_key_for_curation_concern][:file_set] }, - 'work_resource.update_work_members' => { work_members_attributes: work_members_attributes }) - .call(form) - @curation_concern = result.value_or { return after_update_error(transaction_err_msg(result)) } - after_update_response - end - - def work_members_attributes - params[hash_key_for_curation_concern][:work_members_attributes]&.permit!&.to_h - end - - def form_err_msg(form) - form.errors.messages.values.flatten.to_sentence - end - - def transaction_err_msg(result) - result.failure.first - end - - def presenter - @presenter ||= show_presenter.new(search_result_document(id: params[:id]), current_ability, request) - end - - def parent_presenter - return @parent_presenter unless params[:parent_id] - - @parent_presenter ||= - show_presenter.new(search_result_document(id: params[:parent_id]), current_ability, request) - end - - # Include 'hyrax/base' in the search path for views, while prefering - # our local paths. Thus we are unable to just override `self.local_prefixes` - def _prefixes - @_prefixes ||= super + ['hyrax/base'] - end - - def actor_environment - Actors::Environment.new(curation_concern, current_ability, attributes_for_actor) - end - - def hash_key_for_curation_concern - _curation_concern_type.model_name.param_key - end - - def contextual_path(presenter, parent_presenter) - ::Hyrax::ContextualPath.new(presenter, parent_presenter).show - end - - # @deprecated - def curation_concern_from_search_results - Deprecation.warn("'##{__method__}' will be removed in Hyrax 4.0. " \ - "Instead, use '#search_result_document'.") - search_params = params.deep_dup - search_params.delete :page - search_result_document(search_params) - end - - ## - # Only returns unsuppressed documents the user has read access to - # - # @api public - # - # @param search_params [ActionController::Parameters] this should - # include an :id key, but based on implementation and use of the - # WorkSearchBuilder, it need not. - # - # @return [SolrDocument] - # - # @raise [WorkflowAuthorizationException] when the object is not - # found via the search builder's search logic BUT the object is - # suppressed AND the user can read it (Yeah, it's confusing but - # after a lot of debugging that's the logic) - # - # @raise [CanCan::AccessDenied] when the object is not found via - # the search builder's search logic BUT the object is not - # supressed OR not readable by the user (Yeah.) - # - # @note This is Jeremy, I have suspicions about the first line of - # this comment (eg, "Only return unsuppressed..."). The - # reason is that I've encounter situations in the specs - # where the document_list is empty but if I then query Solr - # for the object by ID, I get a document that is NOT - # suppressed AND can be read. In other words, I believe - # there is more going on in the search_results method - # (e.g. a filter is being applied that is beyond what the - # comment indicates) - # - # @see #document_not_found! - def search_result_document(search_params) - _, document_list = search_results(search_params) - return document_list.first unless document_list.empty? - document_not_found! - end - - def document_not_found! - doc = ::SolrDocument.find(params[:id]) - raise WorkflowAuthorizationException if doc.suppressed? && current_ability.can?(:read, doc) - raise CanCan::AccessDenied.new(nil, :show) - end - - def render_unavailable - message = I18n.t("hyrax.workflow.unauthorized") - respond_to do |wants| - wants.html do - unavailable_presenter - flash[:notice] = message - render 'unavailable', status: :unauthorized - end - wants.json { render plain: message, status: :unauthorized } - additional_response_formats(wants) - wants.ttl { render plain: message, status: :unauthorized } - wants.jsonld { render plain: message, status: :unauthorized } - wants.nt { render plain: message, status: :unauthorized } - end - end - - def unavailable_presenter - @presenter ||= show_presenter.new(::SolrDocument.find(params[:id]), current_ability, request) - end - - def decide_layout - layout = case action_name - when 'show' - '1_column' - else - 'dashboard' - end - File.join(theme, layout) - end - - ## - # @todo should the controller know so much about browse_everything? - # hopefully this can be refactored to be more reusable. - # - # Add uploaded_files to the parameters received by the actor. - def attributes_for_actor - raw_params = params[hash_key_for_curation_concern] - attributes = if raw_params - work_form_service.form_class(curation_concern).model_attributes(raw_params) - else - {} - end - - # If they selected a BrowseEverything file, but then clicked the - # remove button, it will still show up in `selected_files`, but - # it will no longer be in uploaded_files. By checking the - # intersection, we get the files they added via BrowseEverything - # that they have not removed from the upload widget. - uploaded_files = params.fetch(:uploaded_files, []) - selected_files = params.fetch(:selected_files, {}).values - browse_everything_urls = uploaded_files & - selected_files.map { |f| f[:url] } - - # we need the hash of files with url and file_name - browse_everything_files = selected_files - .select { |v| uploaded_files.include?(v[:url]) } - attributes[:remote_files] = browse_everything_files - # Strip out any BrowseEverthing files from the regular uploads. - attributes[:uploaded_files] = uploaded_files - - browse_everything_urls - attributes - end - - def after_create_response - respond_to do |wants| - wants.html do - # Calling `#t` in a controller context does not mark _html keys as html_safe - flash[:notice] = view_context.t('hyrax.works.create.after_create_html', application_name: view_context.application_name) - - redirect_to [main_app, curation_concern] - end - wants.json { render :show, status: :created, location: polymorphic_path([main_app, curation_concern]) } - end - end - - def format_error_messages(errors) - errors.messages.map do |field, messages| - field_name = field.to_s.humanize - messages.map { |message| "#{field_name} #{message.sub(/^./, &:downcase)}" } - end.flatten.join("\n") - end - - def after_create_error(errors, original_input_params_for_form = nil) - respond_to do |wants| - wants.html do - flash[:error] = format_error_messages(errors) - rebuild_form(original_input_params_for_form) if original_input_params_for_form.present? - render 'new', status: :unprocessable_entity - end - wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) } - end - end - - # Creating a form object that can re-render most of the submitted parameters. - # Required for ActiveFedora::Base objects only. - def rebuild_form(original_input_params_for_form) - build_form - @form = Hyrax::Forms::FailedSubmissionFormWrapper - .new(form: @form, - input_params: original_input_params_for_form) - end - - def after_update_response - return redirect_to hyrax.confirm_access_permission_path(curation_concern) if permissions_changed? && concern_has_file_sets? - - respond_to do |wants| - wants.html { redirect_to [main_app, curation_concern], notice: "Work \"#{curation_concern}\" successfully updated." } - wants.json { render :show, status: :ok, location: polymorphic_path([main_app, curation_concern]) } - end - end - - def after_update_error(errors) - respond_to do |wants| - wants.html do - flash[:error] = format_error_messages(errors) - build_form unless @form.is_a? Hyrax::ChangeSet - render 'edit', status: :unprocessable_entity - end - wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) } - end - end - - def after_destroy_response(title) - respond_to do |wants| - wants.html { redirect_to hyrax.my_works_path, notice: "Deleted #{title}" } - wants.json { render_json_response(response_type: :deleted, message: "Deleted #{curation_concern.id}") } - end - end - - def additional_response_formats(format) - format.endnote do - send_data(presenter.solr_document.export_as_endnote, - type: "application/x-endnote-refer", - filename: presenter.solr_document.endnote_filename) - end - end - - def save_permissions - @saved_permissions = - case curation_concern - when ActiveFedora::Base - curation_concern.permissions.map(&:to_hash) - else - Hyrax::AccessControl.for(resource: curation_concern).permissions - end - end - - def permissions_changed? - @saved_permissions != - case curation_concern - when ActiveFedora::Base - curation_concern.permissions.map(&:to_hash) - else - Hyrax::AccessControl.for(resource: curation_concern).permissions - end - end - - def concern_has_file_sets? - case curation_concern - when ActiveFedora::Common - curation_concern.file_sets.present? - else - Hyrax.custom_queries.find_child_file_set_ids(resource: curation_concern).any? - end - end - - def uploaded_files - UploadedFile.find(params.fetch(:uploaded_files, [])) - end - - def available_admin_sets - # only returns admin sets in which the user can deposit - admin_set_results = Hyrax::AdminSetService.new(self).search_results(:deposit) - - # get all the templates at once, reducing query load - templates = PermissionTemplate.where(source_id: admin_set_results.map(&:id)).to_a - - admin_sets = admin_set_results.map do |admin_set_doc| - template = templates.find { |temp| temp.source_id == admin_set_doc.id.to_s } - - ## OVERRIDE Hyrax v3.4.2 - # Removes a short-circuit that allowed users with manage access to - # the given permission_template to always be able to edit a record's sharing - # (i.e. the "Sharing" tab in forms). - # - # We remove this because there is currently a bug in Hyrax where, if the - # workflow does not allow access grants, changes to a record's sharing - # are not being persisted, leading to a confusing UX. - # @see https://github.com/samvera/hyrax/issues/5904 - # - # TEMPORARY: This override should be removed when the bug is resolved in - # upstream Hyrax and brought into this project. - # - # determine if sharing tab should be visible - sharing = !!template&.active_workflow&.allows_access_grant? # rubocop:disable Style/DoubleNegation - - AdminSetSelectionPresenter::OptionsEntry - .new(admin_set: admin_set_doc, permission_template: template, permit_sharing: sharing) - end - - AdminSetSelectionPresenter.new(admin_sets: admin_sets) - end - - # added to prepend the show theme views into the view_paths - def inject_show_theme_views - if show_page_theme && show_page_theme != 'default_show' - original_paths = view_paths - show_theme_view_path = Rails.root.join('app', 'views', "themes", show_page_theme.to_s) - prepend_view_path(show_theme_view_path) - yield - # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. - view_paths=(original_paths) - # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - else - yield - end - end - end -end -# rubocop:enable Metrics/ModuleLength -# rubocop:enable Metrics/LineLength diff --git a/app/controllers/hyku/invitations_controller.rb b/app/controllers/hyku/invitations_controller.rb index 2aa11fcac9..68fe8d21e9 100644 --- a/app/controllers/hyku/invitations_controller.rb +++ b/app/controllers/hyku/invitations_controller.rb @@ -10,6 +10,7 @@ def after_invite_path_for(_resource) # override the standard invite so that accounts are added properly # if they already exist on another tenant and invited if they do not + # rubocop:disable Metrics/AbcSize def create authorize! :grant_admin_role, User if params[:user][:role] == ::RolesService::ADMIN_ROLE self.resource = User.find_by(email: params[:user][:email]) || invite_resource @@ -20,20 +21,19 @@ def create yield resource if block_given? # Override destination as this was a success either way - if is_flashing_format? && resource.invitation_sent_at - set_flash_message :notice, :send_instructions, email: resource.email - end + set_flash_message :notice, :send_instructions, email: resource.email if is_flashing_format? && resource.invitation_sent_at if method(:after_invite_path_for).arity == 1 respond_with resource, location: after_invite_path_for(current_inviter) else respond_with resource, location: after_invite_path_for(current_inviter, resource) end end + # rubocop:enable Metrics/AbcSize protected - def user_params - params.require(:user).permit(:email, :role) - end + def user_params + params.require(:user).permit(:email, :role) + end end end diff --git a/app/controllers/hyku/registrations_controller.rb b/app/controllers/hyku/registrations_controller.rb index fe9a9422ac..758cf0afb3 100644 --- a/app/controllers/hyku/registrations_controller.rb +++ b/app/controllers/hyku/registrations_controller.rb @@ -15,8 +15,8 @@ def create private - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_up, keys: [:display_name]) - end + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: [:display_name]) + end end end diff --git a/app/controllers/hyrax/admin/appearances_controller.rb b/app/controllers/hyrax/admin/appearances_controller.rb deleted file mode 100644 index b37308ebcb..0000000000 --- a/app/controllers/hyrax/admin/appearances_controller.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4 to add selectable themes - -module Hyrax - module Admin - class AppearancesController < ApplicationController - before_action :require_permissions - with_themed_layout 'dashboard' - class_attribute :form_class - self.form_class = Hyrax::Forms::Admin::Appearance - - def show - # TODO: make selected font the font that show in select box - # TODO add body and headline font to the import url - add_breadcrumbs - @form = form_class.new - @fonts = [@form.headline_font, @form.body_font] - @home_theme_information = YAML.load_file('config/home_themes.yml') - @show_theme_information = YAML.load_file('config/show_themes.yml') - @home_theme_names = load_home_theme_names - @show_theme_names = load_show_theme_names - @search_themes = load_search_themes - - flash[:alert] = t('hyrax.admin.appearances.show.forms.custom_css.warning') - end - - def update - form_class.new(update_params).update! - - if update_params['default_collection_image'] - # Reindex all Collections and AdminSets to apply new default collection image - ReindexCollectionsJob.perform_later - ReindexAdminSetsJob.perform_later - end - - if update_params['default_work_image'] - # Reindex all Works to apply new default work image - ReindexWorksJob.perform_later - end - - redirect_to({ action: :show }, notice: t('.flash.success')) - end - - private - - def update_params - params.require(:admin_appearance).permit(form_class.permitted_params) - end - - def require_permissions - authorize! :update, :appearance - end - - def add_breadcrumbs - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t(:'hyrax.admin.sidebar.configuration'), '#' - add_breadcrumb t(:'hyrax.admin.sidebar.appearance'), request.path - end - - def load_home_theme_names - home_theme_names = [] - @home_theme_information.each do |theme, value_hash| - value_hash.each do |key, value| - home_theme_names << [value, theme] if key == 'name' - end - end - home_theme_names - end - - def load_show_theme_names - show_theme_names = [] - @show_theme_information.each do |theme, value_hash| - value_hash.each do |key, value| - show_theme_names << [value, theme] if key == 'name' - end - end - show_theme_names - end - - def load_search_themes - { - 'List view' => 'list_view', - 'Gallery view' => 'gallery_view' - } - end - end - end -end diff --git a/app/controllers/hyrax/admin/appearances_controller_decorator.rb b/app/controllers/hyrax/admin/appearances_controller_decorator.rb new file mode 100644 index 0000000000..287c27df12 --- /dev/null +++ b/app/controllers/hyrax/admin/appearances_controller_decorator.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to add selectable themes + +module Hyrax + module Admin + module AppearancesControllerDecorator + def show + # TODO: make selected font the font that show in select box + # TODO add body and headline font to the import url + add_breadcrumbs + @form = form_class.new + @fonts = [@form.headline_font, @form.body_font] + @home_theme_information = YAML.load_file(Hyku::Application.path_for('config/home_themes.yml')) + @show_theme_information = YAML.load_file(Hyku::Application.path_for('config/show_themes.yml')) + @home_theme_names = load_home_theme_names + @show_theme_names = load_show_theme_names + @search_themes = load_search_themes + + flash[:alert] = t('hyrax.admin.appearances.show.forms.custom_css.warning') + end + + def update + form_class.new(update_params).update! + + if update_params['default_collection_image'] + # Reindex all Collections and AdminSets to apply new default collection image + ReindexCollectionsJob.perform_later + ReindexAdminSetsJob.perform_later + end + + if update_params['default_work_image'] + # Reindex all Works to apply new default work image + ReindexWorksJob.perform_later + end + + redirect_to({ action: :show }, notice: t('.flash.success')) + end + + private + + def add_breadcrumbs + add_breadcrumb t(:'hyrax.controls.home'), root_path + add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path + add_breadcrumb t(:'hyrax.admin.sidebar.configuration'), '#' + add_breadcrumb t(:'hyrax.admin.sidebar.appearance'), request.path + end + + def load_home_theme_names + home_theme_names = [] + @home_theme_information.each do |theme, value_hash| + value_hash.each do |key, value| + home_theme_names << [value, theme] if key == 'name' + end + end + home_theme_names + end + + def load_show_theme_names + show_theme_names = [] + @show_theme_information.each do |theme, value_hash| + value_hash.each do |key, value| + show_theme_names << [value, theme] if key == 'name' + end + end + show_theme_names + end + + def load_search_themes + { + 'List view' => 'list_view', + 'Gallery view' => 'gallery_view' + } + end + end + end +end + +Hyrax::Admin::AppearancesController.prepend(Hyrax::Admin::AppearancesControllerDecorator) diff --git a/app/controllers/hyrax/admin/workflow_roles_controller.rb b/app/controllers/hyrax/admin/workflow_roles_controller.rb deleted file mode 100644 index 9e388f66ee..0000000000 --- a/app/controllers/hyrax/admin/workflow_roles_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax v3.4.2 Expand to allow adding groups to workflow roles -module Hyrax - module Admin - class WorkflowRolesController < ApplicationController - before_action :require_permissions - with_themed_layout 'dashboard' - - def index - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t(:'hyrax.admin.workflow_roles.header'), hyrax.admin_workflow_roles_path - @presenter = WorkflowRolesPresenter.new - end - - def destroy - responsibility = Sipity::WorkflowResponsibility.find(params[:id]) - authorize! :destroy, responsibility - responsibility.destroy - redirect_to admin_workflow_roles_path - end - - # OVERRIDE: Add WorkflowResponsibilityGroupForm - def create - authorize! :create, Sipity::WorkflowResponsibility - - # Determine which form it is, user or group - form_params = params[:sipity_workflow_responsibility] - form = if form_params[:user_id].present? - Forms::WorkflowResponsibilityForm.new(form_params) - else - Forms::WorkflowResponsibilityGroupForm.new(form_params) - end - - begin - form.save! - rescue ActiveRecord::RecordNotUnique - logger.info "Not unique *****\n\n\n" - end - redirect_to admin_workflow_roles_path - end - - private - - def require_permissions - authorize! :read, :admin_dashboard - end - end - end -end diff --git a/app/controllers/hyrax/contact_form_controller.rb b/app/controllers/hyrax/contact_form_controller.rb deleted file mode 100644 index a420f82d45..0000000000 --- a/app/controllers/hyrax/contact_form_controller.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE: Hyrax v3.4.0 -# - add inject_theme_views method for theming -# - add homepage presenter for access to feature flippers -# - add access to content blocks in the show method -# - add @featured_collection_list to new method - -module Hyrax - class ContactFormController < ApplicationController - # OVERRIDE: Hyrax v3.4.0 Add for theming - # Adds Hydra behaviors into the application controller - include Blacklight::SearchContext - include Blacklight::SearchHelper - include Blacklight::AccessControls::Catalog - before_action :build_contact_form - layout 'homepage' - # OVERRIDE: Adding inject theme views method for theming - around_action :inject_theme_views - class_attribute :model_class - self.model_class = Hyrax::ContactForm - before_action :setup_negative_captcha, only: %i[new create] - # OVERRIDE: Hyrax v3.4.0 Add for theming - # The search builder for finding recent documents - # Override of Blacklight::RequestBuilders - def search_builder_class - Hyrax::HomepageSearchBuilder - end - - # OVERRIDE: Hyrax v3.4.0 Add for theming - class_attribute :presenter_class - # OVERRIDE: Hyrax v3.4.0 Add for theming - self.presenter_class = Hyrax::HomepagePresenter - - helper Hyrax::ContentBlockHelper - - def new - # OVERRIDE: Hyrax v3.4.0 Add for theming - @presenter = presenter_class.new(current_ability, collections) - @featured_researcher = ContentBlock.for(:researcher) - @marketing_text = ContentBlock.for(:marketing) - @home_text = ContentBlock.for(:home_text) - @featured_work_list = FeaturedWorkList.new - # OVERRIDE: Hyrax 3.4.0 add @featured_collection_list - @featured_collection_list = FeaturedCollectionList.new - @announcement_text = ContentBlock.for(:announcement) - end - - def create - # not spam, form is valid, and captcha is valid - @captcha.values[:category] = params[:contact_form][:category] - @captcha.values[:contact_method] = params[:contact_form][:contact_method] - @captcha.values[:subject] = params[:contact_form][:subject] - @contact_form = model_class.new(@captcha.values) - if @contact_form.valid? && @captcha.valid? - ContactMailer.contact(@contact_form).deliver_now - flash.now[:notice] = 'Thank you for your message!' - after_deliver - else - flash.now[:error] = 'Sorry, this message was not sent successfully. ' + - @contact_form.errors.full_messages.map(&:to_s).join(", ") + - "" + @captcha.error - end - render :new - rescue RuntimeError => exception - handle_create_exception(exception) - end - - def handle_create_exception(exception) - logger.error("Contact form failed to send: #{exception.inspect}") - flash.now[:error] = 'Sorry, this message was not delivered.' - render :new - end - - # Override this method if you want to perform additional operations - # when a email is successfully sent, such as sending a confirmation - # response to the user. - def after_deliver; end - - private - - def build_contact_form - @contact_form = model_class.new(contact_form_params) - end - - def contact_form_params - return {} unless params.key?(:contact_form) - params.require(:contact_form).permit(:contact_method, :category, :name, :email, :subject, :message) - end - - # OVERRIDE: return collections for theming - def collections(rows: 6) - builder = Hyrax::CollectionSearchBuilder.new(self) - .rows(rows) - response = repository.search(builder) - response.documents - rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest - [] - 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' - original_paths = view_paths - home_theme_view_path = Rails.root.join('app', 'views', "themes", home_page_theme.to_s) - prepend_view_path(home_theme_view_path) - yield - # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. - view_paths=(original_paths) - # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - else - yield - end - end - - def setup_negative_captcha - @captcha = NegativeCaptcha.new( - # A secret key entered in environment.rb. 'rake secret' will give you a good one. - secret: ENV.fetch('NEGATIVE_CAPTCHA_SECRET', 'default-value-change-me'), - spinner: request.remote_ip, - # Whatever fields are in your form - fields: %i[name email subject message], - # If you wish to override the default CSS styles (position: absolute; left: -2000px;) - # used to position the fields off-screen - css: "display: none", - params: params - ) - end - end -end diff --git a/app/controllers/hyrax/contact_form_controller_decorator.rb b/app/controllers/hyrax/contact_form_controller_decorator.rb new file mode 100644 index 0000000000..63d7bef19a --- /dev/null +++ b/app/controllers/hyrax/contact_form_controller_decorator.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +# OVERRIDE: Hyrax v5.0.0rc2 +# - adds inject_theme_views method for theming +# - adds homepage presenter for access to feature flippers +# - adds access to content blocks in the show method +# - adds @featured_collection_list to new method +# - adds captcha +module Hyrax + module ContactFormControllerDecorator + extend ActiveSupport::Concern + + # OVERRIDE: Add for theming + # Adds Hydra behaviors into the application controller + include Blacklight::SearchContext + include Blacklight::AccessControls::Catalog + + prepended do + # OVERRIDE: Adding inject theme views method for theming + around_action :inject_theme_views + before_action :setup_negative_captcha, only: %i[new create] + + # OVERRIDE: Add for theming + class_attribute :presenter_class + self.presenter_class = Hyrax::HomepagePresenter + + helper Hyrax::ContentBlockHelper + end + + # OVERRIDE: Add for theming + # The search builder for finding recent documents + # Override of Blacklight::RequestBuilders + def search_builder_class + Hyrax::HomepageSearchBuilder + end + + def new + # OVERRIDE: Add for theming + @presenter = presenter_class.new(current_ability, collections) + @featured_researcher = ContentBlock.for(:researcher) + @marketing_text = ContentBlock.for(:marketing) + @home_text = ContentBlock.for(:home_text) + @featured_work_list = FeaturedWorkList.new + @featured_collection_list = FeaturedCollectionList.new + @announcement_text = ContentBlock.for(:announcement) + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def create + # not spam and a valid form + # Override to include captcha + @captcha.values[:category] = params[:contact_form][:category] + @captcha.values[:contact_method] = params[:contact_form][:contact_method] + @captcha.values[:subject] = params[:contact_form][:subject] + @contact_form = model_class.new(@captcha.values) + if @contact_form.valid? && @captcha.valid? + ContactMailer.contact(@contact_form).deliver_now + flash.now[:notice] = 'Thank you for your message!' + after_deliver + else + flash.now[:error] = 'Sorry, this message was not sent successfully. ' + + @contact_form.errors.full_messages.map(&:to_s).join(", ") + end + render :new + rescue RuntimeError => exception + handle_create_exception(exception) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + private + + # OVERRIDE: return collections for theming + # Return 6 collections, sorts by title + def collections(rows: 6) + Hyrax::CollectionsService.new(self).search_results do |builder| + builder.rows(rows) + builder.merge(sort: "title_ssi") + end + rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest + [] + 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' + original_paths = view_paths + Hyku::Application.theme_view_path_roots.each do |root| + home_theme_view_path = File.join(root, 'app', 'views', "themes", home_page_theme.to_s) + prepend_view_path(home_theme_view_path) + end + yield + # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. + view_paths=(original_paths) + # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + else + yield + end + end + + def setup_negative_captcha + @captcha = NegativeCaptcha.new( + # A secret key entered in environment.rb. 'rake secret' will give you a good one. + secret: ENV.fetch('NEGATIVE_CAPTCHA_SECRET', 'default-value-change-me'), + spinner: request.remote_ip, + # Whatever fields are in your form + fields: %i[name email subject message], + # If you wish to override the default CSS styles (position: absolute; left: -2000px;) + # used to position the fields off-screen + css: "display: none", + params: + ) + end + end +end + +Hyrax::ContactFormController.prepend(Hyrax::ContactFormControllerDecorator) diff --git a/app/controllers/hyrax/content_blocks_controller.rb b/app/controllers/hyrax/content_blocks_controller.rb deleted file mode 100644 index 60edc5286d..0000000000 --- a/app/controllers/hyrax/content_blocks_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax v3.4.0 to add home_text to permitted_params - Adding themes -module Hyrax - class ContentBlocksController < ApplicationController - load_and_authorize_resource - with_themed_layout 'dashboard' - - def edit - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t(:'hyrax.admin.sidebar.configuration'), '#' - add_breadcrumb t(:'hyrax.admin.sidebar.content_blocks'), hyrax.edit_content_blocks_path - end - - def update - respond_to do |format| - if @content_block.update(value: update_value_from_params) - format.html { redirect_to hyrax.edit_content_blocks_path, notice: t(:'hyrax.content_blocks.updated') } - else - format.html { render :edit } - end - end - end - - private - - # override hyrax v2.9.0 added the home_text content block to permitted_params - Adding Themes - def permitted_params - params.require(:content_block).permit(:marketing, - :announcement, - :home_text, - :researcher) - end - - # When a request comes to the controller, it will be for one and - # only one of the content blocks. Params always looks like: - # {'about_page' => 'Here is an awesome about page!'} - # So reach into permitted params and pull out the first value. - def update_value_from_params - permitted_params.values.first - end - end -end diff --git a/app/controllers/hyrax/content_blocks_controller_decorator.rb b/app/controllers/hyrax/content_blocks_controller_decorator.rb new file mode 100644 index 0000000000..bcca43e360 --- /dev/null +++ b/app/controllers/hyrax/content_blocks_controller_decorator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to add home_text to permitted_params - Adding themes + +module Hyrax + module ContentBlocksControllerDecorator + private + + def permitted_params + params.require(:content_block).permit(:marketing, + :announcement, + :home_text, + :homepage_about_section_heading, + :homepage_about_section_content, + :researcher) + end + end +end + +Hyrax::ContentBlocksController.prepend Hyrax::ContentBlocksControllerDecorator diff --git a/app/controllers/hyrax/dashboard/collections_controller.rb b/app/controllers/hyrax/dashboard/collections_controller.rb deleted file mode 100644 index 7622526455..0000000000 --- a/app/controllers/hyrax/dashboard/collections_controller.rb +++ /dev/null @@ -1,737 +0,0 @@ -# OVERRIDE Hyrax v3.4.2 -# - Fix file upload in logo and banner -# - Use work titles for collection thumbnail select & to add an option to reset to the default thumbnail -module Hyrax - module Dashboard - ## Shows a list of all collections to the admins - class CollectionsController < Hyrax::My::CollectionsController - include Blacklight::AccessControls::Catalog - include Blacklight::Base - - configure_blacklight do |config| - config.search_builder_class = Hyrax::Dashboard::CollectionsSearchBuilder - end - - include BreadcrumbsForCollections - with_themed_layout 'dashboard' - - before_action :filter_docs_with_read_access!, except: %i[show edit] - before_action :remove_select_something_first_flash, except: :show - - include Hyrax::Collections::AcceptsBatches - - # include the render_check_all view helper method - helper Hyrax::BatchEditsHelper - # include the display_trophy_link view helper method - helper Hyrax::TrophyHelper - - # Catch permission errors - rescue_from Hydra::AccessDenied, CanCan::AccessDenied, with: :deny_collection_access - - # actions: index, create, new, edit, show, update, destroy, permissions, citation - before_action :authenticate_user!, except: [:index] - - class_attribute :presenter_class, - :form_class, - :single_item_search_builder_class, - :membership_service_class - - self.presenter_class = Hyrax::CollectionPresenter - - self.form_class = Hyrax::Forms::CollectionForm - - # The search builder to find the collection - self.single_item_search_builder_class = SingleCollectionSearchBuilder - # The search builder to find the collections' members - self.membership_service_class = Collections::CollectionMemberSearchService - - load_and_authorize_resource except: [:index, :create], - instance_name: :collection, - class: Hyrax.config.collection_model - - def deny_collection_access(exception) - if exception.action == :edit - redirect_to(url_for(action: 'show'), alert: 'You do not have sufficient privileges to edit this document') - elsif current_user&.persisted? - redirect_to root_url, alert: exception.message - else - session['user_return_to'] = request.url - redirect_to main_app.new_user_session_url, alert: exception.message - end - end - - def new - # Coming from the UI, a collection type id should always be present. Coming from the API, if a collection type id is not specified, - # use the default collection type (provides backward compatibility with versions < Hyrax 2.1.0) - collection_type_id = params[:collection_type_id].presence || default_collection_type.id - @collection.collection_type_gid = CollectionType.find(collection_type_id).to_global_id - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t('.header', type_title: collection_type.title), request.path - @collection.try(:apply_depositor_metadata, current_user.user_key) - form - end - - def show - # unused assignment to be removed in 4.0.0 - @banner_file = presenter.banner_file if collection_type.brandable? - - presenter - query_collection_members - end - - def edit - form - collection_type - # Gets original filename of an uploaded thumbnail. See #update - if ::SolrDocument.find(@collection.id).thumbnail_path.include? "uploaded_collection_thumbnails" and uploaded_thumbnail? - @thumbnail_filename = File.basename(uploaded_thumbnail_files.reject { |f| File.basename(f).include? @collection.id }.first) - end - end - - def uploaded_thumbnail? - uploaded_thumbnail_files.any? - end - - def uploaded_thumbnail_files - Dir["#{UploadedCollectionThumbnailPathService.upload_dir(@collection)}/*"] - end - - def after_create - Deprecation.warn("Method `#after_create` will be removed in Hyrax 4.0.") - after_create_response # call private method for processing - end - - def after_create_error - Deprecation.warn("Method `#after_create_error` will be removed in Hyrax 4.0.") - after_create_errors("") # call private method for processing - end - - def create - # Manual load and authorize necessary because Cancan will pass in all - # form attributes. When `permissions_attributes` are present the - # collection is saved without a value for `has_model.` - @collection = Hyrax.config.collection_class.new - authorize! :create, @collection - - case @collection - when ActiveFedora::Base - create_active_fedora_collection - else - create_valkyrie_collection - end - end - - def after_update - Deprecation.warn("Method `#after_update` will be removed in Hyrax 4.0.") - after_update_response # call private method for processing - end - - def after_update_error - Deprecation.warn("Method `#after_update_error` will be removed in Hyrax 4.0.") - after_update_errors(@collection.errors) # call private method for processing - end - - def update - # OVERRIDE: ensure user is allowed to change visibility - authorize! :manage_discovery, @collection if collection_params[:visibility].present? && @collection.visibility != collection_params[:visibility] - - case @collection - when ActiveFedora::Base - update_active_fedora_collection - else - update_valkyrie_collection - end - end - - def process_branding - process_banner_input - process_logo_input - # TODO does this still work? - process_uploaded_thumbnail(params[:collection][:thumbnail_upload]) if params[:collection][:thumbnail_upload] - end - - # Deletes any previous thumbnails. The thumbnail indexer (see services/hyrax/indexes_thumbnails) - # checks if an uploaded thumbnail exists in the public folder before indexing the thumbnail path. - def delete_uploaded_thumbnail - FileUtils.rm_rf(uploaded_thumbnail_files) - @collection.update_index - - respond_to do |format| - format.html - format.js # renders delete_uploaded_thumbnail.js.erb, which updates _current_thumbnail.html.erb - end - end - - def after_destroy(_id) - # leaving id to avoid changing the method's parameters prior to release - respond_to do |format| - format.html do - redirect_to hyrax.my_collections_path, - notice: t('hyrax.dashboard.my.action.collection_delete_success') - end - format.json { head :no_content, location: hyrax.my_collections_path } - end - end - - def after_destroy_error(id) - respond_to do |format| - format.html do - flash[:notice] = t('hyrax.dashboard.my.action.collection_delete_fail') - render :edit, status: :unprocessable_entity - end - format.json { render json: { id: id }, status: :unprocessable_entity, location: dashboard_collection_path(@collection) } - end - end - - def destroy - case @collection - when Valkyrie::Resource - valkyrie_destroy - else - if @collection.destroy - after_destroy(params[:id]) - else - after_destroy_error(params[:id]) - end - end - rescue StandardError => err - Rails.logger.error(err) - after_destroy_error(params[:id]) - end - - def collection - action_name == 'show' ? @presenter : @collection - end - - # Renders a JSON response with a list of files in this collection - # This is used by the edit form to populate the thumbnail_id dropdown - # OVERRIDE: Hyrax 2.9 to use work titles for collection thumbnail select & to add an option to reset to the default thumbnail - def files - params[:q] = '' unless params[:q] - builder = Hyrax::CollectionMemberSearchBuilder.new(scope: self, collection: collection, search_includes_models: :works) - # get the default work image because we do not want to show any works in this dropdown that only have the default work image. this indicates that they have no files attached, and will throw an error if selected. - default_work_thumbnail_path = Site.instance.default_work_image&.url.presence || ActionController::Base.helpers.image_path('default.png') - work_with_no_files_thumbnail_path = ActionController::Base.helpers.image_path('work.png') - response = repository.search(builder.where(params[:q]).query) - # only return the works that have files, because these will be the only ones with a viable thumbnail - result = response.documents.reject { |document| document["thumbnail_path_ss"].blank? || document["thumbnail_path_ss"].include?(default_work_thumbnail_path) || document["thumbnail_path_ss"].include?(work_with_no_files_thumbnail_path) }.map do |document| - { id: document["thumbnail_path_ss"].split('/').last.gsub(/\?.*/, ''), text: document["title_tesim"].first } - end - reset_thumbnail_option = { - id: '', - text: 'Default thumbnail' - } - result << reset_thumbnail_option - render json: result - end - - private - - def create_active_fedora_collection - # Coming from the UI, a collection type gid should always be present. Coming from the API, if a collection type gid is not specified, - # use the default collection type (provides backward compatibility with versions < Hyrax 2.1.0) - @collection.collection_type_gid = params[:collection_type_gid].presence || default_collection_type.to_global_id - @collection.attributes = collection_params.except(:members, :parent_id, :collection_type_gid) - @collection.apply_depositor_metadata(current_user.user_key) - @collection.visibility = Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE unless @collection.discoverable? - if @collection.save - after_create_response - else - after_create_errors(@collection.errors) - end - end - - def create_valkyrie_collection - return after_create_errors(form_err_msg(form)) unless form.validate(collection_params) - - result = - transactions['change_set.create_collection'] - .with_step_args( - 'change_set.set_user_as_depositor' => { user: current_user }, - 'change_set.add_to_collections' => { collection_ids: Array(params[:parent_id]) }, - 'collection_resource.apply_collection_type_permissions' => { user: current_user } - ) - .call(form) - - @collection = result.value_or { return after_create_errors(result.failure.first) } - after_create_response - end - - def update_active_fedora_collection - process_member_changes - process_branding - - @collection.visibility = Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE unless @collection.discoverable? - # we don't have to reindex the full graph when updating collection - @collection.try(:reindex_extent=, Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX) - if @collection.update(collection_params.except(:members)) - after_update_response - else - after_update_errors(@collection.errors) - end - end - - def update_valkyrie_collection - return after_update_errors(form_err_msg(form)) unless form.validate(collection_params) - - result = transactions['change_set.update_collection'] - .with_step_args( - 'collection_resource.save_collection_banner' => { update_banner_file_ids: params["banner_files"], - banner_unchanged_indicator: params["banner_unchanged"] }, - 'collection_resource.save_collection_logo' => { update_logo_file_ids: params["logo_files"], - alttext_values: params["alttext"], - linkurl_values: params["linkurl"] } - ) - .call(form) - @collection = result.value_or { return after_update_errors(result.failure.first) } - - process_member_changes - after_update_response - end - - def valkyrie_destroy - if transactions['collection_resource.destroy'].call(@collection).successful? - after_destroy(params[:id]) - else - after_destroy_error(params[:id]) - end - end - - def form_err_msg(form) - errmsg = [] - form.errors.messages.each do |fld, err| - errmsg << "#{fld} #{err.to_sentence}" - end - errmsg.to_sentence - end - - def default_collection_type - Hyrax::CollectionType.find_or_create_default_collection_type - end - - def default_collection_type_gid - default_collection_type.to_global_id.to_s - end - - def collection_type - @collection_type ||= CollectionType.find_by_gid!(collection.collection_type_gid) - end - - def link_parent_collection(parent_id) - child = collection.respond_to?(:valkyrie_resource) ? collection.valkyrie_resource : collection - Hyrax::Collections::CollectionMemberService.add_member(collection_id: parent_id, - new_member: child, - user: current_user) - end - - def uploaded_files(uploaded_file_ids) - return [] if uploaded_file_ids.empty? - UploadedFile.find(uploaded_file_ids) - end - - def update_referer - return edit_dashboard_collection_path(@collection) + (params[:referer_anchor] || '') if params[:stay_on_edit] - dashboard_collection_path(@collection) - end - - # branding specific methods - def process_banner_input - return update_existing_banner if params["banner_unchanged"] == "true" - remove_banner - uploaded_file_ids = params["banner_files"] - banner_text = params["banner_text"]&.first - add_new_banner(uploaded_file_ids, banner_text) if uploaded_file_ids - end - - def update_existing_banner - banner_info = CollectionBrandingInfo.where(collection_id: @collection.id.to_s).where(role: "banner") - banner_info.first.alt_text = params["banner_text"].first - banner_info.first.save(banner_info.first.local_path, false) - end - - def add_new_banner(uploaded_file_ids, banner_text) - f = uploaded_files(uploaded_file_ids).first - ## OVERRIDE Hyrax 3.4.0 - process locations - file_location = process_file_location(f) - - banner_info = CollectionBrandingInfo.new( - collection_id: @collection.id, - filename: File.split(f.file_url).last, - role: "banner", - alt_txt: banner_text, - target_url: "" - ) - ## OVERRIDE Hyrax 3.4.0 - process locations - banner_info.save file_location - end - - def remove_banner - banner_info = CollectionBrandingInfo.where(collection_id: @collection.id.to_s).where(role: "banner") - banner_info&.delete_all - end - - def update_logo_info(uploaded_file_id, alttext, linkurl) - logo_info = CollectionBrandingInfo.where(collection_id: @collection.id.to_s).where(role: "logo").where(local_path: uploaded_file_id.to_s) - logo_info.first.alt_text = alttext - logo_info.first.target_url = linkurl - logo_info.first.local_path = uploaded_file_id - logo_info.first.save(uploaded_file_id, false) - end - - def create_logo_info(uploaded_file_id, alttext, linkurl) - file = uploaded_files(uploaded_file_id) - file_location = process_file_location(file) # OVERRIDE Hyrax 3.4.0 to clean file location - - logo_info = CollectionBrandingInfo.new( - collection_id: @collection.id, - filename: File.split(file.file_url).last, - role: "logo", - alt_txt: alttext, - target_url: linkurl - ) - logo_info.save file_location - logo_info - end - - def remove_redundant_files(public_files) - # remove any public ones that were not included in the selection. - logos_info = CollectionBrandingInfo.where(collection_id: @collection.id.to_s).where(role: "logo") - logos_info.each do |logo_info| - logo_info.delete(logo_info.local_path) unless public_files.include? logo_info.local_path - logo_info.destroy unless public_files.include? logo_info.local_path - end - end - - def process_logo_records(uploaded_file_ids) - public_files = [] - uploaded_file_ids.each_with_index do |ufi, i| - # If the user has chosen a new logo, the ufi will be an integer - # If the logo was previously chosen, the ufi will be a path - # If it is a path, update the rec, else create a new rec - if !ufi.match(/\D/).nil? - update_logo_info(ufi, params["alttext"][i], verify_linkurl(params["linkurl"][i])) - public_files << ufi - else # brand new one, insert in the database - logo_info = create_logo_info(ufi, params["alttext"][i], verify_linkurl(params["linkurl"][i])) - public_files << logo_info.local_path - end - end - public_files - end - - def process_logo_input - uploaded_file_ids = params["logo_files"] - public_files = [] - - if uploaded_file_ids.nil? - remove_redundant_files public_files - return - end - - public_files = process_logo_records uploaded_file_ids - remove_redundant_files public_files - end - - # run a solr query to get the collections the user has access to edit - # @return [Array] a list of the user's collections - def find_collections_for_form - Hyrax::CollectionsService.new(self).search_results(:edit) - end - - def remove_select_something_first_flash - flash.delete(:notice) if flash.notice == 'Select something first' - end - - def presenter - @presenter ||= presenter_class.new(curation_concern, current_ability) - end - - def curation_concern - # Query Solr for the collection. - # run the solr query to find the collection members - response, _docs = single_item_search_service.search_results - curation_concern = response.documents.first - raise CanCan::AccessDenied unless curation_concern - curation_concern - end - - def single_item_search_service - Hyrax::SearchService.new(config: blacklight_config, user_params: params.except(:q, :page), scope: self, search_builder_class: single_item_search_builder_class) - end - - # Instantiates the search builder that builds a query for a single item - # this is useful in the show view. - def single_item_search_builder - search_service.search_builder - end - deprecation_deprecate :single_item_search_builder - - def collection_params - if Hyrax.config.collection_class < ActiveFedora::Base - @participants = extract_old_style_permission_attributes(params[:collection]) - form_class.model_attributes(params[:collection]) - else - params.permit(collection: {})[:collection] - .merge(params.permit(:collection_type_gid) - .with_defaults(collection_type_gid: default_collection_type_gid)) - .merge(member_of_collection_ids: Array(params[:parent_id])) - end - end - - def extract_old_style_permission_attributes(attributes) - # TODO: REMOVE in 3.0 - part of deprecation of permission attributes - permissions = attributes.delete("permissions_attributes") - return [] unless permissions - Deprecation.warn(self, "Passing in permissions_attributes parameter with a new collection is deprecated and support will be removed from Hyrax 3.0. " \ - "Use Hyrax::PermissionTemplate instead to grant Manage, Deposit, or View access.") - participants = [] - permissions.each do |p| - access = access(p) - participants << { agent_type: agent_type(p), agent_id: p["name"], access: access } if access - end - participants - end - - def agent_type(permission) - # TODO: REMOVE in 3.0 - part of deprecation of permission attributes - return 'group' if permission["type"] == 'group' - 'user' - end - - def access(permission) - # TODO: REMOVE in 3.0 - part of deprecation of permission attributes - return Hyrax::PermissionTemplateAccess::MANAGE if permission["access"] == 'edit' - return Hyrax::PermissionTemplateAccess::VIEW if permission["access"] == 'read' - end - - def process_member_changes - case params[:collection][:members] - when 'add' then add_members_to_collection - when 'remove' then remove_members_from_collection - when 'move' then move_members_between_collections - end - end - - def add_members_to_collection(collection = nil, collection_id: nil) - collection_id ||= (collection.try(:id) || @collection.id) - - Hyrax::Collections::CollectionMemberService - .add_members_by_ids(collection_id: collection_id, - new_member_ids: batch, - user: current_user) - end - - def remove_members_from_collection - Hyrax::Collections::CollectionMemberService - .remove_members_by_ids(collection_id: @collection.id, - member_ids: batch, - user: current_user) - end - - def move_members_between_collections - remove_members_from_collection - add_members_to_collection(collection_id: params[:destination_collection_id]) - - destination_title = - Hyrax.query_service.find_by(id: params[:destination_collection_id]).title.first || - params[:destination_collection_id] - flash[:notice] = "Successfully moved #{batch.count} files to #{destination_title} Collection." - rescue StandardError => err - Rails.logger.error(err) - destination_title = - Hyrax.query_service.find_by(id: params[:destination_collection_id]).title.first || - destination_id - flash[:error] = "An error occured. Files were not moved to #{destination_title} Collection." - end - - # Include 'catalog' and 'hyrax/base' in the search path for views, while prefering - # our local paths. Thus we are unable to just override `self.local_prefixes` - def _prefixes - @_prefixes ||= super + ['catalog', 'hyrax/base'] - end - - def ensure_admin! - # Even though the user can view this collection, they may not be able to view - # it on the admin page. - authorize! :read, :admin_dashboard - end - - def search_action_url(*args) - hyrax.dashboard_collections_url(*args) - end - - def form - @form ||= - case @collection - when Valkyrie::Resource - form = Hyrax::Forms::ResourceForm.for(@collection) - form.prepopulate! - form - else - form_class.new(@collection, current_ability, repository) - end - end - - def set_default_permissions - additional_grants = @participants # Grants converted from older versions (< Hyrax 2.1.0) where share was edit or read access instead of managers, depositors, and viewers - Collections::PermissionsCreateService.create_default(collection: @collection, creating_user: current_user, grants: additional_grants) - end - - def query_collection_members - member_works - member_subcollections if collection_type.nestable? - parent_collections if collection_type.nestable? && action_name == 'show' - end - - # Instantiate the membership query service - def collection_member_service - @collection_member_service ||= membership_service_class.new(scope: self, collection: collection, params: params_for_query) - end - - def member_works - @response = collection_member_service.available_member_works - @member_docs = @response.documents - @members_count = @response.total - end - - def member_subcollections - results = collection_member_service.available_member_subcollections - @subcollection_solr_response = results - @subcollection_docs = results.documents - @subcollection_count = @presenter.nil? ? 0 : @subcollection_count = @presenter.subcollection_count = results.total - end - - def parent_collections - page = params[:parent_collection_page].to_i - query = Hyrax::Collections::NestedCollectionQueryService - collection.parent_collections = query.parent_collections(child: collection_object, scope: self, page: page) - end - - def collection_object - @collection - end - - # You can override this method if you need to provide additional - # inputs to the search builder. For example: - # search_field: 'all_fields' - # @return the inputs required for the collection member search builder - def params_for_query - params.merge(q: params[:cq]) - end - - # Only accept HTTP|HTTPS urls; - # @return the url - def verify_linkurl(linkurl) - url = Loofah.scrub_fragment(linkurl, :prune).to_s - url if valid_url?(url) - end - - def valid_url?(url) - (url =~ URI.regexp(['http', 'https'])) - end - - def after_create_response - if @collection.is_a?(ActiveFedora::Base) - form - set_default_permissions - # if we are creating the new collection as a subcollection (via the nested collections controller), - # we pass the parent_id through a hidden field in the form and link the two after the create. - link_parent_collection(params[:parent_id]) unless params[:parent_id].nil? - end - respond_to do |format| - Hyrax::SolrService.commit - format.html { redirect_to edit_dashboard_collection_path(@collection), notice: t('hyrax.dashboard.my.action.collection_create_success') } - format.json { render json: @collection, status: :created, location: dashboard_collection_path(@collection) } - end - add_members_to_collection unless batch.empty? - end - - def after_create_errors_for_active_fedora(errors) - form - respond_to do |format| - format.html do - flash[:error] = errors.to_s - render action: 'new' - end - format.json { render json: @collection.errors, status: :unprocessable_entity } - end - end - - def after_create_errors(errors) # for valkyrie - return after_create_errors_for_active_fedora(errors) if @collection.is_a? ActiveFedora::Base - respond_to do |wants| - wants.html do - flash[:error] = errors.to_s - render 'new', status: :unprocessable_entity - end - wants.json do - render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) - end - end - end - - def after_update_response - respond_to do |format| - format.html { redirect_to update_referer, notice: t('hyrax.dashboard.my.action.collection_update_success') } - format.json { render json: @collection, status: :updated, location: dashboard_collection_path(@collection) } - end - end - - def after_update_errors_for_active_fedora(errors) - form - respond_to do |format| - format.html do - flash[:error] = errors.to_s - render action: 'edit' - end - format.json { render json: @collection.errors, status: :unprocessable_entity } - end - end - - def after_update_errors(errors) # for valkyrie - return after_update_errors_for_active_fedora(errors) if @collection.is_a? ActiveFedora::Base - respond_to do |wants| - wants.html do - flash[:error] = errors.to_s - render 'edit', status: :unprocessable_entity - end - wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) } - end - end - - def process_uploaded_thumbnail(uploaded_file) - dir_name = UploadedCollectionThumbnailPathService.upload_dir(@collection) - saved_file = Rails.root.join(dir_name, uploaded_file.original_filename) - # Create directory if it doesn't already exist - unless File.directory?(dir_name) - FileUtils.mkdir_p(dir_name) - else # clear contents - delete_uploaded_thumbnail - end - File.open(saved_file, 'wb') do |file| - file.write(uploaded_file.read) - end - image = MiniMagick::Image.open(saved_file) - # Save two versions of the image: one for homepage feature cards and one for regular thumbnail - image.resize('500x900').format("jpg").write("#{dir_name}/#{@collection.id}_card.jpg") - image.resize('150x300').format("jpg").write("#{dir_name}/#{@collection.id}_thumbnail.jpg") - File.chmod(0664,"#{dir_name}/#{@collection.id}_thumbnail.jpg") - File.chmod(0664,"#{dir_name}/#{@collection.id}_card.jpg") - end - - ## OVERRIDE Hyrax 3.4.0 handle file locations - def process_file_location(f) - if f.file_url =~ /^http/ - f.file.download!(f.file_url) - f.file_url - elsif f.file_url =~ %r{^\/} - f.file.path - else - f.file_url - end - end - ## END OVERRIDE - end - end -end diff --git a/app/controllers/hyrax/dashboard/collections_controller_decorator.rb b/app/controllers/hyrax/dashboard/collections_controller_decorator.rb new file mode 100644 index 0000000000..dcc1fb94b5 --- /dev/null +++ b/app/controllers/hyrax/dashboard/collections_controller_decorator.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 +# - Fix file upload in logo and banner +# - Use work titles for collection thumbnail select & to add an option to reset to the default thumbnail +module Hyrax + module Dashboard + ## Shows a list of all collections to the admins + # rubocop:disable Metrics/ModuleLength + module CollectionsControllerDecorator + def edit + form + collection_type + # Gets original filename of an uploaded thumbnail. See #update + return unless ::SolrDocument.find(@collection.id).thumbnail_path.include?("uploaded_collection_thumbnails") && uploaded_thumbnail? + @thumbnail_filename = File.basename(uploaded_thumbnail_files.reject { |f| File.basename(f).include? @collection.id }.first) + end + + def uploaded_thumbnail? + uploaded_thumbnail_files.any? + end + + def uploaded_thumbnail_files + Dir["#{UploadedCollectionThumbnailPathService.upload_dir(@collection)}/*"] + end + + def update + # OVERRIDE: ensure user is allowed to change visibility + authorize! :manage_discovery, @collection if collection_params[:visibility].present? && @collection.visibility != collection_params[:visibility] + + super + end + + def process_branding + super + + # TODO: does this still work? + process_uploaded_thumbnail(params[:collection][:thumbnail_upload]) if params[:collection][:thumbnail_upload] + end + + # Deletes any previous thumbnails. The thumbnail indexer (see services/hyrax/indexes_thumbnails) + # checks if an uploaded thumbnail exists in the public folder before indexing the thumbnail path. + def delete_uploaded_thumbnail + FileUtils.rm_rf(uploaded_thumbnail_files) + @collection.update_index + + respond_to do |format| + format.html + format.js # renders delete_uploaded_thumbnail.js.erb, which updates _current_thumbnail.html.erb + end + end + + # Renders a JSON response with a list of files in this collection + # This is used by the edit form to populate the thumbnail_id dropdown + # OVERRIDE: Hyrax 2.9 to use work titles for collection thumbnail select & to add an option to reset to the default thumbnail + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + def files + params[:q] = '' unless params[:q] + builder = Hyrax::CollectionMemberSearchBuilder.new(scope: self, collection:, search_includes_models: :works) + # get the default work image because we do not want to show any works in this + # dropdown that only have the default work image. this indicates that they have + # no files attached, and will throw an error if selected. + default_work_thumbnail_path = Site.instance.default_work_image&.url.presence || ActionController::Base.helpers.image_path('default.png') + work_with_no_files_thumbnail_path = ActionController::Base.helpers.image_path('work.png') + response = repository.search(builder.where(params[:q]).query) + # only return the works that have files, because these will be the only ones with a viable thumbnail + result = response.documents.reject do |document| + document["thumbnail_path_ss"].blank? || document["thumbnail_path_ss"].include?(default_work_thumbnail_path) || + document["thumbnail_path_ss"].include?(work_with_no_files_thumbnail_path) + # rubocop:disable Style/MultilineBlockChain + end.map do |document| + # rubocop:enable Style/MultilineBlockChain + { id: document["thumbnail_path_ss"].split('/').last.gsub(/\?.*/, ''), text: document["title_tesim"].first } + end + reset_thumbnail_option = { + id: '', + text: 'Default thumbnail' + } + result << reset_thumbnail_option + render json: result + end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + + private + + # branding specific methods + def process_banner_input + return update_existing_banner if params["banner_unchanged"] == "true" + remove_banner + uploaded_file_ids = params["banner_files"] + banner_text = params["banner_text"]&.first + add_new_banner(uploaded_file_ids, banner_text) if uploaded_file_ids + end + + def update_existing_banner + banner_info = CollectionBrandingInfo.where(collection_id: @collection.id.to_s).where(role: "banner") + banner_info.first.alt_text = params["banner_text"].first + banner_info.first.save(banner_info.first.local_path, false) + end + + def add_new_banner(uploaded_file_ids, banner_text) + f = uploaded_files(uploaded_file_ids).first + ## OVERRIDE Hyrax v5.0.0rc2 - process locations + file_location = process_file_location(f) + + banner_info = CollectionBrandingInfo.new( + collection_id: @collection.id, + filename: File.split(f.file_url).last, + role: "banner", + alt_txt: banner_text, + target_url: "" + ) + ## OVERRIDE Hyrax v5.0.0rc2 - process locations + banner_info.save file_location + end + + def create_logo_info(uploaded_file_id, alttext, linkurl) + file = uploaded_files(uploaded_file_id) + file_location = process_file_location(file) # OVERRIDE Hyrax v5.0.0rc2 to clean file location + + logo_info = CollectionBrandingInfo.new( + collection_id: @collection.id, + filename: File.split(file.file_url).last, + role: "logo", + alt_txt: alttext, + target_url: linkurl + ) + logo_info.save file_location + logo_info + end + + def collection_params + if Hyrax.config.collection_class < ActiveFedora::Base + @participants = extract_old_style_permission_attributes(params[:collection]) + form_class.model_attributes(params[:collection]) + else + params.permit(collection: {})[:collection] + .merge(params.permit(:collection_type_gid) + .with_defaults(collection_type_gid: default_collection_type_gid)) + .merge(member_of_collection_ids: Array(params[:parent_id])) + end + end + + # rubocop:disable Metrics/MethodLength + def process_uploaded_thumbnail(uploaded_file) + dir_name = UploadedCollectionThumbnailPathService.upload_dir(@collection) + saved_file = Rails.root.join(dir_name, uploaded_file.original_filename) + # Create directory if it doesn't already exist + if File.directory?(dir_name) # clear contents + delete_uploaded_thumbnail + else + FileUtils.mkdir_p(dir_name) + end + File.open(saved_file, 'wb') do |file| + file.write(uploaded_file.read) + end + image = MiniMagick::Image.open(saved_file) + # Save two versions of the image: one for homepage feature cards and one for regular thumbnail + image.resize('500x900').format("jpg").write("#{dir_name}/#{@collection.id}_card.jpg") + image.resize('150x300').format("jpg").write("#{dir_name}/#{@collection.id}_thumbnail.jpg") + File.chmod(0o664, "#{dir_name}/#{@collection.id}_thumbnail.jpg") + File.chmod(0o664, "#{dir_name}/#{@collection.id}_card.jpg") + end + # rubocop:enable Metrics/MethodLength + + ## OVERRIDE Hyrax v5.0.0rc2 handle file locations + def process_file_location(f) + if /^http/.match?(f.file_url) + f.file.download!(f.file_url) + f.file_url + elsif %r{^\/}.match?(f.file_url) + f.file.path + else + f.file_url + end + end + ## END OVERRIDE + end + end +end + +Hyrax::Dashboard::CollectionsController.prepend(Hyrax::Dashboard::CollectionsControllerDecorator) diff --git a/app/controllers/hyrax/featured_collection_lists_controller.rb b/app/controllers/hyrax/featured_collection_lists_controller.rb index 29a9e3802e..d342517355 100644 --- a/app/controllers/hyrax/featured_collection_lists_controller.rb +++ b/app/controllers/hyrax/featured_collection_lists_controller.rb @@ -13,8 +13,8 @@ def create private - def list_params - params.require(:featured_collection_list).permit(featured_collections_attributes: %i[id order]) - end + def list_params + params.require(:featured_collection_list).permit(featured_collections_attributes: %i[id order]) + end end end diff --git a/app/controllers/hyrax/generic_works_controller.rb b/app/controllers/hyrax/generic_works_controller.rb index 243dfcbfd4..9831ea89ae 100644 --- a/app/controllers/hyrax/generic_works_controller.rb +++ b/app/controllers/hyrax/generic_works_controller.rb @@ -7,6 +7,7 @@ module Hyrax class GenericWorksController < ApplicationController # Adds Hyrax behaviors to the controller. include Hyrax::WorksControllerBehavior + include Hyku::WorksControllerBehavior include Hyrax::BreadcrumbsForWorks self.curation_concern_type = ::GenericWork diff --git a/app/controllers/hyrax/homepage_controller.rb b/app/controllers/hyrax/homepage_controller.rb index a38eda8453..a19c161037 100644 --- a/app/controllers/hyrax/homepage_controller.rb +++ b/app/controllers/hyrax/homepage_controller.rb @@ -1,27 +1,41 @@ # frozen_string_literal: true -# OVERRIDE: Hyrax v2.9.0 to add home_text content block to the index method - Adding themes -# OVERRIDE: Hyrax v2.9.0 from Hyrax v2.9.0 to add facets to home page - inheriting from -# CatalogController rather than ApplicationController -# OVERRIDE: Hyrax v2.9.0 from Hyrax v2.9.0 to add inject_theme_views method for theming -# OVERRIDE: Hyrax v2.9.0 to add search_action_url method from Blacklight 6.23.0 to make facet links to go to /catalog -# OVERRIDE: Hyrax v2.9.0 to add .sort_by to return collections in alphabetical order by title on the homepage -# OVERRIDE: Hyrax v2.9.0 add all_collections page for IR theme -# OVERRIDE: Hyrax v2.9.0 to add facet counts for resource types for IR theme -# OVERRIDE: Hyrax v. 2.9.0 to add @featured_collection_list to index method +######################################################################################### +######################################################################################### +# +# +# HACK: We have copied over the Hyrax::HomepageController to address Hyku specific +# customizations. This controller needs significant refactoring and reconciliation +# with Hyrax prime. Note, we are inheriting differently than Hyrax does and +# there are other adjustments. +# +# +######################################################################################### +######################################################################################### + +# OVERRIDE: Hyrax v5.0.0 +# - add home_text content block to the index method - Adding themes +# - add inject_theme_views method for theming +# - add all_collections page for IR theme +# - add facet counts for resource types for IR theme + +# - add facets to home page - inheriting from CatalogController rather than ApplicationController +# - add search_action_url method from Blacklight 6.23.0 to make facet links to go to /catalog +# - add .sort_by to return collections in alphabetical order by title on the homepage +# - add @featured_collection_list to index method module Hyrax # Changed to inherit from CatalogController for home page facets class HomepageController < CatalogController # Adds Hydra behaviors into the application controller include Blacklight::SearchContext - include Blacklight::SearchHelper include Blacklight::AccessControls::Catalog + # OVERRIDE: account for Hyku themes around_action :inject_theme_views # The search builder for finding recent documents - # Override of Blacklight::RequestBuilders + # Override of Blacklight::RequestBuilders and default CatalogController behavior def search_builder_class Hyrax::HomepageSearchBuilder end @@ -31,21 +45,25 @@ def search_builder_class layout 'homepage' helper Hyrax::ContentBlockHelper - # override hyrax v2.9.0 added @home_text - Adding Themes + # rubocop:disable Metrics/MethodLength def index + # BEGIN copy Hyrax prime's Hyrax::HomepageController#index @presenter = presenter_class.new(current_ability, collections) @featured_researcher = ContentBlock.for(:researcher) @marketing_text = ContentBlock.for(:marketing) - @home_text = ContentBlock.for(:home_text) @featured_work_list = FeaturedWorkList.new - # OVERRIDE here to add featured collection list - @featured_collection_list = FeaturedCollectionList.new @announcement_text = ContentBlock.for(:announcement) recent + # END copy + + # BEGIN OVERRIDE + # What follows is Hyku specific overrides + @home_text = ContentBlock.for(:home_text) # hyrax v3.5.0 added @home_text - Adding Themes + @featured_collection_list = FeaturedCollectionList.new # OVERRIDE here to add featured collection list + ir_counts if home_page_theme == 'institutional_repository' - # override hyrax v2.9.0 added for facets on homepage - Adding Themes - (@response, @document_list) = search_results(params) + (@response, @document_list) = search_service.search_results respond_to do |format| format.html { store_preferred_view } @@ -61,6 +79,7 @@ def index document_export_formats(format) end end + # rubocop:enable Metrics/MethodLength def browserconfig; end @@ -75,57 +94,70 @@ def all_collections # Added from Blacklight 6.23.0 to change url for facets on home page protected - # Default route to the search action (used e.g. in global partials). Override this method - # in a controller or in your ApplicationController to introduce custom logic for choosing - # which action the search form should use - def search_action_url(options = {}) - # Rails 4.2 deprecated url helpers accepting string keys for 'controller' or 'action' - main_app.search_catalog_path(options) - end + # Default route to the search action (used e.g. in global partials). Override this method + # in a controller or in your ApplicationController to introduce custom logic for choosing + # which action the search form should use + def search_action_url(options = {}) + # Rails 4.2 deprecated url helpers accepting string keys for 'controller' or 'action' + main_app.search_catalog_path(options) + end private - # Return 6 collections - def collections(rows: 6) - builder = Hyrax::CollectionSearchBuilder.new(self) - .rows(rows) - response = repository.search(builder) - # adding .sort_by to return collections in alphabetical order by title on the homepage - response.documents.sort_by(&:title) - rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest - [] + # Return 6 collections, sorts by title + def collections(rows: 6) + Hyrax::CollectionsService.new(self).search_results do |builder| + builder.rows(rows) + builder.merge(sort: "title_ssi") end + rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest + [] + end - def recent - # grab any recent documents - (_, @recent_documents) = search_results(q: '', sort: sort_field, rows: 6) - rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest - @recent_documents = [] + # override to show 6 recent items + def recent(rows: 6) + (_, @recent_documents) = search_service.search_results do |builder| + builder.rows(rows) + builder.merge(sort: sort_field) end + rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest + @recent_documents = [] + end - # OVERRIDE: Hyrax v2.9.0 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 + def ir_counts + @ir_counts = search_service.facet_field_response('resource_type_sim', "f.resource_type_sim.facet.limit" => "-1") + end - def sort_field - "date_uploaded_dtsi desc" - end + # COPIED from Hyrax::HomepageController + def sort_field + "date_uploaded_dtsi desc" + end - # Add this method to prepend the theme views into the view_paths - def inject_theme_views - if home_page_theme && home_page_theme != 'default_home' - original_paths = view_paths - home_theme_view_path = Rails.root.join('app', 'views', "themes", home_page_theme.to_s) - prepend_view_path(home_theme_view_path) - yield - # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. - view_paths=(original_paths) - # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - else - yield - end + # Add this method to prepend the theme views into the view_paths + def inject_theme_views + if home_page_theme && home_page_theme != 'default_home' + original_paths = view_paths + home_theme_view_path = Rails.root.join('app', 'views', "themes", home_page_theme.to_s) + prepend_view_path(home_theme_view_path) + yield + # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + # Do NOT change this line. This is calling the Rails view_paths=(paths) method and not a variable assignment. + view_paths=(original_paths) + # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + else + yield end + end + + # add this method to vary blacklight config and user_params + def search_service(*_args) + Hyrax::SearchService.new( + config: ::CatalogController.new.blacklight_config, + user_params: params.except(:q, :page), + scope: self, + current_ability:, + search_builder_class: + ) + end end end diff --git a/app/controllers/hyrax/images_controller.rb b/app/controllers/hyrax/images_controller.rb index e377989647..ec45442aa8 100644 --- a/app/controllers/hyrax/images_controller.rb +++ b/app/controllers/hyrax/images_controller.rb @@ -7,6 +7,7 @@ module Hyrax class ImagesController < ApplicationController # Adds Hyrax behaviors to the controller. include Hyrax::WorksControllerBehavior + include Hyku::WorksControllerBehavior include Hyrax::BreadcrumbsForWorks self.curation_concern_type = ::Image diff --git a/app/controllers/hyrax/my/works_controller_decorator.rb b/app/controllers/hyrax/my/works_controller_decorator.rb deleted file mode 100644 index df68ce1ca0..0000000000 --- a/app/controllers/hyrax/my/works_controller_decorator.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -## -# OVERRIDE Hyrax 3.5.0; when Hyrax hits v4.0.0 we can remove this. -# @see https://github.com/samvera/hyrax/pull/5972 -module Hyrax - module My - module WorksControllerDecorator - def collections_service - cloned = clone - cloned.params = {} - Hyrax::CollectionsService.new(cloned) - end - end - end -end - -Hyrax::My::WorksController.prepend(Hyrax::My::WorksControllerDecorator) diff --git a/app/controllers/hyrax/pages_controller.rb b/app/controllers/hyrax/pages_controller.rb deleted file mode 100644 index cdfd8d0a7d..0000000000 --- a/app/controllers/hyrax/pages_controller.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE: Hyrax v3.4.0 -# - add inject_theme_views method for theming -# - add homepage presenter for access to feature flippers -# - add access to content blocks in the show method -# - adds @featured_collection_list to show method - -module Hyrax - # Shows the about and help page - class PagesController < ApplicationController - load_and_authorize_resource class: ContentBlock, except: :show - layout :pages_layout - - # OVERRIDE: Hyrax v3.4.0 Add for theming - # Adds Hydra behaviors into the application controller - include Blacklight::SearchContext - include Blacklight::SearchHelper - include Blacklight::AccessControls::Catalog - - # OVERRIDE: Adding inject theme views method for theming - around_action :inject_theme_views - - # OVERRIDE: Hyrax v3.4.0 Add for theming - # The search builder for finding recent documents - # Override of Blacklight::RequestBuilders - def search_builder_class - Hyrax::HomepageSearchBuilder - end - - # OVERRIDE: Hyrax v3.4.0 Add for theming - class_attribute :presenter_class - # OVERRIDE: Hyrax v3.4.0 Add for theming - self.presenter_class = Hyrax::HomepagePresenter - - helper Hyrax::ContentBlockHelper - - def show - @page = ContentBlock.for(params[:key]) - # OVERRIDE: Hyrax v3.4.0 Add for theming - @presenter = presenter_class.new(current_ability, collections) - @featured_researcher = ContentBlock.for(:researcher) - @marketing_text = ContentBlock.for(:marketing) - @home_text = ContentBlock.for(:home_text) - @featured_work_list = FeaturedWorkList.new - # OVERRIDE here to add featured collection list to show page - @featured_collection_list = FeaturedCollectionList.new - @announcement_text = ContentBlock.for(:announcement) - end - - def edit - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t(:'hyrax.admin.sidebar.configuration'), '#' - add_breadcrumb t(:'hyrax.admin.sidebar.pages'), hyrax.edit_pages_path - end - - def update - respond_to do |format| - if @page.update(value: update_value_from_params) - redirect_path = "#{hyrax.edit_pages_path}##{params[:content_block].keys.first}" - format.html { redirect_to redirect_path, notice: t(:'hyrax.pages.updated') } - else - format.html { render :edit } - end - end - end - - private - - def permitted_params - params.require(:content_block).permit(:about, - :agreement, - :help, - :terms) - end - - # When a request comes to the controller, it will be for one and - # only one of the content blocks. Params always looks like: - # {'about_page' => 'Here is an awesome about page!'} - # So reach into permitted params and pull out the first value. - def update_value_from_params - permitted_params.values.first - end - - def pages_layout - action_name == 'show' ? 'homepage' : 'hyrax/dashboard' - end - - # OVERRIDE: return collections for theming - def collections(rows: 6) - builder = Hyrax::CollectionSearchBuilder.new(self) - .rows(rows) - response = repository.search(builder) - response.documents - rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest - [] - 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' - original_paths = view_paths - home_theme_view_path = Rails.root.join('app', 'views', "themes", home_page_theme.to_s) - prepend_view_path(home_theme_view_path) - yield - # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - # Do NOT change this method. This is an override of the view_paths= method and not a variable assignment. - view_paths=(original_paths) - # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses - else - yield - end - end - end -end diff --git a/app/controllers/hyrax/pages_controller_decorator.rb b/app/controllers/hyrax/pages_controller_decorator.rb new file mode 100644 index 0000000000..3f470a4dba --- /dev/null +++ b/app/controllers/hyrax/pages_controller_decorator.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# OVERRIDE: Hyrax v5.0.0rc2 +# - add inject_theme_views method for theming +# - add homepage presenter for access to feature flippers +# - add access to content blocks in the show method +# - adds @featured_collection_list to show method + +module Hyrax + # Shows the about and help page + module PagesControllerDecorator + extend ActiveSupport::Concern + + # OVERRIDE: Add for theming + # Adds Hydra behaviors into the application controller + include Blacklight::SearchContext + include Blacklight::AccessControls::Catalog + + prepended do + # OVERRIDE: Adding inject theme views method for theming + around_action :inject_theme_views + + # OVERRIDE: Hyrax v5.0.0rc2 Add for theming + class_attribute :presenter_class + self.presenter_class = Hyrax::HomepagePresenter + end + + # OVERRIDE: Add for theming + # The search builder for finding recent documents + # Override of Blacklight::RequestBuilders + def search_builder_class + Hyrax::HomepageSearchBuilder + end + + def show + super + + # OVERRIDE: Additional for theming + @presenter = presenter_class.new(current_ability, collections) + @featured_researcher = ContentBlock.for(:researcher) + @marketing_text = ContentBlock.for(:marketing) + @home_text = ContentBlock.for(:home_text) + @featured_work_list = FeaturedWorkList.new + @featured_collection_list = FeaturedCollectionList.new + @announcement_text = ContentBlock.for(:announcement) + end + + private + + # OVERRIDE: return collections for theming + # Return 6 collections, sorts by title + def collections(rows: 6) + Hyrax::CollectionsService.new(self).search_results do |builder| + builder.rows(rows) + builder.merge(sort: "title_ssi") + end + rescue Blacklight::Exceptions::ECONNREFUSED, Blacklight::Exceptions::InvalidRequest + [] + 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' + original_paths = view_paths + Hyku::Application.theme_view_path_roots.each do |root| + home_theme_view_path = File.join(root, 'app', 'views', "themes", home_page_theme.to_s) + prepend_view_path(home_theme_view_path) + end + yield + # rubocop:disable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + # Do NOT change this method. This is an override of the view_paths= method and not a variable assignment. + view_paths=(original_paths) + # rubocop:enable Lint/UselessAssignment, Layout/SpaceAroundOperators, Style/RedundantParentheses + else + yield + end + end + end +end + +Hyrax::PagesController.prepend(Hyrax::PagesControllerDecorator) diff --git a/app/controllers/identity_providers_controller.rb b/app/controllers/identity_providers_controller.rb index 28774a0bc2..7656e9c6eb 100644 --- a/app/controllers/identity_providers_controller.rb +++ b/app/controllers/identity_providers_controller.rb @@ -7,6 +7,7 @@ class IdentityProvidersController < ApplicationController before_action :set_identity_provider, only: %i[edit update destroy] def index + add_breadcrumbs @identity_providers = IdentityProvider.all end @@ -72,27 +73,27 @@ def add_breadcrumbs private - # Use callbacks to share common setup or constraints between actions. - def set_identity_provider - @identity_provider = IdentityProvider.find(params[:id]) - end + # Use callbacks to share common setup or constraints between actions. + def set_identity_provider + @identity_provider = IdentityProvider.find(params[:id]) + end - def ensure_admin! - authorize! :read, :admin_dashboard - end + def ensure_admin! + authorize! :read, :admin_dashboard + end - # Only allow a list of trusted parameters through. - def identity_provider_params - return @identity_provider_params if @identity_provider_params - @identity_provider_params = params.require(:identity_provider).permit( - :name, - :provider, - :options, - :logo_image, - :logo_image_text - ) - @identity_provider_params['options'].presence && - @identity_provider_params['options'] = JSON.parse(@identity_provider_params['options']) - @identity_provider_params - end + # Only allow a list of trusted parameters through. + def identity_provider_params + return @identity_provider_params if @identity_provider_params + @identity_provider_params = params.require(:identity_provider).permit( + :name, + :provider, + :options, + :logo_image, + :logo_image_text + ) + @identity_provider_params['options'].presence && + @identity_provider_params['options'] = JSON.parse(@identity_provider_params['options']) + @identity_provider_params + end end diff --git a/app/controllers/labels_controller.rb b/app/controllers/labels_controller.rb index 7252920bfd..df13a1fbaf 100644 --- a/app/controllers/labels_controller.rb +++ b/app/controllers/labels_controller.rb @@ -23,10 +23,10 @@ def update private - # Never trust parameters from the scary internet, only allow the permitted parameters through. - def site_params - params.require(:site).permit(:application_name, - :institution_name, - :institution_name_full) - end + # Never trust parameters from the scary internet, only allow the permitted parameters through. + def site_params + params.require(:site).permit(:application_name, + :institution_name, + :institution_name_full) + end end diff --git a/app/controllers/proprietor/accounts_controller.rb b/app/controllers/proprietor/accounts_controller.rb index d21986cf13..9f3dd8f868 100644 --- a/app/controllers/proprietor/accounts_controller.rb +++ b/app/controllers/proprietor/accounts_controller.rb @@ -87,41 +87,41 @@ def destroy private - def ensure_admin! - authorize! :read, :admin_dashboard - end + def ensure_admin! + authorize! :read, :admin_dashboard + end - # Never trust parameters from the scary internet, only allow the permitted parameters through. - def edit_account_params - params.require(:account).permit(:name, - :cname, - :title, - :is_public, - :search_only, - *@account.live_settings.keys, - admin_emails: [], - full_account_cross_searches_attributes: [:id, - :_destroy, - :full_account_id, - full_account_attributes: [:id]], - solr_endpoint_attributes: %i[id url], - fcrepo_endpoint_attributes: %i[id url base_path], - data_cite_endpoint_attributes: %i[mode prefix username password]) - end + # Never trust parameters from the scary internet, only allow the permitted parameters through. + def edit_account_params + params.require(:account).permit(:name, + :cname, + :title, + :is_public, + :search_only, + *@account.live_settings.keys, + admin_emails: [], + full_account_cross_searches_attributes: [:id, + :_destroy, + :full_account_id, + full_account_attributes: [:id]], + solr_endpoint_attributes: %i[id url], + fcrepo_endpoint_attributes: %i[id url base_path], + data_cite_endpoint_attributes: %i[mode prefix username password]) + end - def account_params - params.require(:account).permit( - :name, - :search_only, - admin_emails: [], - full_account_cross_searches_attributes: [:id, :_destroy, :full_account_id, full_account_attributes: [:id]] - ) - end + def account_params + params.require(:account).permit( + :name, + :search_only, + admin_emails: [], + full_account_cross_searches_attributes: [:id, :_destroy, :full_account_id, full_account_attributes: [:id]] + ) + end - def deleted_or_new(hash) - hash.detect do |_k, v| - ActiveModel::Type::Boolean.new.cast(v["_destroy"]) == true || v["id"].blank? - end + def deleted_or_new(hash) + hash.detect do |_k, v| + ActiveModel::Type::Boolean.new.cast(v["_destroy"]) == true || v["id"].blank? end + end end end diff --git a/app/controllers/proprietor/users_controller.rb b/app/controllers/proprietor/users_controller.rb index 1aa371c383..a3ba263b64 100644 --- a/app/controllers/proprietor/users_controller.rb +++ b/app/controllers/proprietor/users_controller.rb @@ -83,44 +83,46 @@ def destroy private - def ensure_admin! - authorize! :read, :admin_dashboard - end + def ensure_admin! + authorize! :read, :admin_dashboard + end - def user_params - # remove blank passwords - params[:user].delete(:password) if params[:user] && params[:user][:password].blank? + # rubocop:disable Metrics/MethodLength + def user_params + # remove blank passwords + params[:user].delete(:password) if params[:user] && params[:user][:password].blank? - params.require(:user).permit(:email, - :password, - :is_superadmin, - :facebook_handle, - :twitter_handle, - :googleplus_handle, - :display_name, - :address, - :department, - :title, - :office, - :chat_id, - :website, - :affiliation, - :telephone, - :avatar, - :group_list, - :linkedin_handle, - :orcid, - :arkivo_token, - :arkivo_subscription, - :zotero_token, - :zotero_userid, - :preferred_locale, - role_ids: []) - end + params.require(:user).permit(:email, + :password, + :superadmin, + :facebook_handle, + :twitter_handle, + :googleplus_handle, + :display_name, + :address, + :department, + :title, + :office, + :chat_id, + :website, + :affiliation, + :telephone, + :avatar, + :group_list, + :linkedin_handle, + :orcid, + :arkivo_token, + :arkivo_subscription, + :zotero_token, + :zotero_userid, + :preferred_locale, + role_ids: []) + end + # rubocop:enable Metrics/MethodLength - def find_user - @user ||= ::User.from_url_component(params[:id]) - raise ActiveRecord::RecordNotFound unless @user - end + def find_user + @user ||= ::User.from_url_component(params[:id]) + raise ActiveRecord::RecordNotFound unless @user + end end end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb deleted file mode 100644 index f3c2a64cdf..0000000000 --- a/app/controllers/roles_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -## -# CRUD actions for assigning exhibit roles to -# existing users -class RolesController < ApplicationController - load_and_authorize_resource :user, parent: false - layout 'hyrax/dashboard' - - before_action do - authorize! :manage, Role - end - - def index - @users = User.all - add_breadcrumb t(:'hyrax.controls.home'), root_path - add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path - add_breadcrumb t(:'hyrax.admin.sidebar.roles_and_permissions'), site_roles_path - end - - def update - if @user.update(user_params) - redirect_to site_roles_path, notice: notice - else - render action: 'index' - end - end - - protected - - def user_params - params.require(:user).permit(site_roles: []) - end -end diff --git a/app/controllers/saved_searches_controller.rb b/app/controllers/saved_searches_controller.rb new file mode 100644 index 0000000000..f2618be722 --- /dev/null +++ b/app/controllers/saved_searches_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SavedSearchesController < ApplicationController + include Blacklight::SavedSearches + + helper BlacklightAdvancedSearch::RenderConstraintsOverride +end diff --git a/app/controllers/search_history_controller.rb b/app/controllers/search_history_controller.rb new file mode 100644 index 0000000000..eb6c0983a2 --- /dev/null +++ b/app/controllers/search_history_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SearchHistoryController < ApplicationController + include Blacklight::SearchHistory + helper BlacklightAdvancedSearch::RenderConstraintsOverride + helper RangeLimitHelper +end diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 166f6390f8..5dc8efe6be 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -19,36 +19,36 @@ def update private - def set_site - @site ||= Site.instance - end + def set_site + @site ||= Site.instance + end - def update_params - params.permit(:remove_banner_image, - :remove_favicon, - :remove_logo_image, - :remove_directory_image, - :remove_default_collection_image, - :remove_default_work_image) - end + def update_params + params.permit(:remove_banner_image, + :remove_favicon, + :remove_logo_image, + :remove_directory_image, + :remove_default_collection_image, + :remove_default_work_image) + end - def site_theme_params - params.require(:site).permit(:home_theme, :search_theme, :show_theme) - end + def site_theme_params + params.require(:site).permit(:home_theme, :search_theme, :show_theme) + end - REMOVE_TEXT_MAPS = { - "remove_logo_image" => "logo_image_text", - "remove_banner_image" => "banner_image_text", - "remove_directory_image" => "directory_image_text", - "remove_default_collection_image" => "default_collection_image_text", - "remove_default_work_image" => "default_work_image_text" - }.freeze - - def remove_appearance_text(update_params) - image_text_keys = update_params.keys - image_text_keys.each do |image_text_key| - block = ContentBlock.find_by(name: REMOVE_TEXT_MAPS[image_text_key]) - block.delete if block&.value.present? - end + REMOVE_TEXT_MAPS = { + "remove_logo_image" => "logo_image_text", + "remove_banner_image" => "banner_image_text", + "remove_directory_image" => "directory_image_text", + "remove_default_collection_image" => "default_collection_image_text", + "remove_default_work_image" => "default_work_image_text" + }.freeze + + def remove_appearance_text(update_params) + image_text_keys = update_params.keys + image_text_keys.each do |image_text_key| + block = ContentBlock.find_by(name: REMOVE_TEXT_MAPS[image_text_key]) + block.delete if block&.value.present? end + end end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index f5cff3c0f5..0509c6e408 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -30,7 +30,7 @@ def callback alias saml callback def passthru - render status: 404, plain: 'Not found. Authentication passthru.' + render status: :not_found, plain: 'Not found. Authentication passthru.' end # def failure diff --git a/app/forms/hyrax/forms/admin/appearance.rb b/app/forms/hyrax/forms/admin/appearance_decorator.rb similarity index 55% rename from app/forms/hyrax/forms/admin/appearance.rb rename to app/forms/hyrax/forms/admin/appearance_decorator.rb index 7f20affaeb..96ffb80193 100644 --- a/app/forms/hyrax/forms/admin/appearance.rb +++ b/app/forms/hyrax/forms/admin/appearance_decorator.rb @@ -1,83 +1,143 @@ # frozen_string_literal: true -# OVERRIDE Hyrax 3.4.0 ot add custom theming +# OVERRIDE Hyrax v5.0.0rc2 to add custom theming -# rubocop:disable Metrics/ClassLength +# rubocop:disable Metrics/ModuleLength module Hyrax module Forms module Admin # An object to model and persist the form data for the appearance # customization menu - class Appearance - extend ActiveModel::Naming - delegate :banner_image, :banner_image?, to: :site - delegate :logo_image, :logo_image?, to: :site - delegate :favicon, :favicon?, to: :site - delegate :directory_image, :directory_image?, to: :site - delegate :default_collection_image, :default_collection_image?, to: :site - delegate :default_work_image, :default_work_image?, to: :site - - DEFAULT_FONTS = { - 'body_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;', - 'headline_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;' - }.freeze - - DEFAULT_COLORS = { - 'header_and_footer_background_color' => '#3c3c3c', - 'header_and_footer_text_color' => '#dcdcdc', - 'navbar_background_color' => '#000000', - 'navbar_link_background_hover_color' => '#ffffff', - 'navbar_link_text_color' => '#eeeeee', - 'navbar_link_text_hover_color' => '#eeeeee', - 'link_color' => '#2e74b2', - 'link_hover_color' => '#215480', - 'footer_link_color' => '#ffebcd', - 'footer_link_hover_color' => '#ffffff', - 'primary_button_hover_color' => '#286090', - 'default_button_background_color' => '#ffffff', - 'default_button_border_color' => '#cccccc', - 'default_button_text_color' => '#333333', - # 'active_tabs_background_color' => '#337ab7', - 'facet_panel_background_color' => '#f5f5f5', - 'facet_panel_text_color' => '#333333' - }.freeze - - DEFAULT_VALUES = DEFAULT_FONTS.merge(DEFAULT_COLORS).freeze - - # @param [Hash] attributes the list of parameters from the form - def initialize(attributes = {}) - @attributes = attributes - end - - attr_reader :attributes - private :attributes - - # This allows this object to route to the correct path - def self.model_name - ActiveModel::Name.new(self, Hyrax, "Hyrax::Admin::Appearance") - end - - # Override this method if your form takes more than just the customization_params - def self.permitted_params - customization_params + image_params - end - - def self.image_params - %i[favicon banner_image logo_image directory_image default_collection_image default_work_image] - end + module AppearanceDecorator + extend ActiveSupport::Concern + + # rubocop:disable Metrics/BlockLength + prepended do + delegate :banner_image, :banner_image?, to: :site + delegate :logo_image, :logo_image?, to: :site + delegate :favicon, :favicon?, to: :site + delegate :directory_image, :directory_image?, to: :site + delegate :default_collection_image, :default_collection_image?, to: :site + delegate :default_work_image, :default_work_image?, to: :site + + ## + # @!group Class Attributes + # + # @!attribute default_fonts + # @return [Hash] there should be at least the key "body_font" and + # "headline_font" + class_attribute :default_fonts, default: { + 'body_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;', + 'headline_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;' + } + + ## + # @!attribute default_colors + # @return [Hash] + class_attribute :default_colors, default: { + 'header_and_footer_background_color' => '#3c3c3c', + 'header_and_footer_text_color' => '#dcdcdc', + 'navbar_background_color' => '#000000', + 'navbar_link_background_hover_color' => '#ffffff', + 'navbar_link_text_color' => '#eeeeee', + 'navbar_link_text_hover_color' => '#eeeeee', + 'link_color' => '#2e74b2', + 'link_hover_color' => '#215480', + 'footer_link_color' => '#ffebcd', + 'footer_link_hover_color' => '#ffffff', + 'primary_button_hover_color' => '#286090', + 'default_button_background_color' => '#ffffff', + 'default_button_border_color' => '#cccccc', + 'default_button_text_color' => '#333333', + # 'active_tabs_background_color' => '#337ab7', + 'facet_panel_background_color' => '#f5f5f5', + 'facet_panel_text_color' => '#333333' + } + # @!endgroup Class Attributes + end + + class_methods do + # Override this method if your form takes more than just the customization_params + def permitted_params + customization_params + image_params + end - def site - @site ||= Site.instance - end + def image_params + %i[favicon banner_image logo_image directory_image default_collection_image default_work_image] + end - # Required to back a form - def to_key - [] + # @return [Array] a list of fields that are related to the banner + def banner_fields + %i[ + banner_image banner_label + ] + end + + def favicon_fields + [: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 + ] + 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 + + # A list of parameters that are related to customizations + # rubocop:disable Metrics/MethodLength + def customization_params + %i[ + 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 + default_button_background_color + default_button_border_color + default_button_text_color + active_tabs_background_color + facet_panel_background_color + facet_panel_text_color + navbar_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 + ] + end + # rubocop:enable Metrics/MethodLength end + # rubocop:enable Metrics/BlockLength - # Required to back a form (for route determination) - def persisted? - true + def site + @site ||= Site.instance end # The alt text for the logo image @@ -190,35 +250,22 @@ def primary_button_hover_background_color darken_color(primary_button_hover_color, 0.1) 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 background of active "primary" buttons def primary_button_active_background_color darken_color(primary_button_hover_color, 0.1) 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 background of focused "primary" buttons def primary_button_focus_background_color darken_color(primary_button_hover_color, 0.1) 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 custom css module def custom_css_block # we want to be able to read the css + # rubocop:disable Rails/OutputSafety block_for('custom_css_block', '/* custom stylesheet */').html_safe + # rubocop:enable Rails/OutputSafety end # DEFAULT BUTTON COLORS @@ -315,41 +362,6 @@ def default_image_attributes attributes.slice(*self.class.default_image_fields) end - # @return [Array] a list of fields that are related to the banner - def self.banner_fields - %i[ - banner_image banner_label - ] - end - - def self.favicon_fields - [:favicon] - end - - # @return [Array] a list of fields that are related to the logo - def self.logo_fields - %i[ - logo_image logo_label - ] - end - - # @return [Array] a list of fields that are related to the directory - def self.directory_fields - %i[ - directory_image directory_image_label - ] - end - - # @return [Array] a list of fields that are related to default works & collections - def self.default_image_fields - %i[ - default_collection_image - default_work_image - default_collection_label - default_work_label - ] - end - # Persist the form values def update! self.class.customization_params.each do |field| @@ -362,37 +374,6 @@ def update! .merge(default_image_attributes)) end - # A list of parameters that are related to customizations - def self.customization_params - %i[ - 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 - default_button_background_color - default_button_border_color - default_button_text_color - active_tabs_background_color - facet_panel_background_color - facet_panel_text_color - navbar_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 - ] - end - def font_import_body_url body = body_font.split('|').first.to_s.tr(" ", "+") # we need to be able to read the url to import fonts @@ -414,41 +395,49 @@ def font_headline_family private - def darken_color(hex_color, adjustment = 0.2) - amount = 1.0 - adjustment - hex_color = hex_color.delete('#') - rgb = hex_color.scan(/../).map { |color| (color.to_i(16) * amount).round } - rgb[0] = (rgb[0].to_i * amount).round - rgb[1] = (rgb[1].to_i * amount).round - rgb[2] = (rgb[2].to_i * amount).round - format("#%02x%02x%02x", *rgb) - end + def darken_color(hex_color, adjustment = 0.2) + amount = 1.0 - adjustment + hex_color = hex_color.delete('#') + rgb = hex_color.scan(/../).map { |color| (color.to_i(16) * amount).round } + rgb[0] = (rgb[0].to_i * amount).round + rgb[1] = (rgb[1].to_i * amount).round + rgb[2] = (rgb[2].to_i * amount).round + format("#%02x%02x%02x", *rgb) + end - def convert_to_rgba(hex_color, alpha = 0.5) - hex_color = hex_color.delete('#') - rgb = hex_color.scan(/../).map(&:hex) - "rgba(#{rgb[0]}, #{rgb[1]}, #{rgb[2]}, #{alpha})" - end + def convert_to_rgba(hex_color, alpha = 0.5) + hex_color = hex_color.delete('#') + rgb = hex_color.scan(/../).map(&:hex) + "rgba(#{rgb[0]}, #{rgb[1]}, #{rgb[2]}, #{alpha})" + end - def block_for(name, dynamic_default = nil) - ContentBlock.block_for(name: name, fallback_value: DEFAULT_VALUES[name] || dynamic_default) - end + def default_values + @default_values ||= default_fonts.merge(default_colors) + end - # Persist a key/value tuple as a ContentBlock - # @param [Symbol] name the identifier for the ContentBlock - # @param [String] value the value to set - def update_block(name, value) - ContentBlock.update_block(name: name, value: value) - end + def block_for(name, dynamic_default = nil) + ContentBlock.block_for(name:, fallback_value: default_values[name] || dynamic_default) + end - def format_font_names(font_style) - # the fonts come with `Font Name:font-weight` - this removes the weight - parts = font_style.split(':') - # Google fonts use `+` in place of spaces. This fixes it for CSS. - parts[0].tr('+', ' ').html_safe - end + # Persist a key/value tuple as a ContentBlock + # @param [Symbol] name the identifier for the ContentBlock + # @param [String] value the value to set + def update_block(name, value) + ContentBlock.update_block(name:, value:) + end + + def format_font_names(font_style) + # the fonts come with `Font Name:font-weight` - this removes the weight + parts = font_style.split(':') + # Google fonts use `+` in place of spaces. This fixes it for CSS. + # rubocop:disable Rails/OutputSafety + parts[0].tr('+', ' ').html_safe + # rubocop:enable Rails/OutputSafety + end end end end end -# rubocop:enable Metrics/ClassLength +# rubocop:enable Metrics/ModuleLength + +Hyrax::Forms::Admin::Appearance.prepend(Hyrax::Forms::Admin::AppearanceDecorator) diff --git a/app/forms/hyrax/forms/permission_template_form_decorator.rb b/app/forms/hyrax/forms/permission_template_form_decorator.rb index c09e514f23..7eda3fe0b3 100644 --- a/app/forms/hyrax/forms/permission_template_form_decorator.rb +++ b/app/forms/hyrax/forms/permission_template_form_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax 3.4.1 to fix Hyrax::Groups args +# OVERRIDE Hyrax v5.0.0rc2 to fix Hyrax::Groups args module Hyrax module Forms module PermissionTemplateFormDecorator diff --git a/app/forms/hyrax/forms/workflow_responsibility_form_decorator.rb b/app/forms/hyrax/forms/workflow_responsibility_form_decorator.rb new file mode 100644 index 0000000000..87119cdba9 --- /dev/null +++ b/app/forms/hyrax/forms/workflow_responsibility_form_decorator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 Expand to allow adding groups to workflow roles + +module Hyrax + module Forms + module WorkflowResponsibilityFormDecorator + ## + # @note We introduced this little crease in the code to allow for conditional switching; and + # thus avoid copying a very large controller + # (e.g. Hyrax::Admin::WorkflowRolesController) + # @see Hyrax::Forms::WorkflowResponsibilityGroupForm + module ClassMethods + ## + # Determine which form it is, user or group. By default, it will be a user + # (e.g. {Hyrax::Forms::WorkflowResponsibilityForm}); however when you provide a :group_id it + # will be a group form (e.g. {Hyrax::Forms::WorkflowResponsibilityGroupForm}. + def new(params = {}) + if params[:group_id].present? + Forms::WorkflowResponsibilityGroupForm.new(params) + else + super + end + end + end + end + end +end + +Hyrax::Forms::WorkflowResponsibilityForm.singleton_class + .send(:prepend, Hyrax::Forms::WorkflowResponsibilityFormDecorator::ClassMethods) diff --git a/app/forms/hyrax/forms/workflow_responsibility_group_form.rb b/app/forms/hyrax/forms/workflow_responsibility_group_form.rb index 71f4323fb1..7cb6d07158 100644 --- a/app/forms/hyrax/forms/workflow_responsibility_group_form.rb +++ b/app/forms/hyrax/forms/workflow_responsibility_group_form.rb @@ -10,6 +10,11 @@ def initialize(params = {}) model_instance.agent = group.to_sipity_agent end + ## + # This method is necessary for the HTML form fields to have the correct name (e.g. ``). + # + # @see ActiveModel::Naming def model_instance @model ||= Sipity::WorkflowResponsibility.new end diff --git a/app/helpers/admin_stats_helper.rb b/app/helpers/admin_stats_helper.rb index e1558e5022..bcd96b6779 100644 --- a/app/helpers/admin_stats_helper.rb +++ b/app/helpers/admin_stats_helper.rb @@ -5,7 +5,7 @@ def graph_tag(id, data, options) content_tag :div, class: 'graph-container', data: { graph_data: data.to_json, graph_options: options } do - content_tag :div, nil, class: 'graph', id: id + content_tag :div, nil, class: 'graph', id: end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e27eee4aeb..0e8cba8e78 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,9 +5,10 @@ module ApplicationHelper include Hyrax::OverrideHelperBehavior include GroupNavigationHelper include SharedSearchHelper + include HykuKnapsack::ApplicationHelper def hint_for(term:, record_class: nil) - hint = locale_for(type: 'hints', term: term, record_class: record_class) + hint = locale_for(type: 'hints', term:, record_class:) return hint unless missing_translation(hint) end @@ -24,7 +25,8 @@ def locale_for(type:, term:, record_class:) locale end - def missing_translation(value) - value.include?('translation missing') + def missing_translation(value, _options = {}) + return true if value.try(:false?) + false end end diff --git a/app/helpers/blacklight/blacklight_helper_behavior.rb b/app/helpers/blacklight/blacklight_helper_behavior.rb deleted file mode 100644 index b1c0778e43..0000000000 --- a/app/helpers/blacklight/blacklight_helper_behavior.rb +++ /dev/null @@ -1,423 +0,0 @@ -# frozen_string_literal: true - -# Override Blacklight v6.23.0 to add preferred view from search theme - Hyku Theming -# Methods added to this helper will be available to all templates in the hosting application -module Blacklight - module BlacklightHelperBehavior - include BlacklightUrlHelper - include BlacklightConfigurationHelper - include HashAsHiddenFieldsHelper - include RenderConstraintsHelper - include RenderPartialsHelper - include FacetsHelper - extend Deprecation - self.deprecation_horizon = 'Blacklight version 7.0.0' - - ## - # Get the name of this application, from either: - # - the Rails configuration - # - an i18n string (key: blacklight.application_name; preferred) - # - # @return [String] the application name - def application_name - if Rails.application.config.respond_to? :application_name - Deprecation.warn( - self, "BlacklightHelper#application_name will no longer delegate to config.application_name - in version 7.0. Set the i18n for blacklight.application_name instead" - ) - return Rails.application.config.application_name - end - - t('blacklight.application_name') - end - - ## - # Get the page's HTML title - # - # @return [String] - def render_page_title - (content_for(:page_title) if content_for?(:page_title)) || @page_title || application_name - end - - ## - # Create links from a documents dynamically - # provided export formats. - # - # Returns empty string if no links available. - # - # @param [SolrDocument] document - # @param [Hash] options - # @option options [Boolean] :unique ensures only one link is output for every - # content type, e.g. as required by atom - # @option options [Array] :exclude array of format shortnames to not include in the output - def render_link_rel_alternates(document = @document, options = {}) - return if document.nil? - presenter(document).link_rel_alternates(options) - end - - ## - # Render OpenSearch headers for this search - # @return [String] - def render_opensearch_response_metadata - render partial: 'catalog/opensearch_response_metadata' - end - - ## - # Render classes for the element - # @return [String] - def render_body_class - extra_body_classes.join " " - end - - ## - # List of classes to be applied to the element - # @see render_body_class - # @return [Array] - def extra_body_classes - @extra_body_classes ||= [ - 'blacklight-' + controller.controller_name, - 'blacklight-' + [controller.controller_name, controller.action_name].join('-') - ] - end - - ## - # Render the search navbar - # @return [String] - def render_search_bar - render partial: 'catalog/search_form' - end - - ## - # Determine whether to render a given field in the index view. - # - # @param [SolrDocument] document - # @param [Blacklight::Configuration::Field] field_config - # @return [Boolean] - def should_render_index_field?(document, field_config) - should_render_field?(field_config, document) && document_has_value?(document, field_config) - end - - ## - # Determine whether to render a given field in the show view - # - # @param [SolrDocument] document - # @param [Blacklight::Configuration::Field] field_config - # @return [Boolean] - def should_render_show_field?(document, field_config) - should_render_field?(field_config, document) && document_has_value?(document, field_config) - end - - ## - # Check if a document has (or, might have, in the case of accessor methods) a value for - # the given solr field - # @param [SolrDocument] document - # @param [Blacklight::Configuration::Field] field_config - # @return [Boolean] - def document_has_value?(document, field_config) - document.has?(field_config.field) || - (document.has_highlight_field? field_config.field if field_config.highlight) || - field_config.accessor - end - - ## - # Determine whether to display spellcheck suggestions - # - # @param [Blacklight::Solr::Response] response - # @return [Boolean] - def should_show_spellcheck_suggestions?(response) - response.total <= spell_check_max && - !response.spelling.nil? && - response.spelling.words.any? - end - - ## - # Render the index field label for a document - # - # @overload render_index_field_label(options) - # Use the default, document-agnostic configuration - # @param [Hash] opts - # @option opts [String] :field - # @overload render_index_field_label(document, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [Hash] opts - # @option opts [String] :field - def render_index_field_label(*args) - options = args.extract_options! - document = args.first - - field = options[:field] - html_escape t( - :"blacklight.search.index.#{document_index_view_type}.label", - default: :'blacklight.search.index.label', - label: index_field_label(document, field) - ) - end - - ## - # Render the index field label for a document - # - # @overload render_index_field_value(options) - # Use the default, document-agnostic configuration - # @param [Hash] opts - # @option opts [String] :field - # @option opts [String] :value - # @option opts [String] :document - # @overload render_index_field_value(document, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [Hash] opts - # @option opts [String] :field - # @option opts [String] :value - # @overload render_index_field_value(document, field, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [String] field - # @param [Hash] opts - # @option opts [String] :value - # @deprecated use IndexPresenter#field_value - def render_index_field_value(*args) - render_field_value(*args) - end - deprecation_deprecate render_index_field_value: 'replaced by IndexPresenter#field_value' - - # @deprecated use IndexPresenter#field_value - def render_field_value(*args) - options = args.extract_options! - document = args.shift || options[:document] - - field = args.shift || options[:field] - presenter(document).field_value field, options.except(:document, :field) - end - deprecation_deprecate render_field_value: 'replaced by IndexPresenter#field_value' - - ## - # Render the show field label for a document - # - # @overload render_document_show_field_label(options) - # Use the default, document-agnostic configuration - # @param [Hash] opts - # @option opts [String] :field - # @overload render_document_show_field_label(document, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [Hash] opts - # @option opts [String] :field - def render_document_show_field_label(*args) - options = args.extract_options! - document = args.first - - field = options[:field] - - t(:'blacklight.search.show.label', label: document_show_field_label(document, field)) - end - - ## - # Render the index field label for a document - # - # @overload render_document_show_field_value(options) - # Use the default, document-agnostic configuration - # @param [Hash] opts - # @option opts [String] :field - # @option opts [String] :value - # @option opts [String] :document - # @overload render_document_show_field_value(document, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [Hash] opts - # @option opts [String] :field - # @option opts [String] :value - # @overload render_document_show_field_value(document, field, options) - # Allow an extention point where information in the document - # may drive the value of the field - # @param [SolrDocument] doc - # @param [String] field - # @param [Hash] opts - # @option opts [String] :value - # @deprecated use ShowPresenter#field_value - def render_document_show_field_value(*args) - render_field_value(*args) - end - deprecation_deprecate render_document_show_field_value: 'replaced by ShowPresenter#field_value' - - ## - # Get the value of the document's "title" field, or a placeholder - # value (if empty) - # - # @param [SolrDocument] document - # @return [String] - def document_heading(document = nil) - document ||= @document - presenter(document).heading - end - - ## - # Get the document's "title" to display in the element. - # (by default, use the #document_heading) - # - # @see #document_heading - # @param [SolrDocument] document - # @return [String] - def document_show_html_title(document = nil) - document ||= @document - - presenter(document).html_title - end - - ## - # Render the document "heading" (title) in a content tag - # @overload render_document_heading(document, options) - # @param [SolrDocument] document - # @param [Hash] options - # @option options [Symbol] :tag - # @overload render_document_heading(options) - # @param [Hash] options - # @option options [Symbol] :tag - def render_document_heading(*args) - options = args.extract_options! - document = args.first - tag = options.fetch(:tag, :h4) - document ||= @document - - content_tag(tag, presenter(document).heading, itemprop: "name") - end - - ## - # Get the value for a document's field, and prepare to render it. - # - highlight_field - # - accessor - # - solr field - # - # Rendering: - # - helper_method - # - link_to_search - # @param [SolrDocument] document - # @param [String] _field name - # @param [Blacklight::Configuration::Field] field_config solr field configuration - # @param [Hash] options additional options to pass to the rendering helpers - def get_field_values(document, _field, field_config, options = {}) - presenter(document).field_values field_config, options - end - deprecation_deprecate :get_field_values - - ## - # Get the current "view type" (and ensure it is a valid type) - # - # @param [Hash] query_params the query parameters to check - # @return [Symbol] - def document_index_view_type(query_params = params) - view_param = query_params[:view] - # Override Blacklight v6.23.0 to add preferred view from search theme - Hyku Theming - view_param ||= search_results_theme.split('_').first - view_param ||= session[:preferred_view] - if view_param && document_index_views.keys.include?(view_param.to_sym) - view_param.to_sym - else - default_document_index_view_type - end - end - - ## - # Render a partial of an arbitrary format inside a - # template of a different format. (e.g. render an HTML - # partial from an XML template) - # code taken from: - # http://stackoverflow.com/questions/339130/how-do-i-render-a-partial-of-a-different-format-in-rails (zgchurch) - # - # @param [String] format suffix - # @yield - def with_format(format, _block) - old_formats = formats - self.formats = [format] - yield - self.formats = old_formats - nil - end - - ## - # Should we render a grouped response (because the response - # contains a grouped response instead of the normal response) - def render_grouped_response?(response = @response) - response.grouped? - end - - ## - # Returns a document presenter for the given document - # TODO: Move this to the controller. It can just pass a presenter or set of presenters. - def presenter(document) - case action_name - when 'show', 'citation' - show_presenter(document) - when 'index' - index_presenter(document) - else - Deprecation.warn( - Blacklight::BlacklightHelperBehavior, "Unable to determine presenter type for - #{action_name} on #{controller_name}, falling back on deprecated Blacklight::DocumentPresenter" - ) - presenter_class.new(document, self) - end - end - - def show_presenter(document) - show_presenter_class(document).new(document, self) - end - - def index_presenter(document) - index_presenter_class(document).new(document, self) - end - - def presenter_class - blacklight_config.document_presenter_class - end - deprecation_deprecate presenter_class: "replaced by show_presenter_class and index_presenter_class" - - ## - # Override this method if you want to use a different presenter class - def show_presenter_class(_document) - blacklight_config.show.document_presenter_class - end - - def index_presenter_class(_document) - blacklight_config.index.document_presenter_class - end - - ## - # Open Search discovery tag for HTML <head> links - def opensearch_description_tag(title, href) - tag :link, href: href, title: title, type: "application/opensearchdescription+xml", rel: "search" - end - - # OVERIDE: Blacklight::UrlHelperBehavior: - # override link_to_document to substitute method generate_work_url - # to fix URLs for gallery view groupings in shared tenants - # link_to_document(doc, 'VIEW', :counter => 3) - def link_to_document(doc, field_or_opts = nil, opts = { counter: nil }) - if field_or_opts.is_a? Hash - opts = field_or_opts - else - field = field_or_opts - end - - field ||= document_show_link_field(doc) - label = index_presenter(doc).label field, opts - # pull solr_document from input if we don't already have a solr_document - document = doc&.try(:solr_document) || doc - link_to label, generate_work_url(document, request), document_link_params(document, opts) - end - - # OVERIDE: Blacklight::UrlHelperBehavior: - # disable link jacking for tracking - # see https://playbook-staging.notch8.com/en/samvera/hyku/troubleshooting/multi-tenancy-and-full-urls - # If we need to preserve the link jacking for tracking, then we need to also amend - # method `session_tracking_params`so that instead of a path we have a URL - def document_link_params(_doc, opts) - opts.except(:label, :counter) - end - end -end diff --git a/app/helpers/google_tag_manager_helper.rb b/app/helpers/google_tag_manager_helper.rb index 6a9f6f70c3..87518a045d 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.strip_heredoc.html_safe + <<~HTML.html_safe <!-- Google Tag Manager --> <script> (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': @@ -23,7 +23,7 @@ def render_gtm_body(_host) # render 'layouts/google/gtm_body' return '' if current_account.gtm_id.blank? # rubocop:disable Rails/OutputSafety - <<-HTML.strip_heredoc.html_safe + <<~HTML.html_safe <!-- Google Tag Manager (noscript) --> <noscript><iframe src='https://www.googletagmanager.com/ns.html?id="#{current_account.gtm_id}"' diff --git a/app/helpers/group_navigation_helper.rb b/app/helpers/group_navigation_helper.rb index 2bc90c72e1..e29868d8aa 100644 --- a/app/helpers/group_navigation_helper.rb +++ b/app/helpers/group_navigation_helper.rb @@ -2,6 +2,6 @@ module GroupNavigationHelper def navigation_presenter - @navigation_presenter ||= Hyku::Admin::Group::NavigationPresenter.new(params: params) + @navigation_presenter ||= Hyku::Admin::Group::NavigationPresenter.new(params:) end end diff --git a/app/helpers/hyku/blacklight_helper_behavior.rb b/app/helpers/hyku/blacklight_helper_behavior.rb new file mode 100644 index 0000000000..91be4cd8a4 --- /dev/null +++ b/app/helpers/hyku/blacklight_helper_behavior.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Override Blacklight v7.35.0 to add preferred view from search theme - Hyku Theming +# Methods added to this helper will be available to all templates in the hosting application +module Hyku + module BlacklightHelperBehavior + ## + # Get the current "view type" (and ensure it is a valid type) + # + # @param [Hash] query_params the query parameters to check + # @return [Symbol] + def document_index_view_type(query_params = params) + view_param = query_params[:view] + view_param ||= search_results_theme.split('_').first + view_param ||= session[:preferred_view] + if view_param && document_index_views.key?(view_param.to_sym) + view_param.to_sym + else + default_document_index_view_type + end + end + + # OVERRIDE: Blacklight::UrlHelperBehavior: + # override link_to_document to substitute method generate_work_url + # to fix URLs for gallery view groupings in shared tenants + # link_to_document(doc, 'VIEW', :counter => 3) + # rubocop:disable Metrics/MethodLength + def link_to_document(doc, field_or_opts = nil, opts = { counter: nil }) + label = case field_or_opts + when NilClass + document_presenter(doc).heading + when Hash + opts = field_or_opts + document_presenter(doc).heading + when Proc, Symbol + Deprecation.warn(self, "passing a #{field_or_opts.class} to link_to_document is deprecated and will be removed in Blacklight 8") + Deprecation.silence(Blacklight::IndexPresenter) do + index_presenter(doc).label field_or_opts, opts + end + else # String + field_or_opts + end + + # pull solr_document from input if we don't already have a solr_document + document = doc&.try(:solr_document) || doc + Deprecation.silence(Blacklight::UrlHelperBehavior) do + link_to label, generate_work_url(document, request), document_link_params(document, opts) + end + end + # rubocop:enable Metrics/MethodLength + + # OVERRIDE: Blacklight::UrlHelperBehavior: + # disable link jacking for tracking + # see https://playbook-staging.notch8.com/en/samvera/hyku/troubleshooting/multi-tenancy-and-full-urls + # If we need to preserve the link jacking for tracking, then we need to also amend + # method `session_tracking_params`so that instead of a path we have a URL + # @private + def document_link_params(_doc, opts) + opts.except(:label, :counter) + end + private :document_link_params + end +end diff --git a/app/helpers/hyrax_helper.rb b/app/helpers/hyrax_helper.rb index 921498837f..fd1caaecbe 100644 --- a/app/helpers/hyrax_helper.rb +++ b/app/helpers/hyrax_helper.rb @@ -4,6 +4,7 @@ module HyraxHelper include ::BlacklightHelper include Hyrax::BlacklightOverride include Hyrax::HyraxHelperBehavior + include Hyku::BlacklightHelperBehavior def application_name Site.application_name || super @@ -26,7 +27,7 @@ def logo_image end def block_for(name:) - ContentBlock.block_for(name: name, fallback_value: false) + ContentBlock.block_for(name:, fallback_value: false) end def directory_image diff --git a/app/helpers/shared_search_helper.rb b/app/helpers/shared_search_helper.rb index ef419f6cb4..0bbae1b186 100644 --- a/app/helpers/shared_search_helper.rb +++ b/app/helpers/shared_search_helper.rb @@ -17,20 +17,20 @@ def generate_work_url(model, request) id = model["id"] end request_params = %i[protocol host port].map { |method| ["request_#{method}".to_sym, request.send(method)] }.to_h - get_url(id: id, request: request_params, account_cname: account_cname, has_model: has_model) + get_url(id:, request: request_params, account_cname:, has_model:) end private - def get_url(id:, request:, account_cname:, has_model:) - new_url = "#{request[:request_protocol]}#{account_cname || request[:request_host]}" - new_url += ":#{request[:request_port]}" if Rails.env.development? || Rails.env.test? - new_url += case has_model - when "collections" - "/#{has_model}/#{id}" - else - "/concern/#{has_model}/#{id}" - end - new_url - end + def get_url(id:, request:, account_cname:, has_model:) + new_url = "#{request[:request_protocol]}#{account_cname || request[:request_host]}" + new_url += ":#{request[:request_port]}" if Rails.env.development? || Rails.env.test? + new_url += case has_model + when "collections" + "/#{has_model}/#{id}" + else + "/concern/#{has_model}/#{id}" + end + new_url + end end diff --git a/app/jobs/cleanup_account_job.rb b/app/jobs/cleanup_account_job.rb index 504436f0b1..6dcc500e03 100644 --- a/app/jobs/cleanup_account_job.rb +++ b/app/jobs/cleanup_account_job.rb @@ -13,21 +13,21 @@ def perform(account) private - def cleanup_fedora(account) - account.fcrepo_endpoint.remove! - end + def cleanup_fedora(account) + account.fcrepo_endpoint.remove! + end - def cleanup_redis(account) - account.redis_endpoint.remove! - end + def cleanup_redis(account) + account.redis_endpoint.remove! + end - def cleanup_solr(account) - account.solr_endpoint.remove! - end + def cleanup_solr(account) + account.solr_endpoint.remove! + end - def cleanup_database(account) - Apartment::Tenant.drop(account.tenant) - rescue StandardError - nil # ignore if account.tenant missing - end + def cleanup_database(account) + Apartment::Tenant.drop(account.tenant) + rescue StandardError + nil # ignore if account.tenant missing + end end diff --git a/app/jobs/create_default_admin_set_job.rb b/app/jobs/create_default_admin_set_job.rb index 592b32f8e8..1f8ee9437e 100644 --- a/app/jobs/create_default_admin_set_job.rb +++ b/app/jobs/create_default_admin_set_job.rb @@ -2,6 +2,6 @@ class CreateDefaultAdminSetJob < ApplicationJob def perform(_account) - AdminSet.find_or_create_default_admin_set_id + Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id end end diff --git a/app/jobs/create_solr_collection_job.rb b/app/jobs/create_solr_collection_job.rb index 6d56486eb5..e818f8fae5 100644 --- a/app/jobs/create_solr_collection_job.rb +++ b/app/jobs/create_solr_collection_job.rb @@ -22,10 +22,10 @@ def without_account(name, tenant_list = '') return if collection_exists?(name) if tenant_list.present? client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATEALIAS', - name: name, collections: tenant_list) + name:, collections: tenant_list) else client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATE', - name: name) + name:) end end @@ -50,84 +50,84 @@ def to_h private - def transform_entry(k, v) - case v - when Hash - v.map do |k1, v1| - ["#{transform_key(k)}.#{transform_key(k1)}", v1] - end - else - [transform_key(k), v] + def transform_entry(k, v) + case v + when Hash + v.map do |k1, v1| + ["#{transform_key(k)}.#{transform_key(k1)}", v1] end + else + [transform_key(k), v] end + end - def transform_key(k) - k.to_s.camelize(:lower) - end + def transform_key(k) + k.to_s.camelize(:lower) + end end private - def client - Blacklight.default_index.connection - end + def client + Blacklight.default_index.connection + end - def collection_options - CollectionOptions.new(account ? account.solr_collection_options : Account.solr_collection_options).to_h - end + def collection_options + CollectionOptions.new(account ? account.solr_collection_options : Account.solr_collection_options).to_h + end - def collection_exists?(name) - response = client.get '/solr/admin/collections', params: { action: 'LIST' } - collections = response['collections'] + def collection_exists?(name) + response = client.get '/solr/admin/collections', params: { action: 'LIST' } + collections = response['collections'] - collections.include? name - end + collections.include? name + end - def collection_url(name) - uri = URI(solr_url) + name + def collection_url(name) + uri = URI(solr_url) + name - uri.to_s - end + uri.to_s + end - def solr_url - @solr_url ||= ENV['SOLR_URL'] || solr_url_parts - @solr_url = @solr_url.ends_with?('/') ? @solr_url : "#{@solr_url}/" - end + def solr_url + @solr_url ||= ENV['SOLR_URL'] || solr_url_parts + @solr_url = @solr_url.ends_with?('/') ? @solr_url : "#{@solr_url}/" + end - def solr_url_parts - "http://#{ENV.fetch('SOLR_ADMIN_USER', 'admin')}:#{ENV.fetch('SOLR_ADMIN_PASSWORD', 'admin')}" \ - "@#{ENV.fetch('SOLR_HOST', 'solr')}:#{ENV.fetch('SOLR_PORT', '8983')}/solr/" - end + def solr_url_parts + "http://#{ENV.fetch('SOLR_ADMIN_USER', 'admin')}:#{ENV.fetch('SOLR_ADMIN_PASSWORD', 'admin')}" \ + "@#{ENV.fetch('SOLR_HOST', 'solr')}:#{ENV.fetch('SOLR_PORT', '8983')}/solr/" + end - def add_solr_endpoint_to_account(account, name) - account.create_solr_endpoint(url: collection_url(name), collection: name) - end + def add_solr_endpoint_to_account(account, name) + account.create_solr_endpoint(url: collection_url(name), collection: name) + end - def perform_for_normal_tenant(account, name) - unless collection_exists? name - client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATE', - name: name) - end - add_solr_endpoint_to_account(account, name) + def perform_for_normal_tenant(account, name) + unless collection_exists? name + client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATE', + name:) end + add_solr_endpoint_to_account(account, name) + end - def perform_for_cross_search_tenant(account, name) - return if account.full_accounts.blank? - if account.saved_changes&.[]('created_at').present? || account.solr_endpoint.is_a?(NilSolrEndpoint) - create_shared_search_collection(account.full_accounts.map(&:tenant).uniq, name) - account.create_solr_endpoint(url: collection_url(name), collection: name) - account.save - else - solr_options = account.solr_endpoint.connection_options.dup - RemoveSolrCollectionJob.perform_now(name, solr_options, 'cross_search_tenant') - create_shared_search_collection(account.full_accounts.map(&:tenant).uniq, name) - account.solr_endpoint.update(url: collection_url(name), collection: name) - end + def perform_for_cross_search_tenant(account, name) + return if account.full_accounts.blank? + if account.saved_changes&.[]('created_at').present? || account.solr_endpoint.is_a?(NilSolrEndpoint) + create_shared_search_collection(account.full_accounts.map(&:tenant).uniq, name) + account.create_solr_endpoint(url: collection_url(name), collection: name) + account.save + else + solr_options = account.solr_endpoint.connection_options.dup + RemoveSolrCollectionJob.perform_now(name, solr_options, 'cross_search_tenant') + create_shared_search_collection(account.full_accounts.map(&:tenant).uniq, name) + account.solr_endpoint.update(url: collection_url(name), collection: name) end + end - def create_shared_search_collection(tenant_list, name) - return true if collection_exists?(name) - client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATEALIAS', - name: name, collections: tenant_list) - end + def create_shared_search_collection(tenant_list, name) + return true if collection_exists?(name) + client.get '/solr/admin/collections', params: collection_options.merge(action: 'CREATEALIAS', + name:, collections: tenant_list) + end end diff --git a/app/jobs/delete_old_guests_job.rb b/app/jobs/delete_old_guests_job.rb index 5ca5e0a407..51eafba830 100644 --- a/app/jobs/delete_old_guests_job.rb +++ b/app/jobs/delete_old_guests_job.rb @@ -7,12 +7,12 @@ class DeleteOldGuestsJob < ApplicationJob end def perform - User.unscope(:where).where("guest = ? and updated_at < ?", true, Time.current - 7.days).destroy_all + User.unscope(:where).where("guest = ? and updated_at < ?", true, 7.days.ago).destroy_all end private - def reenqueue - DeleteOldGuestsJob.set(wait_until: Date.tomorrow.midnight).perform_later - end + def reenqueue + DeleteOldGuestsJob.set(wait_until: Date.tomorrow.midnight).perform_later + end end diff --git a/app/jobs/embargo_auto_expiry_job.rb b/app/jobs/embargo_auto_expiry_job.rb index 1b7106eb4a..7fe83c7d1c 100644 --- a/app/jobs/embargo_auto_expiry_job.rb +++ b/app/jobs/embargo_auto_expiry_job.rb @@ -15,7 +15,7 @@ def perform(account) private - def reenqueue(account) - EmbargoAutoExpiryJob.set(wait_until: Date.tomorrow.midnight).perform_later(account) - end + def reenqueue(account) + EmbargoAutoExpiryJob.set(wait_until: Date.tomorrow.midnight).perform_later(account) + end end diff --git a/app/jobs/import_work_from_purl_job.rb b/app/jobs/import_work_from_purl_job.rb deleted file mode 100644 index c7dc351aa9..0000000000 --- a/app/jobs/import_work_from_purl_job.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'stanford' -# Import works from Purl/stacks services at Stanford -# -# Example usage: -# log = Hyrax::Operation.create!(user: current_user, -# operation_type: "Import Purl Metadata") -# ImportWorkFromPurlJob.perform_later(current_user, -# 'abcd1234xxxx', -# log) -class ImportWorkFromPurlJob < ApplicationJob - queue_as :ingest - - before_enqueue do |job| - log = job.arguments.last - log.pending_job(self) - end - - # This copies metadata from the passed in attribute to all of the works that - # are members of the given upload set - # @param [User] user - # @param [String] druid - # @param [Hyrax::Operation] log - def perform(user, druid, log) - xml = Stanford::Importer::PurlRetriever.get(druid) - parser = Stanford::Importer::PurlParser.new(xml) - attributes = process_attributes(parser.attributes) - model = model_to_create(attributes) - - CreateWorkJob.perform_later(user, model, attributes, log) - rescue Faraday::ResourceNotFound - Rails.logger.error "Unable to fetch #{druid} from purl." - end - - private - - def process_attributes(attributes) - # We're pruning off :form_of_work, :record_origin, :created_attributes, :identifiers - attributes = attributes.slice(*attributes_to_keep) - # rename :location to :based_near - attributes[:based_near] = attributes.delete(:location) - - # rename :rights to :license - attributes[:license] = attributes.delete(:rights) - - attributes[:collection][:collection_type] ||= Hyrax::CollectionType.find_or_create_default_collection_type - - process_collection(attributes) - filenames = attributes.delete(:files) - attributes[:remote_files] = filenames.map do |name| - { url: "https://stacks.stanford.edu/file/druid:#{attributes[:id]}/#{name}", - file_name: name } - end - - attributes - end - - class_attribute :attributes_to_keep - self.attributes_to_keep = %i[title - description - subject - language - resource_type - location - rights - visibility - id - collection - files - collection_type] - - def process_collection(attributes) - # rename :collection to :member_of_collection_attributes - collection = attributes.delete(:collection) - - # Workaround for ActiveFedora #1186 - id = collection[:id] - begin - retries ||= 0 - Collection.create!(collection) unless Collection.exists?(id) - rescue Ldp::Conflict => e - ## Another process has likely beat us to the punch. Wait a bit and try again. - sleep(3) - retry if (retries += 1) < 3 - raise e - end - attributes[:member_of_collection_attributes] = [{ id: id }] - end - - # Override this method if you have a different rubric for choosing the model - # @param [Hash] attributes - # @return String the model to create - def model_to_create(_attributes) - GenericWork.model_name.name - end -end diff --git a/app/jobs/lease_auto_expiry_job.rb b/app/jobs/lease_auto_expiry_job.rb index 16f6bcf9cb..fe21273ede 100644 --- a/app/jobs/lease_auto_expiry_job.rb +++ b/app/jobs/lease_auto_expiry_job.rb @@ -15,7 +15,7 @@ def perform(account) private - def reenqueue(account) - LeaseAutoExpiryJob.set(wait_until: Date.tomorrow.midnight).perform_later(account) - end + def reenqueue(account) + LeaseAutoExpiryJob.set(wait_until: Date.tomorrow.midnight).perform_later(account) + end end diff --git a/app/jobs/remove_solr_collection_job.rb b/app/jobs/remove_solr_collection_job.rb index a93eebeb94..2efbf28430 100644 --- a/app/jobs/remove_solr_collection_job.rb +++ b/app/jobs/remove_solr_collection_job.rb @@ -15,8 +15,8 @@ def perform(collection, connection_options, tenant_type = 'normal') private - def solr_client(connection_options) - # We remove the adapter, otherwise RSolr 2 will try to use it as a Faraday middleware - RSolr.connect(connection_options.without('adapter')) - end + def solr_client(connection_options) + # We remove the adapter, otherwise RSolr 2 will try to use it as a Faraday middleware + RSolr.connect(connection_options.without('adapter')) + end end diff --git a/app/mailers/hyku_mailer.rb b/app/mailers/hyku_mailer.rb index 924a0834d1..9539011d7a 100644 --- a/app/mailers/hyku_mailer.rb +++ b/app/mailers/hyku_mailer.rb @@ -8,7 +8,7 @@ def default_url_options private - def host_for_tenant - Account.find_by(tenant: Apartment::Tenant.current)&.cname || Account.admin_host - end + def host_for_tenant + Account.find_by(tenant: Apartment::Tenant.current)&.cname || Account.admin_host + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index c433ad198b..80708e6066 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -25,7 +25,7 @@ class Ability self.ability_logic += %i[everyone_can_create_curation_concerns] end - # OVERRIDE METHOD from blacklight-access_controls v0.6.2 + # OVERRIDE METHOD from blacklight-access_controls v6.0.1 # # NOTE: DO NOT RENAME THIS METHOD - it is required for permissions to function properly. # diff --git a/app/models/account.rb b/app/models/account.rb index 449795ac36..9dcfc3e9b1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # Customer organization account +# rubocop:disable Metrics/ClassLength class Account < ApplicationRecord include AccountEndpoints include AccountSettings @@ -110,6 +111,8 @@ def switch_host!(cname) Hyrax::Engine.routes.default_url_options[:host] = cname end + DEFAULT_FILE_CACHE_STORE = ENV.fetch('HYKU_CACHE_ROOT', '/app/samvera/file_cache') + def setup_tenant_cache(is_enabled) Rails.application.config.action_controller.perform_caching = is_enabled ActionController::Base.perform_caching = is_enabled @@ -117,7 +120,7 @@ def setup_tenant_cache(is_enabled) if is_enabled Rails.application.config.cache_store = :redis_cache_store, { url: Redis.current.id } else - Rails.application.config.cache_store = :file_store, ENV.fetch('HYKU_CACHE_ROOT', '/app/samvera/file_cache') + Rails.application.config.cache_store = :file_store, DEFAULT_FILE_CACHE_STORE end # rubocop:enable Style/ConditionalAssignment Rails.cache = ActiveSupport::Cache.lookup_store(Rails.application.config.cache_store) @@ -144,3 +147,4 @@ def cache_api? cache_api end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/collection.rb b/app/models/collection.rb index 39bd584926..2b2298fd86 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,6 +8,7 @@ class Collection < ActiveFedora::Base self.indexer = CollectionIndexer after_update :remove_featured, if: proc { |collection| collection.private? } after_destroy :remove_featured + prepend OrderAlready.for(:creator) def remove_featured FeaturedCollection.where(collection_id: id).destroy_all diff --git a/app/models/concerns/account_cname.rb b/app/models/concerns/account_cname.rb index 0306a11100..8c13d56305 100644 --- a/app/models/concerns/account_cname.rb +++ b/app/models/concerns/account_cname.rb @@ -12,7 +12,7 @@ module AccountCname # @raise [ArgumentError] if piece contains a trailing dot def default_cname(piece) return unless piece - raise ArgumentError, "param '#{piece}' must not contain trailing dots" if piece =~ /\.\Z/ + raise ArgumentError, "param '#{piece}' must not contain trailing dots" if /\.\Z/.match?(piece) # rubocop:disable Style/FormatStringToken default_host = ENV.fetch('HYKU_DEFAULT_HOST', "%{tenant}.#{admin_host}") # rubocop:enable Style/FormatStringToken @@ -51,7 +51,7 @@ def cname=(value) private - def default_cname(piece = name) - self.class.default_cname(piece) - end + def default_cname(piece = name) + self.class.default_cname(piece) + end end diff --git a/app/models/concerns/account_settings.rb b/app/models/concerns/account_settings.rb index 5cd2458c05..85e98fd5d6 100644 --- a/app/models/concerns/account_settings.rb +++ b/app/models/concerns/account_settings.rb @@ -3,6 +3,7 @@ # All settings have a presedence order as follows # Per Tenant Setting > ENV['HYKU_SETTING_NAME'] > ENV['HYRAX_SETTING_NAME'] > default +# rubocop:disable Metrics/ModuleLength module AccountSettings extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength @@ -79,6 +80,7 @@ def setting(name, args) end end + # rubocop:disable Metrics/MethodLength def solr_collection_options { async: nil, @@ -99,6 +101,7 @@ def solr_collection_options snitch: nil } end + # rubocop:disable Metrics/MethodLength end # rubocop:enable Metrics/BlockLength @@ -112,80 +115,81 @@ def live_settings private - def set_type(value, to_type) - case to_type - when 'array' - value.is_a?(String) ? value.split(',') : Array.wrap(value) - when 'boolean' - ActiveModel::Type::Boolean.new.cast(value) - when 'hash' - value.is_a?(String) ? JSON.parse(value) : value - when 'string' - value.to_s - end + def set_type(value, to_type) + case to_type + when 'array' + value.is_a?(String) ? value.split(',') : Array.wrap(value) + when 'boolean' + ActiveModel::Type::Boolean.new.cast(value) + when 'hash' + value.is_a?(String) ? JSON.parse(value) : value + when 'string' + value.to_s end + end - def validate_email_format - return if settings['email_format'].blank? - settings['email_format'].each do |email| - errors.add(:email_format) unless email.match?(/@\S*\.\S*/) - end + def validate_email_format + return if settings['email_format'].blank? + settings['email_format'].each do |email| + errors.add(:email_format) unless email.match?(/@\S*\.\S*/) end + end - def validate_contact_emails - ['weekly_email_list', 'monthly_email_list', 'yearly_email_list'].each do |key| - next if settings[key].blank? - settings[key].each do |email| - errors.add(:"#{key}") unless email.match?(URI::MailTo::EMAIL_REGEXP) - end + def validate_contact_emails + ['weekly_email_list', 'monthly_email_list', 'yearly_email_list'].each do |key| + next if settings[key].blank? + settings[key].each do |email| + errors.add(:"#{key}") unless email.match?(URI::MailTo::EMAIL_REGEXP) end end + end - def initialize_settings - return true unless self.class.column_names.include?('settings') - set_smtp_settings - reload_library_config - end + def initialize_settings + return true unless self.class.column_names.include?('settings') + set_smtp_settings + reload_library_config + end - def set_smtp_settings - current_smtp_settings = settings&.[]("smtp_settings").presence || {} - self.smtp_settings = current_smtp_settings.with_indifferent_access.reverse_merge!( - PerTenantSmtpInterceptor.available_smtp_fields.each_with_object("").to_h - ) + def set_smtp_settings + current_smtp_settings = settings&.[]("smtp_settings").presence || {} + self.smtp_settings = current_smtp_settings.with_indifferent_access.reverse_merge!( + PerTenantSmtpInterceptor.available_smtp_fields.each_with_object("").to_h + ) + end + + # rubocop:disable Metrics/AbcSize + def reload_library_config + Hyrax.config do |config| + config.contact_email = contact_email + config.geonames_username = geonames_username + config.uploader[:maxFileSize] = file_size_limit.to_i end - def reload_library_config - Hyrax.config do |config| - config.contact_email = contact_email - config.analytics = google_analytics_id.present? - config.google_analytics_id = google_analytics_id if google_analytics_id.present? - config.geonames_username = geonames_username - config.uploader[:maxFileSize] = file_size_limit - end + Devise.mailer_sender = contact_email - Devise.mailer_sender = contact_email - - if s3_bucket.present? - CarrierWave.configure do |config| - config.storage = :aws - config.aws_bucket = s3_bucket - config.aws_acl = 'bucket-owner-full-control' - end - elsif !file_acl - CarrierWave.configure do |config| - config.permissions = nil - config.directory_permissions = nil - end - else - CarrierWave.configure do |config| - config.storage = :file - config.permissions = 420 - config.directory_permissions = 493 - end + if s3_bucket.present? + CarrierWave.configure do |config| + config.storage = :aws + config.aws_bucket = s3_bucket + config.aws_acl = 'bucket-owner-full-control' + end + elsif !file_acl + CarrierWave.configure do |config| + config.permissions = nil + config.directory_permissions = nil + end + else + CarrierWave.configure do |config| + config.storage = :file + config.permissions = 420 + config.directory_permissions = 493 end - - return unless ssl_configured - ActionMailer::Base.default_url_options ||= {} - ActionMailer::Base.default_url_options[:protocol] = 'https' end + + return unless ssl_configured + ActionMailer::Base.default_url_options ||= {} + ActionMailer::Base.default_url_options[:protocol] = 'https' + end + # rubocop:enable Metrics/AbcSize end +# rubocop:enable Metrics/ModuleLength diff --git a/app/models/concerns/account_switch.rb b/app/models/concerns/account_switch.rb index 7c5a02f006..613dbec600 100644 --- a/app/models/concerns/account_switch.rb +++ b/app/models/concerns/account_switch.rb @@ -6,11 +6,12 @@ module AccountSwitch DOMAIN_REGEXP = %r{^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,8}(:[0-9]{1,5})?(/.*)?$}ix class_methods do + # rubocop:disable Metrics/MethodLength def switch!(cname_or_name_or_account) account = if cname_or_name_or_account.is_a?(Account) cname_or_name_or_account # is it a domain name? - elsif cname_or_name_or_account =~ DOMAIN_REGEXP + elsif DOMAIN_REGEXP.match?(cname_or_name_or_account) Account.joins(:domain_names).find_by(domain_names: { is_active: true, cname: Account.canonical_cname(cname_or_name_or_account) @@ -26,6 +27,7 @@ def switch!(cname_or_name_or_account) Rails.logger.info "It looks like we're in single tenant mode. No tenant found for #{cname_or_name_or_account}" end end + # rubocop:enable Metrics/MethodLength end def switch!(cname_or_name_or_account) diff --git a/app/models/concerns/hyrax/ability/collection_ability.rb b/app/models/concerns/hyrax/ability/collection_ability.rb index 3af46e0c2a..612e1070aa 100644 --- a/app/models/concerns/hyrax/ability/collection_ability.rb +++ b/app/models/concerns/hyrax/ability/collection_ability.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Alter abilities for Groups with Roles feature +# OVERRIDE Hyrax v5.0.0rc2 Alter abilities for Groups with Roles feature module Hyrax module Ability + # rubocop:disable Metrics/ModuleLength module CollectionAbility # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/BlockLength # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity + # rubocop:disable Metrics/CyclomaticComplexity def collection_abilities models = [Hyrax::PcdmCollection, Hyrax.config.collection_class].uniq if admin? @@ -157,6 +160,9 @@ def collection_roles # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/BlockLength # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity end + # rubocop:enable Metrics/ModuleLength end end diff --git a/app/models/concerns/hyrax/ability/solr_document_ability.rb b/app/models/concerns/hyrax/ability/solr_document_ability.rb index 2196f28bd0..1248eefbc5 100644 --- a/app/models/concerns/hyrax/ability/solr_document_ability.rb +++ b/app/models/concerns/hyrax/ability/solr_document_ability.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Alter abilities for Groups with Roles feature +# OVERRIDE Hyrax v5.0.0rc2 Alter abilities for Groups with Roles feature module Hyrax module Ability module SolrDocumentAbility + # rubocop:disable Metrics/MethodLength def solr_document_abilities if admin? can [:manage], ::SolrDocument @@ -27,6 +28,7 @@ def solr_document_abilities end end end + # rubocop:enable Metrics/MethodLength end end end diff --git a/app/models/concerns/hyrax/ability/work_ability.rb b/app/models/concerns/hyrax/ability/work_ability.rb index b025c979c0..9d7daebc0f 100644 --- a/app/models/concerns/hyrax/ability/work_ability.rb +++ b/app/models/concerns/hyrax/ability/work_ability.rb @@ -32,7 +32,7 @@ def admin_set_with_deposit? return false if ids.empty? - Hyrax.custom_queries.find_ids_by_model(model: Hyrax::AdministrativeSet, ids: ids).any? + Hyrax.custom_queries.find_ids_by_model(model: Hyrax::AdministrativeSet, ids:).any? end end end diff --git a/app/models/content_block.rb b/app/models/content_block.rb index c9e72b26eb..b4f291f43f 100644 --- a/app/models/content_block.rb +++ b/app/models/content_block.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.0 to add home_text to registry -# and getter/setter methods - Adding themes +# OVERRIDE Hyrax v5.0.0rc to add home_text to registry and getter/setter methods - Adding themes +# NOTE: This class inherits from ApplicationRecord while Hyrax's version inherits from ActiveRecord::Base +# so we cannot use the decorator pattern to override the methods. +# rubocop:disable Metrics/ClassLength class ContentBlock < ApplicationRecord # The keys in this registry are "public" names for collaborator # objects, and the values are reserved names of ContentBlock @@ -16,7 +18,9 @@ class ContentBlock < ApplicationRecord help: :help_page, terms: :terms_page, agreement: :agreement_page, - home_text: :home_text + home_text: :home_text, + homepage_about_section_heading: :homepage_about_section_heading, + homepage_about_section_content: :homepage_about_section_content }.freeze # NOTE: method defined outside the metaclass wrapper below because @@ -41,7 +45,7 @@ def whitelisted?(key) # # @return [Object] either the named block's value or the fallback_value. def block_for(name:, fallback_value: false) - block = ContentBlock.find_by(name: name) + block = ContentBlock.find_by(name:) block&.value.presence || fallback_value end @@ -50,7 +54,7 @@ def block_for(name:, fallback_value: false) # @param name [#to_s] the named content block # @param value [Object] the value to update the given content block def update_block(name:, value:) - find_or_create_by(name: name.to_s).update!(value: value) + find_or_create_by(name: name.to_s).update!(value:) end def registered?(key) @@ -62,7 +66,7 @@ def marketing_text end def marketing_text=(value) - marketing_text.update(value: value) + marketing_text.update(value:) end def announcement_text @@ -70,7 +74,7 @@ def announcement_text end def announcement_text=(value) - announcement_text.update(value: value) + announcement_text.update(value:) end def featured_researcher @@ -78,7 +82,7 @@ def featured_researcher end def featured_researcher=(value) - featured_researcher.update(value: value) + featured_researcher.update(value:) end # OVERRIDE Hyrax v3.4.0 to add home_text getter/setter methods - Adding themes @@ -87,7 +91,23 @@ def home_text end def home_text=(value) - home_text.update(value: value) + home_text.update(value:) + end + + def homepage_about_section_heading + find_or_create_by(name: 'homepage_about_section_heading') + end + + def homepage_about_section_heading=(value) + homepage_about_section_heading.update(value:) + end + + def homepage_about_section_content + find_or_create_by(name: 'homepage_about_section_content') + end + + def homepage_about_section_content=(value) + homepage_about_section_content.update(value:) end def about_page @@ -95,7 +115,7 @@ def about_page end def about_page=(value) - about_page.update(value: value) + about_page.update(value:) end def agreement_page @@ -104,7 +124,7 @@ def agreement_page end def agreement_page=(value) - agreement_page.update(value: value) + agreement_page.update(value:) end def help_page @@ -112,7 +132,7 @@ def help_page end def help_page=(value) - help_page.update(value: value) + help_page.update(value:) end def terms_page @@ -121,7 +141,7 @@ def terms_page end def terms_page=(value) - terms_page.update(value: value) + terms_page.update(value:) end def default_agreement_text @@ -141,3 +161,4 @@ def default_terms_text end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/fcrepo_endpoint.rb b/app/models/fcrepo_endpoint.rb index 7b02ff281e..cd5a6e88b8 100644 --- a/app/models/fcrepo_endpoint.rb +++ b/app/models/fcrepo_endpoint.rb @@ -27,7 +27,7 @@ def ping def remove! switch! # Preceding slash must be removed from base_path when calling delete() - path = base_path.sub!(%r{^/}, '') + path = base_path.sub(%r{^/}, '') ActiveFedora.fedora.connection.delete(path) destroy end diff --git a/app/models/featured_collection_list.rb b/app/models/featured_collection_list.rb index 1de617815a..deb912cb1d 100644 --- a/app/models/featured_collection_list.rb +++ b/app/models/featured_collection_list.rb @@ -6,14 +6,14 @@ class FeaturedCollectionList # @param [ActionController::Parameters] a collection of nested perameters def featured_collections_attributes=(attributes_collection) attributes_collection = attributes_collection.to_h if attributes_collection.respond_to?(:permitted?) - # rubocop:disable Metrics/LineLength + # rubocop:disable Layout/LineLength attributes_collection = attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes } if attributes_collection.is_a? Hash attributes_collection.each do |attributes| raise "Missing id" if attributes['id'].blank? existing_record = FeaturedCollection.find(attributes['id']) existing_record.update(attributes.except('id')) end - # rubocop:enable Metrics/LineLength + # rubocop:enable Layout/LineLength end def featured_collections @@ -30,24 +30,24 @@ def featured_collections private - def add_solr_document_to_collections - collection_presenters.each do |presenter| - collection_with_id(presenter.id).presenter = presenter - end + def add_solr_document_to_collections + collection_presenters.each do |presenter| + collection_with_id(presenter.id).presenter = presenter end + end - def ids - @collections.pluck(:collection_id) - end + def ids + @collections.pluck(:collection_id) + end - def collection_presenters - ability = nil - Hyrax::PresenterFactory.build_for(ids: ids, - presenter_class: Hyku::WorkShowPresenter, - presenter_args: ability) - end + def collection_presenters + ability = nil + Hyrax::PresenterFactory.build_for(ids:, + presenter_class: Hyku::WorkShowPresenter, + presenter_args: ability) + end - def collection_with_id(id) - @collections.find { |c| c.collection_id == id } - end + def collection_with_id(id) + @collections.find { |c| c.collection_id == id } + end end diff --git a/app/models/generic_work.rb b/app/models/generic_work.rb index f69aae6abb..4c45fad1e4 100644 --- a/app/models/generic_work.rb +++ b/app/models/generic_work.rb @@ -12,4 +12,6 @@ class GenericWork < ActiveFedora::Base validates :title, presence: { message: 'Your work must have a title.' } self.indexer = GenericWorkIndexer + + prepend OrderAlready.for(:creator) end diff --git a/app/models/hydra/access_controls/embargo_decorator.rb b/app/models/hydra/access_controls/embargo_decorator.rb index 6d1bed4edc..9daf716e3c 100644 --- a/app/models/hydra/access_controls/embargo_decorator.rb +++ b/app/models/hydra/access_controls/embargo_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hydra-access-controls 12.0.1 +# OVERRIDE Hydra-access-controls v12.1.0 # Fix releasing embargos on the day they are expired - this solves a 1 second bug around how # midnights are calculated, which causes day of embargos to incorrectly set the permissions to private module Hydra diff --git a/app/models/hydra/access_controls/lease_decorator.rb b/app/models/hydra/access_controls/lease_decorator.rb index 971f7f58bf..37ab3b8563 100644 --- a/app/models/hydra/access_controls/lease_decorator.rb +++ b/app/models/hydra/access_controls/lease_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hydra-access-controls 12.0.1 +# OVERRIDE Hydra-access-controls 12.1.0 # Fix releasing leases on the day they are expired - this solves a 1 second bug around how # midnights are calculated, which causes day of leases to incorrectly set the permissions to private module Hydra diff --git a/app/models/hyrax/collection_type_participant_decorator.rb b/app/models/hyrax/collection_type_participant_decorator.rb index c64b667d2d..164f101a1f 100644 --- a/app/models/hyrax/collection_type_participant_decorator.rb +++ b/app/models/hyrax/collection_type_participant_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Expand functionality for Groups with Roles feature +# OVERRIDE Hyrax v5.0.0rc2 Expand functionality for Groups with Roles feature module Hyrax module CollectionTypeParticipantDecorator # OVERRIDE: #titleize agent_id for groups since we are displaying their humanized names in the dropdown diff --git a/app/models/hyrax/contact_form.rb b/app/models/hyrax/contact_form.rb deleted file mode 100644 index fc03c74eec..0000000000 --- a/app/models/hyrax/contact_form.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 to override the mail to -module Hyrax - class ContactForm - include ActiveModel::Model - attr_accessor :contact_method, :category, :name, :email, :subject, :message - validates :email, :category, :name, :subject, :message, presence: true - validates :email, format: /\A([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i, allow_blank: true - - # - can't use this without ActiveRecord::Base - # validates_inclusion_of :category, in: self.class.issue_types_for_locale - - # They should not have filled out the `contact_method' field. That's there to prevent spam. - def spam? - contact_method.present? - end - - # Declare the e-mail headers. It accepts anything the mail method - # in ActionMailer accepts. - ###### OVERRODE the to: field to add the Tenant's email, first - def contact_email - Site.account.contact_email_to - end - - def headers - ## OVERRIDE Hyrax 3.4.0 send the mail 'from' the submitter, which doesn't work on most smtp transports - { - subject: "#{Site.account.email_subject_prefix} #{email} #{subject}", - to: contact_email, - from: Site.account.contact_email, - reply_to: email - } - end - - def self.issue_types_for_locale - I18n.t('hyrax.contact_form.issue_types').values.select(&:present?) - end - end -end diff --git a/app/models/hyrax/contact_form_decorator.rb b/app/models/hyrax/contact_form_decorator.rb new file mode 100644 index 0000000000..27cbf3fb0e --- /dev/null +++ b/app/models/hyrax/contact_form_decorator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to override the mail to +module Hyrax + module ContactFormDecorator + # Declare the e-mail headers. It accepts anything the mail method + # in ActionMailer accepts. + ###### OVERRIDE the to: field to add the Tenant's email, first + def contact_email + Site.account.contact_email_to + end + + def headers + ## OVERRIDE Hyrax 3.4.0 send the mail 'from' the submitter, which doesn't work on most smtp transports + { + subject: "#{Site.account.email_subject_prefix} #{email} #{subject}", + to: contact_email, + from: Site.account.contact_email, + reply_to: email + } + end + end +end + +Hyrax::ContactForm.prepend(Hyrax::ContactFormDecorator) diff --git a/app/models/hyrax/group.rb b/app/models/hyrax/group.rb index bc33f2d732..e612a68ea3 100644 --- a/app/models/hyrax/group.rb +++ b/app/models/hyrax/group.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Expand functionality for Groups with Roles Feature +# OVERRIDE Hyrax v5.0.0rc2 Expand functionality for Groups with Roles Feature # @see https://github.com/samvera/hyku/wiki/Groups-with-Roles-Feature module Hyrax class Group < ApplicationRecord @@ -16,6 +16,26 @@ class Group < ApplicationRecord before_destroy :can_destroy? after_destroy :remove_all_members + ## + # What is going on here? In Hyrax proper, the Group model is a plain old Ruby object (PORO). In + # Hyku, the {Hyrax::Group} is based on ActiveRecord. + # + # The Hyrax version instantiates with a single string parameter. Importantly, we want to re-use + # the Hyrax::Workflow::PermissionQuery logic, without re-writing it. In particular we want to + # consider the Hyrax::Workflow::PermissionQuery#scope_processing_agents_for which casts the + # group to a Sipity::Agent + # + # @see https://github.com/samvera/hyrax/blob/main/app/models/hyrax/group.rb + # @see https://github.com/samvera/hyrax/blob/main/app/services/hyrax/workflow/permission_query.rb + def self.new(*args) + # This logic path is likely coming from Hyrax specific code; in which it expects a string. + if args.size == 1 && args.first.is_a?(String) + find_by(name: args.first) || super(name: args.first) + else + super + end + end + def self.name_prefix DEFAULT_NAME_PREFIX end @@ -43,7 +63,7 @@ def search_members(query, member_class: DEFAULT_MEMBER_CLASS) if query.present? && member_class == DEFAULT_MEMBER_CLASS members.where("email LIKE :q OR display_name LIKE :q", q: "%#{query}%") else - members(member_class: member_class) + members(member_class:) end end @@ -85,12 +105,12 @@ def default_group? def description_label label = description || I18n.t("hyku.admin.groups.description.#{name}") - return '' if label =~ /^translation missing:/ + return '' if /^translation missing:/.match?(label) label end - def has_site_role?(role_name) # rubocop:disable Naming/PredicateName + def site_role?(role_name) site_roles = roles.select { |role| role.resource_type == 'Site' } site_roles.map(&:name).include?(role_name.to_s) @@ -98,22 +118,22 @@ def has_site_role?(role_name) # rubocop:disable Naming/PredicateName private - def can_destroy? - return false if default_group? + def can_destroy? + return false if default_group? - true - end + true + end - def remove_all_members - members.map { |m| m.remove_role(MEMBERSHIP_ROLE, self) } - end + def remove_all_members + members.map { |m| m.remove_role(MEMBERSHIP_ROLE, self) } + end - def sipity_agent - Sipity::Agent.find_by(proxy_for_id: name, proxy_for_type: self.class.name) - end + def sipity_agent + Sipity::Agent.find_by(proxy_for_id: name, proxy_for_type: self.class.name) + end - def create_sipity_agent! - Sipity::Agent.create!(proxy_for_id: name, proxy_for_type: self.class.name) - end + def create_sipity_agent! + Sipity::Agent.create!(proxy_for_id: name, proxy_for_type: self.class.name) + end end end diff --git a/app/models/hyrax/permission_template_access_decorator.rb b/app/models/hyrax/permission_template_access_decorator.rb index 7ca6bb32bd..f6f6fa479f 100644 --- a/app/models/hyrax/permission_template_access_decorator.rb +++ b/app/models/hyrax/permission_template_access_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 #titleize agent_id for groups since we are displaying their humanized names in the dropdown +# OVERRIDE Hyrax v5.0.0rc2 #titleize agent_id for groups since we are displaying their humanized names in the dropdown module Hyrax module PermissionTemplateAccessDecorator def label diff --git a/app/models/image.rb b/app/models/image.rb index 51afe75997..e259048b3f 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -19,6 +19,8 @@ class Image < ActiveFedora::Base include ::Hyrax::BasicMetadata self.indexer = ImageIndexer + prepend OrderAlready.for(:creator) + # Change this to restrict which works can be added as a child. # self.valid_child_concerns = [] validates :title, presence: { message: 'Your work must have a title.' } diff --git a/app/models/nil_fcrepo_endpoint.rb b/app/models/nil_fcrepo_endpoint.rb index d726145176..d833c0f423 100644 --- a/app/models/nil_fcrepo_endpoint.rb +++ b/app/models/nil_fcrepo_endpoint.rb @@ -15,7 +15,7 @@ def base_path private - def options - { url: 'http://127.0.0.1:99999/nil_fcrepo_endpoint' } - end + def options + { url: 'http://127.0.0.1:99999/nil_fcrepo_endpoint' } + end end diff --git a/app/models/nil_solr_endpoint.rb b/app/models/nil_solr_endpoint.rb index 7a16634fc0..9dd11f166d 100644 --- a/app/models/nil_solr_endpoint.rb +++ b/app/models/nil_solr_endpoint.rb @@ -13,16 +13,16 @@ def url private - # Return an RSolr connection, that points at an invalid endpoint - # Note: We could have returned a NilRSolrConnection here, but Blacklight - # makes it's own RSolr connection, so we'd end up with an RSolr connection in - # blacklight anyway. - def connection - RSolr.connect(connection_options) - end + # Return an RSolr connection, that points at an invalid endpoint + # Note: We could have returned a NilRSolrConnection here, but Blacklight + # makes it's own RSolr connection, so we'd end up with an RSolr connection in + # blacklight anyway. + def connection + RSolr.connect(connection_options) + end - # Return options that will never return a valid connection. - def connection_options - { url: 'http://127.0.0.1:99999/nil_solr_endpoint' } - end + # Return options that will never return a valid connection. + def connection_options + { url: 'http://127.0.0.1:99999/nil_solr_endpoint' } + end end diff --git a/app/models/redis_endpoint.rb b/app/models/redis_endpoint.rb index b6899cf989..3cd68f7079 100644 --- a/app/models/redis_endpoint.rb +++ b/app/models/redis_endpoint.rb @@ -21,20 +21,14 @@ def ping # Remove all the keys in Redis in this namespace, then destroy the record def remove! switch! - # Redis::Namespace currently doesn't support flushall or flushdb. - # See https://github.com/resque/redis-namespace/issues/56 - # So, instead we select all keys in current namespace and delete - keys = redis_instance.keys '*' - return if keys.empty? - # Delete in slices to avoid "stack level too deep" errors for large numbers of keys - # See https://github.com/redis/redis-rb/issues/122 - keys.each_slice(1000) { |key_slice| redis_instance.del(*key_slice) } + # redis-namespace v1.10.0 introduced clear https://github.com/resque/redis-namespace/pull/202 + redis_instance.clear destroy end private - def redis_instance - Hyrax::RedisEventStore.instance - end + def redis_instance + Hyrax::RedisEventStore.instance + end end diff --git a/app/models/role.rb b/app/models/role.rb index 4b5c4eefdf..f30b58e30b 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -21,7 +21,7 @@ class Role < ApplicationRecord def description_label label = description || I18n.t("hyku.admin.roles.description.#{name}") - return '' if label =~ /^translation missing:/ + return '' if /^translation missing:/.match?(label) label end @@ -29,13 +29,13 @@ def description_label def set_sort_value self.sort_value = if name == 'admin' 0 - elsif name =~ /manager/ + elsif /manager/.match?(name) 1 - elsif name =~ /editor/ + elsif /editor/.match?(name) 2 - elsif name =~ /depositor/ + elsif /depositor/.match?(name) 3 - elsif name =~ /reader/ + elsif /reader/.match?(name) 4 else 99 diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index ad50b6e1ea..4410a5bc84 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -2,6 +2,10 @@ class SearchBuilder < Blacklight::SearchBuilder include Blacklight::Solr::SearchBuilderBehavior + include BlacklightRangeLimit::RangeLimitBuilder + include BlacklightAdvancedSearch::AdvancedSearchBuilder include Hydra::AccessControlsEnforcement include Hyrax::SearchFilters + + self.default_processor_chain += %i[add_advanced_parse_q_to_solr add_advanced_search_to_solr] end diff --git a/app/models/site.rb b/app/models/site.rb index 777bf1a0ba..b250a193cd 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -51,26 +51,26 @@ def admin_emails=(emails) private - # Add/invite admins via email address - # @param [Array<String>] Array of user emails - def add_admins_by_email(emails) - # For users that already have accounts, add to role immediately - existing_emails = User.where(email: emails).map do |u| - u.add_role :admin, self - u.email - end - # For new users, send invitation and add to role - (emails - existing_emails).each do |email| - u = User.invite!(email: email) - u.add_role :admin, self - end + # Add/invite admins via email address + # @param [Array<String>] Array of user emails + def add_admins_by_email(emails) + # For users that already have accounts, add to role immediately + existing_emails = User.where(email: emails).map do |u| + u.add_role :admin, self + u.email + end + # For new users, send invitation and add to role + (emails - existing_emails).each do |email| + u = User.invite!(email:) + u.add_role :admin, self end + end - # Remove specific administrators - # @param [Array<String>] Array of user emails - def remove_admins_by_email(emails) - User.where(email: emails).find_each do |u| - u.remove_role :admin, self - end + # Remove specific administrators + # @param [Array<String>] Array of user emails + def remove_admins_by_email(emails) + User.where(email: emails).find_each do |u| + u.remove_role :admin, self end + end end diff --git a/app/models/solr_endpoint.rb b/app/models/solr_endpoint.rb index be217ab81f..3e90a68120 100644 --- a/app/models/solr_endpoint.rb +++ b/app/models/solr_endpoint.rb @@ -29,6 +29,9 @@ def switch! # Remove the solr collection then destroy this record def remove! + # NOTE: Other end points first call switch!; is that an oversight? Perhaps not as we're relying + # on a scheduled job to do the destructive work. + # Spin off as a job, so that it can fail and be retried separately from the other logic. if account.search_only? RemoveSolrCollectionJob.perform_later(collection, connection_options, 'cross_search_tenant') diff --git a/app/models/user.rb b/app/models/user.rb index 7bfdd16cfc..c81d0255e8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,14 +9,11 @@ class User < ApplicationRecord include Hyrax::User include Hyrax::UserUsageStats - attr_accessible :email, :password, :password_confirmation if Blacklight::Utils.needs_attr_accessible? # Connects this user object to Blacklights Bookmarks. include Blacklight::User # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable - devise :database_authenticatable, :invitable, :registerable, - :recoverable, :rememberable, :trackable, :validatable, - :omniauthable, omniauth_providers: %i[saml openid_connect cas] + devise(*Hyku::Application.user_devise_parameters) after_create :add_default_group_membership! @@ -25,7 +22,7 @@ def self.default_scope where(guest: false) end - scope :for_repository, -> { + scope :for_repository, lambda { joins(:roles) } @@ -49,18 +46,18 @@ def to_s email end - def is_admin + def admin? has_role?(:admin) || has_role?(:admin, Site.instance) end - def is_superadmin + def superadmin? has_role? :superadmin end # This comes from a checkbox in the proprietor interface # Rails checkboxes are often nil or "0" so we handle that # case directly - def is_superadmin=(value) + def superadmin=(value) value = ActiveModel::Type::Boolean.new.cast(value) if value add_role :superadmin @@ -99,7 +96,8 @@ def site_roles=(roles) # u.hyrax_groups # => [#<Hyrax::Group id: 2, name: "registered", description: nil,...>] def hyrax_groups - roles.where(name: 'member', resource_type: 'Hyrax::Group').map(&:resource).uniq + # Why compact? In theory we shouldn't need this. But in tests we're seeing a case + roles.where(name: 'member', resource_type: 'Hyrax::Group').map(&:resource).uniq.compact end # Override method from hydra-access-controls v11.0.0 to use Hyrax::Groups. @@ -107,6 +105,13 @@ def hyrax_groups # @return [Array] Hyrax::Group names the User is a member of def groups hyrax_groups.map(&:name) + rescue NoMethodError + # Not quite raising the same exception, but this code is here to catch a flakey spec. What we're + # seeing is that an element in `hyrax_groups` is `nil` and which does not respond to `#name`. + # Looking at `#hyrax_groups` method, it's unclear how we'd find `nil`. + # + # Perhaps the `Hyrax::Group` is in a tenant and `Role` is not? Hmm. + raise "Hyrax::Groups: #{roles.where(name: 'member', resource_type: 'Hyrax::Group').all.inspect}\nRoles: #{roles.all.inspect}" end # NOTE: This is an alias for #groups to clarify what the method is doing. diff --git a/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb b/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb index 8348c245f0..815f35bfa3 100644 --- a/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb +++ b/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb @@ -8,82 +8,106 @@ module IiifAv # request.base_url => hostname # also to remove #auth_service since it was not working for now module DisplaysContentDecorator - private + def solr_document + defined?(super) ? super : object + end - def solr_document - defined?(super) ? super : object - end + def current_ability + defined?(super) ? super : @ability + end - def current_ability - defined?(super) ? super : @ability - end + Request = Struct.new(:base_url, keyword_init: true) - Request = Struct.new(:base_url, keyword_init: true) + def request + Request.new(base_url: hostname) + end - def request - Request.new(base_url: hostname) - end + private - def image_content - return nil unless latest_file_id - url = Hyrax.config.iiif_image_url_builder.call( - latest_file_id, - request.base_url, - Hyrax.config.iiif_image_size_default, - solr_document.mime_type - ) + def image_content + return nil unless latest_file_id + url = Hyrax.config.iiif_image_url_builder.call( + latest_file_id, + request.base_url, + Hyrax.config.iiif_image_size_default, + solr_document.mime_type + ) - # Serving up only prezi 3 - image_content_v3(url) - end + # Serving up only prezi 3 + image_content_v3(url) + end - def video_display_content(_url, label = '') - width = solr_document.width&.try(:to_i) || 320 - height = solr_document.height&.try(:to_i) || 240 - duration = conformed_duration_in_seconds - IIIFManifest::V3::DisplayContent.new( - Hyrax::IiifAv::Engine.routes.url_helpers.iiif_av_content_url( - solr_document.id, - label: label, - host: request.base_url - ), - label: label, - width: width, - height: height, - duration: duration, - type: 'Video', - format: solr_document.mime_type - ) + ## + # @note In the case where we have stream_urls, we'll assume the URL is correct. In the case + # where we're deferring to the document, we'll use {.iiif_video_labels_and_mime_types} + def video_content + # @see https://github.com/samvera-labs/iiif_manifest + streams = stream_urls + if streams.present? + streams.collect { |label, url| video_display_content(url, label) } + else + Hyku::Application.iiif_video_labels_and_mime_types.map do |label, mime_type| + url = Hyku::Application.iiif_video_url_builder.call(document: solr_document, label:, host: request.base_url) + video_display_content(url, label, mime_type:) + end end + end - def audio_display_content(_url, label = '') - duration = conformed_duration_in_seconds - IIIFManifest::V3::DisplayContent.new( - Hyrax::IiifAv::Engine.routes.url_helpers.iiif_av_content_url( - solr_document.id, - label: label, - host: request.base_url - ), - label: label, - duration: duration, - type: 'Sound', - format: solr_document.mime_type - ) - end + # rubocop:disable Metrics/MethodLength + def video_display_content(url, label = '', mime_type: solr_document.mime_type) + width = solr_document.width&.try(:to_i) || 320 + height = solr_document.height&.try(:to_i) || 240 + duration = conformed_duration_in_seconds + IIIFManifest::V3::DisplayContent.new( + url, + label:, + width:, + height:, + duration:, + type: 'Video', + format: mime_type + ) + end - def conformed_duration_in_seconds - if Array(solr_document.duration)&.first&.count(':') == 3 - # takes care of milliseconds like ["0:0:01:001"] - Time.zone.parse(Array(solr_document.duration).first.sub(/.*\K:/, '.')).seconds_since_midnight - elsif Array(solr_document.duration)&.first&.include?(':') - # if solr_document.duration evaluates to something like ["0:01:00"] which will get converted to seconds - Time.zone.parse(Array(solr_document.duration).first).seconds_since_midnight - else - # handles cases if solr_document.duration evaluates to something like ['25 s'] - Array(solr_document.duration).first.try(:to_f) - end || - 400.0 + def audio_content + streams = stream_urls + if streams.present? + streams.collect { |label, url| audio_display_content(url, label) } + else + Hyku::Application.iiif_audio_labels_and_mime_types.map do |label, mime_type| + audio_display_content(download_path(label), label, mime_type:) + end end + end + + def audio_display_content(_url, label = '', mime_type: solr_document.mime_type) + duration = conformed_duration_in_seconds + IIIFManifest::V3::DisplayContent.new( + Hyrax::IiifAv::Engine.routes.url_helpers.iiif_av_content_url( + solr_document.id, + label:, + host: request.base_url + ), + label:, + duration:, + type: 'Sound', + format: mime_type + ) + end + + def conformed_duration_in_seconds + if Array(solr_document.duration)&.first&.count(':') == 3 + # takes care of milliseconds like ["0:0:01:001"] + Time.zone.parse(Array(solr_document.duration).first.sub(/.*\K:/, '.')).seconds_since_midnight + elsif Array(solr_document.duration)&.first&.include?(':') + # if solr_document.duration evaluates to something like ["0:01:00"] which will get converted to seconds + Time.zone.parse(Array(solr_document.duration).first).seconds_since_midnight + else + # handles cases if solr_document.duration evaluates to something like ['25 s'] + Array(solr_document.duration).first.try(:to_f) + end || + 400.0 + end end end end diff --git a/app/presenters/hyku/admin/group/navigation_presenter.rb b/app/presenters/hyku/admin/group/navigation_presenter.rb index 7afaf6a9cd..99d31daf13 100644 --- a/app/presenters/hyku/admin/group/navigation_presenter.rb +++ b/app/presenters/hyku/admin/group/navigation_presenter.rb @@ -20,76 +20,76 @@ def tabs private - attr_reader :group_id, :params + attr_reader :group_id, :params - def edit_tab - Tab.new( - name: I18n.t('hyku.admin.groups.nav.attributes'), - controller: 'admin/groups', - action: 'edit', - path: Rails.application.routes.url_helpers.edit_admin_group_path(group_id), - context: params - ) - end + def edit_tab + Tab.new( + name: I18n.t('hyku.admin.groups.nav.attributes'), + controller: 'admin/groups', + action: 'edit', + path: Rails.application.routes.url_helpers.edit_admin_group_path(group_id), + context: params + ) + end - def members_tab - Tab.new( - name: I18n.t('hyku.admin.groups.nav.members'), - controller: 'admin/group_users', - action: 'index', - path: Rails.application.routes.url_helpers.admin_group_users_path(group_id), - context: params - ) - end + def members_tab + Tab.new( + name: I18n.t('hyku.admin.groups.nav.members'), + controller: 'admin/group_users', + action: 'index', + path: Rails.application.routes.url_helpers.admin_group_users_path(group_id), + context: params + ) + end - def roles_tab - Tab.new( - name: I18n.t('hyku.admin.groups.nav.roles'), - controller: 'admin/group_roles', - action: 'index', - path: Rails.application.routes.url_helpers.admin_group_roles_path(group_id), - context: params - ) - end + def roles_tab + Tab.new( + name: I18n.t('hyku.admin.groups.nav.roles'), + controller: 'admin/group_roles', + action: 'index', + path: Rails.application.routes.url_helpers.admin_group_roles_path(group_id), + context: params + ) + end - def remove_tab - Tab.new( - name: I18n.t('hyku.admin.groups.nav.delete'), - controller: 'admin/groups', - action: 'remove', - path: Rails.application.routes.url_helpers.remove_admin_group_path(group_id), - context: params - ) - end + def remove_tab + Tab.new( + name: I18n.t('hyku.admin.groups.nav.delete'), + controller: 'admin/groups', + action: 'remove', + path: Rails.application.routes.url_helpers.remove_admin_group_path(group_id), + context: params + ) + end - def group_id_key - return :id if params.key?(:id) - return :group_id if params.key?(:group_id) - :key_not_found - end + def group_id_key + return :id if params.key?(:id) + return :group_id if params.key?(:group_id) + :key_not_found + end - class Tab - ACTIVE_CSS_CLASS = 'active' + class Tab + ACTIVE_CSS_CLASS = 'active' - attr_reader :name, :path, :action + attr_reader :name, :path, :action - def initialize(name:, controller:, action:, path:, context:) - @name = name - @controller = controller - @action = action - @path = path - @context = context - end + def initialize(name:, controller:, action:, path:, context:) + @name = name + @controller = controller + @action = action + @path = path + @context = context + end - def css_class - return ACTIVE_CSS_CLASS if context.fetch(:controller) == controller && context.fetch(:action) == action - '' - end + def css_class + return ACTIVE_CSS_CLASS if context.fetch(:controller) == controller && context.fetch(:action) == action + '' + end - private + private - attr_reader :controller, :context - end + attr_reader :controller, :context + end end end end diff --git a/app/presenters/hyku/collapsable_section_presenter.rb b/app/presenters/hyku/collapsable_section_presenter.rb index f513b56a09..b2a263e730 100644 --- a/app/presenters/hyku/collapsable_section_presenter.rb +++ b/app/presenters/hyku/collapsable_section_presenter.rb @@ -1,31 +1,32 @@ # frozen_string_literal: true module Hyku - # Draws a collapsable list widget using the Bootstrap 3 / Collapse.js plugin + # Draws a collapsable list widget using the Bootstrap 4 / Collapse.js plugin class CollapsableSectionPresenter < Hyrax::CollapsableSectionPresenter - # Override Hyrax 3.5.0 to pass in html_options + # Override Hyrax v5.0.0rc2 to pass in the title attribute # rubocop:disable Metrics/ParameterLists - def initialize(view_context:, text:, id:, icon_class:, open:, html_options: {}) + def initialize(view_context:, text:, id:, icon_class:, open:, title: nil) # rubocop:enable Metrics/ParameterLists - super(view_context: view_context, text: text, id: id, icon_class: icon_class, open: open) - @html_options = html_options + super(view_context:, text:, id:, icon_class:, open:) + @title = title end - attr_reader :html_options + attr_reader :title private - def button_tag - tag.a({ role: 'button', - class: "#{button_class}collapse-toggle", - data: { toggle: 'collapse' }, - href: "##{id}", - onclick: "toggleCollapse(this)", - 'aria-expanded' => open, - 'aria-controls' => id }.merge(html_options)) do - safe_join([tag.span('', class: icon_class, 'aria-hidden': true), - tag.span(text)], ' ') - end + def button_tag + tag.a(role: 'button', + class: "#{button_class}collapse-toggle nav-link", + data: { toggle: 'collapse' }, + href: "##{id}", + onclick: "toggleCollapse(this)", + 'aria-expanded' => open, + 'aria-controls' => id, + title:) do + safe_join([tag.span('', class: icon_class, 'aria-hidden': true), + tag.span(text)], ' ') end + end end end diff --git a/app/presenters/hyku/menu_presenter.rb b/app/presenters/hyku/menu_presenter.rb index 7ed0c9e8fb..a0a6c71480 100644 --- a/app/presenters/hyku/menu_presenter.rb +++ b/app/presenters/hyku/menu_presenter.rb @@ -50,16 +50,16 @@ def show_task? end # Draw a collaspable menu section. The passed block should contain <li> items. - # Override Hyrax 3.5.0 to pass in html_options + # Override Hyrax v5.0.0rc2 to pass in title attribute # rubocop:disable Metrics/ParameterLists - def collapsable_section(text, id:, icon_class:, open:, **html_options, &block) + def collapsable_section(text, id:, icon_class:, title:, open:, &block) # rubocop:enable Metrics/ParameterLists - CollapsableSectionPresenter.new(view_context: view_context, - text: text, - id: id, - icon_class: icon_class, - open: open, - html_options: html_options).render(&block) + CollapsableSectionPresenter.new(view_context:, + text:, + id:, + icon_class:, + title:, + open:).render(&block) end end end diff --git a/app/presenters/hyku/work_show_presenter.rb b/app/presenters/hyku/work_show_presenter.rb index 12427bc418..5e36d7af3b 100644 --- a/app/presenters/hyku/work_show_presenter.rb +++ b/app/presenters/hyku/work_show_presenter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # OVERRIDE here to add featured collection methods and to delegate collection presenters to the member presenter factory -# OVERRIDE: Hyrax 3.4.0 to add Hyrax IIIF AV +# OVERRIDE: Hyrax 5.0.0rc2 to add Hyrax IIIF AV module Hyku class WorkShowPresenter < Hyrax::WorkShowPresenter @@ -12,7 +12,7 @@ class WorkShowPresenter < Hyrax::WorkShowPresenter delegate :title_or_label, :extent, to: :solr_document - # OVERRIDE Hyrax v2.9.0 here to make featured collections work + # OVERRIDE Hyrax v5.0.0rc2 here to make featured collections work delegate :collection_presenters, to: :member_presenter_factory # assumes there can only be one doi @@ -46,9 +46,7 @@ def display_unfeature_collection_link? def collection_featured? # only look this up if it's not boolean; ||= won't work here - if @collection_featured.nil? - @collection_featured = FeaturedCollection.where(collection_id: solr_document.id).exists? - end + @collection_featured = FeaturedCollection.where(collection_id: solr_document.id).exists? if @collection_featured.nil? @collection_featured end @@ -68,23 +66,23 @@ def iiif_viewer? private - def iiif_media?(presenter: representative_presenter) - presenter.image? || presenter.video? || presenter.audio? || presenter.pdf? - end + def iiif_media?(presenter: representative_presenter) + presenter.image? || presenter.video? || presenter.audio? || presenter.pdf? + end - def members_include_viewable? - file_set_presenters.any? do |presenter| - iiif_media?(presenter: presenter) && current_ability.can?(:read, presenter.id) - end + def members_include_viewable? + file_set_presenters.any? do |presenter| + iiif_media?(presenter:) && current_ability.can?(:read, presenter.id) end + end - def extract_from_identifier(rgx) - if solr_document['identifier_tesim'].present? - ref = solr_document['identifier_tesim'].map do |str| - str.scan(rgx) - end + def extract_from_identifier(rgx) + if solr_document['identifier_tesim'].present? + ref = solr_document['identifier_tesim'].map do |str| + str.scan(rgx) end - ref end + ref + end end end diff --git a/app/presenters/hyrax/admin/dashboard_presenter.rb b/app/presenters/hyrax/admin/dashboard_presenter.rb deleted file mode 100644 index e197d45e7e..0000000000 --- a/app/presenters/hyrax/admin/dashboard_presenter.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 to fix user count. by joining roles, we keep the count correct even though there are many users -module Hyrax - module Admin - class DashboardPresenter - # @return [Fixnum] the number of currently registered users - def user_count(start_date, end_date) - ::User.for_repository - .where(guest: false) - .where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day) - .count - end - - def repository_objects - @repository_objects ||= Admin::RepositoryObjectPresenter.new - end - - def repository_growth(start_date, end_date) - @repository_growth ||= Admin::RepositoryGrowthPresenter.new(start_date, end_date) - end - - def user_activity(start_date, end_date) - @user_activity ||= Admin::UserActivityPresenter.new(start_date, end_date) - end - end - end -end diff --git a/app/presenters/hyrax/admin/dashboard_presenter_decorator.rb b/app/presenters/hyrax/admin/dashboard_presenter_decorator.rb new file mode 100644 index 0000000000..13d385c858 --- /dev/null +++ b/app/presenters/hyrax/admin/dashboard_presenter_decorator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to fix user count. by joining roles, we keep the count correct even though there are many users +module Hyrax + module Admin + module DashboardPresenterDecorator + # @return [Fixnum] the number of currently registered users + def user_count(start_date, end_date) + ::User.for_repository + .where(guest: false) + .where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day) + .count + end + end + end +end + +Hyrax::Admin::DashboardPresenter.prepend(Hyrax::Admin::DashboardPresenterDecorator) diff --git a/app/presenters/hyrax/admin/users_presenter.rb b/app/presenters/hyrax/admin/users_presenter.rb index 17d2fc0ba2..8d49fd538f 100644 --- a/app/presenters/hyrax/admin/users_presenter.rb +++ b/app/presenters/hyrax/admin/users_presenter.rb @@ -34,10 +34,10 @@ def show_last_access? private - # Returns a list of users excluding the system users and guest_users - def search - ::User.registered.for_repository.without_system_accounts.uniq - end + # Returns a list of users excluding the system users and guest_users + def search + ::User.registered.for_repository.without_system_accounts.uniq + end end end end diff --git a/app/presenters/hyrax/admin/workflow_roles_presenter.rb b/app/presenters/hyrax/admin/workflow_roles_presenter.rb deleted file mode 100644 index db04ac22a6..0000000000 --- a/app/presenters/hyrax/admin/workflow_roles_presenter.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax v3.4.2 Expand to allow adding groups to workflow roles -module Hyrax - module Admin - # Displays a list of users and their associated workflow roles - class WorkflowRolesPresenter - def users - ::User.registered - end - - # OVERRIDE: New method for adding groups - def groups - Hyrax::Group.all - end - - # OVERRIDE: New method for adding groups - def group_presenter_for(group) - agent = group.to_sipity_agent - return unless agent - AgentPresenter.new(agent) - end - - def presenter_for(user) - agent = user.sipity_agent - return unless agent - AgentPresenter.new(agent) - end - - class AgentPresenter - def initialize(agent) - @agent = agent - end - - def responsibilities_present? - @agent.workflow_responsibilities.any? - end - - def responsibilities - @agent.workflow_responsibilities.each do |responsibility| - yield ResponsibilityPresenter.new(responsibility) - end - end - end - - class ResponsibilityPresenter - def initialize(responsibility) - @responsibility = responsibility - @workflow_role_presenter = WorkflowRolePresenter.new(responsibility.workflow_role) - end - - attr_accessor :responsibility - - delegate :label, to: :@workflow_role_presenter - end - end - end -end diff --git a/app/presenters/hyrax/admin/workflow_roles_presenter_decorator.rb b/app/presenters/hyrax/admin/workflow_roles_presenter_decorator.rb new file mode 100644 index 0000000000..37b1519135 --- /dev/null +++ b/app/presenters/hyrax/admin/workflow_roles_presenter_decorator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 Expand to allow adding groups to workflow roles + +module Hyrax + module Admin + # Displays a list of users and their associated workflow roles + module WorkflowRolesPresenterDecorator + # OVERRIDE: New method for adding groups + def groups + Hyrax::Group.all + end + + # OVERRIDE: New method for adding groups + def group_presenter_for(group) + agent = group.to_sipity_agent + return unless agent + Hyrax::Admin::WorkflowRolesPresenter::AgentPresenter.new(agent) + end + end + end +end + +Hyrax::Admin::WorkflowRolesPresenter.prepend(Hyrax::Admin::WorkflowRolesPresenterDecorator) diff --git a/app/presenters/hyrax/collection_presenter_decorator.rb b/app/presenters/hyrax/collection_presenter_decorator.rb index c348e58881..d6def36a35 100644 --- a/app/presenters/hyrax/collection_presenter_decorator.rb +++ b/app/presenters/hyrax/collection_presenter_decorator.rb @@ -1,27 +1,18 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.1 +# OVERRIDE Hyrax v5.0.0rc2 # - Add collection methods to collection presenter and override to return # full banner_file data, rather than only download path to file. # - Alter permissions-related behavior. # Terms is the list of fields displayed by app/views/collections/_show_descriptions.html.erb module Hyrax module CollectionPresenterDecorator - def self.decorate(base) - base.prepend(self) + extend ActiveSupport::Concern - base.redefine_singleton_method(:terms) do + class_methods do + def terms # OVERRIDE Hyrax - removed size - %i[total_items - resource_type - creator contributor - keyword license - publisher - date_created - subject language - identifier - based_near - related_url] + super - [:size] end end @@ -33,6 +24,7 @@ def create_any_work_types? create_work_presenter.authorized_models.any? end + # OVERRIDE Hyrax - remove size def [](key) case key when :total_items @@ -49,19 +41,11 @@ def [](key) # @return String the access label (e.g. Manage, Deposit, View) def managed_access # OVERRIDE: Change check for manage access from :edit to :destroy - if current_ability.can?(:destroy, solr_document) - return I18n.t('hyrax.dashboard.my.collection_list.managed_access.manage') - end + return I18n.t('hyrax.dashboard.my.collection_list.managed_access.manage') if current_ability.can?(:destroy, solr_document) # OVERRIDE: Add label for Edit access - if current_ability.can?(:edit, solr_document) - return I18n.t('hyrax.dashboard.my.collection_list.managed_access.edit') - end - if current_ability.can?(:deposit, solr_document) - return I18n.t('hyrax.dashboard.my.collection_list.managed_access.deposit') - end - if current_ability.can?(:read, solr_document) - return I18n.t('hyrax.dashboard.my.collection_list.managed_access.view') - end + return I18n.t('hyrax.dashboard.my.collection_list.managed_access.edit') if current_ability.can?(:edit, solr_document) + return I18n.t('hyrax.dashboard.my.collection_list.managed_access.deposit') if current_ability.can?(:deposit, solr_document) + return I18n.t('hyrax.dashboard.my.collection_list.managed_access.view') if current_ability.can?(:read, solr_document) '' end @@ -87,7 +71,7 @@ def banner_file filename = File.split(banner_info.first.local_path).last unless banner_info.empty? alttext = banner_info.first.alt_text unless banner_info.empty? relative_path = "/" + banner_info.first.local_path.split("/")[-4..-1].join("/") unless banner_info.empty? - { filename: filename, relative_path: relative_path, alt_text: alttext } + { filename:, relative_path:, alt_text: alttext } end end @@ -106,9 +90,7 @@ def display_unfeature_collection_link? def collection_featured? # only look this up if it's not boolean; ||= won't work here - if @collection_featured.nil? - @collection_featured = FeaturedCollection.where(collection_id: solr_document.id).exists? - end + @collection_featured = FeaturedCollection.where(collection_id: solr_document.id).exists? if @collection_featured.nil? @collection_featured end @@ -119,4 +101,4 @@ def user_can_feature_collection? end end -Hyrax::CollectionPresenterDecorator.decorate(Hyrax::CollectionPresenter) +Hyrax::CollectionPresenter.prepend(Hyrax::CollectionPresenterDecorator) diff --git a/app/presenters/hyrax/homepage_presenter.rb b/app/presenters/hyrax/homepage_presenter.rb deleted file mode 100644 index 76fe4bf492..0000000000 --- a/app/presenters/hyrax/homepage_presenter.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 to add methods to: -# hide featured researcher -# hide featured works -# hide recently uploaded -# hide share button -module Hyrax - class HomepagePresenter - class_attribute :create_work_presenter_class - self.create_work_presenter_class = Hyrax::SelectTypeListPresenter - attr_reader :current_ability, :collections - - def initialize(current_ability, collections) - @current_ability = current_ability - @collections = collections - end - - # OVERRIDE: Hyrax v3.4.0 to removed: @return [Boolean] If the - # display_share_button_when_not_logged_in? is activated, then - # return true since we are utilizing the feature flipper - # Flipflop.show_share_button? in Hyku. - - # @return [Boolean] If the current user is a guest - # and the feature flipper is enabled or if the signed in - # user has permission to create at least one kind of work - # and the feature flipper is enabled. - - def display_share_button? - Flipflop.show_share_button? && current_ability.can_create_any_work? || - Flipflop.show_share_button? && user_unregistered? - end - - # A presenter for selecting a work type to create - # this is needed here because the selector is in the header on every page - def create_work_presenter - @create_work_presenter ||= create_work_presenter_class.new(current_ability.current_user) - end - - def create_many_work_types? - create_work_presenter.many? - end - - def draw_select_work_modal? - display_share_button? && create_many_work_types? - end - - def first_work_type - create_work_presenter.first_model - end - - # changed to add feature flag for featured researcher - def display_featured_researcher? - Flipflop.show_featured_researcher? - end - - # changed to add feature flag for featured work - def display_featured_works? - Flipflop.show_featured_works? - end - - # changed to add feature flag for recently uploaded - def display_recently_uploaded? - Flipflop.show_recently_uploaded? - end - - private - - def user_unregistered? - current_ability.current_user.new_record? || - current_ability.current_user.guest? - end - end -end diff --git a/app/presenters/hyrax/homepage_presenter_decorator.rb b/app/presenters/hyrax/homepage_presenter_decorator.rb new file mode 100644 index 0000000000..15b52204e4 --- /dev/null +++ b/app/presenters/hyrax/homepage_presenter_decorator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 5.0 to hide features via flipper +# hide featured researcher +# hide featured works +# hide recently uploaded +# hide share button +module Hyrax + module HomepagePresenterDecorator + def display_share_button? + Flipflop.show_share_button? && current_ability.can_create_any_work? || + Flipflop.show_share_button? && user_unregistered? + end + + def display_featured_researcher? + Flipflop.show_featured_researcher? + end + + def display_featured_works? + Flipflop.show_featured_works? + end + + def display_recently_uploaded? + Flipflop.show_recently_uploaded? + end + end +end + +Hyrax::HomepagePresenter.prepend(Hyrax::HomepagePresenterDecorator) diff --git a/app/presenters/hyrax/iiif_manifest_presenter_decorator.rb b/app/presenters/hyrax/iiif_manifest_presenter_decorator.rb index c34dbb1671..a7c4b59c23 100644 --- a/app/presenters/hyrax/iiif_manifest_presenter_decorator.rb +++ b/app/presenters/hyrax/iiif_manifest_presenter_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax 3.4.0 to check the site's ssl_configured when setting protocols +# OVERRIDE Hyrax v5.0.0rc2 to check the site's ssl_configured when setting protocols module Hyrax module IiifManifestPresenterDecorator attr_writer :iiif_version @@ -30,7 +30,7 @@ def manifest_url return '' if id.blank? protocol = Site.account.ssl_configured ? 'https' : 'http' - Rails.application.routes.url_helpers.polymorphic_url([:manifest, model], host: hostname, protocol: protocol) + Rails.application.routes.url_helpers.polymorphic_url([:manifest, model], host: hostname, protocol:) end def iiif_version diff --git a/app/search_builders/adv_search_builder.rb b/app/search_builders/adv_search_builder.rb new file mode 100644 index 0000000000..92e486f11a --- /dev/null +++ b/app/search_builders/adv_search_builder.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AdvSearchBuilder < IiifPrint::CatalogSearchBuilder + include Blacklight::Solr::SearchBuilderBehavior + include BlacklightAdvancedSearch::AdvancedSearchBuilder + + # A Solr param filter that is NOT included by default in the chain, + # but is appended by AdvancedController#index, to do a search + # for facets _ignoring_ the current query, we want the facets + # as if the current query weren't there. + # + # Also adds any solr params set in blacklight_config.advanced_search[:form_solr_parameters] + def facets_for_advanced_search_form(solr_p) + # ensure empty query is all records, to fetch available facets on entire corpus + solr_p["q"] = '{!lucene}*:*' + # explicitly use lucene defType since we are passing a lucene query above (and appears to be required for solr 7) + solr_p["defType"] = 'lucene' + # We only care about facets, we don't need any rows. + solr_p["rows"] = "0" + + # Anything set in config as a literal + solr_p.merge!(blacklight_config.advanced_search[:form_solr_parameters]) if blacklight_config.advanced_search[:form_solr_parameters] + end +end diff --git a/app/search_builders/hyrax/collection_member_search_builder_decorator.rb b/app/search_builders/hyrax/collection_member_search_builder_decorator.rb index b9ba5ef6dd..0f4676a42e 100644 --- a/app/search_builders/hyrax/collection_member_search_builder_decorator.rb +++ b/app/search_builders/hyrax/collection_member_search_builder_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE: Hyrax 3.4.1 adds filesets to search to allow full text search results on the Collection show pages +# OVERRIDE: Hyrax 5.0.0rc2 adds filesets to search to allow full text search results on the Collection show pages module Hyrax module CollectionMemberSearchBuilderDecorator Hyrax::CollectionMemberSearchBuilder.default_processor_chain += [:show_works_or_works_that_contain_files] diff --git a/app/services/create_account.rb b/app/services/create_account.rb index f021fbff4b..536c2f5952 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -46,18 +46,18 @@ def create_defaults Hyrax::CollectionType.find_or_create_admin_set_type return if account.search_only? - AdminSet.find_or_create_default_admin_set_id + Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id end # Workaround for upstream issue https://github.com/samvera/hyrax/issues/3136 def fillin_translations collection_types = Hyrax::CollectionType.all collection_types.each do |c| - next unless c.title =~ /^translation missing/ + next unless /^translation missing/.match?(c.title) oldtitle = c.title c.title = I18n.t(c.title.gsub("translation missing: en.", '')) c.save - Rails.logger.debug "#{oldtitle} changed to #{c.title}" + Rails.logger.debug { "#{oldtitle} changed to #{c.title}" } end end @@ -87,7 +87,7 @@ def schedule_recurring_jobs private - def initialize_account_data - Site.update(account: account) - end + def initialize_account_data + Site.update(account:) + end end diff --git a/app/services/group_aware_role_checker.rb b/app/services/group_aware_role_checker.rb index 76b7752bcb..2435cf2a23 100644 --- a/app/services/group_aware_role_checker.rb +++ b/app/services/group_aware_role_checker.rb @@ -6,22 +6,22 @@ module GroupAwareRoleChecker # their role checker methods are automatically defined RolesService::DEFAULT_ROLES.each do |role_name| define_method(:"#{role_name}?") do - has_group_aware_role?(role_name) + group_aware_role?(role_name) end end private - # Check for the presence of the passed role_name in the User's Roles and - # the User's Hyrax::Group's Roles. - def has_group_aware_role?(role_name) # rubocop:disable Naming/PredicateName - return false if current_user.new_record? - return true if current_user.has_role?(role_name, Site.instance) + # Check for the presence of the passed role_name in the User's Roles and + # the User's Hyrax::Group's Roles. + def group_aware_role?(role_name) + return false if current_user.new_record? + return true if current_user.has_role?(role_name, Site.instance) - current_user.hyrax_groups.each do |group| - return true if group.has_site_role?(role_name) - end - - false + current_user.hyrax_groups.each do |group| + return true if group.site_role?(role_name) end + + false + end end diff --git a/app/services/hyrax/admin_set_create_service_decorator.rb b/app/services/hyrax/admin_set_create_service_decorator.rb index 631681a0c2..989c8522cb 100644 --- a/app/services/hyrax/admin_set_create_service_decorator.rb +++ b/app/services/hyrax/admin_set_create_service_decorator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Hyrax - # This decorator is used to override logic found in Hyrax v3.4.2 + # This decorator is used to override logic found in Hyrax v5.0.0rc2 # # Because Hyku has converted the Hyrax::Group model from a PORO to a db-backed active record object, # we have to query for existing Hyrax groups instead of initializing empty ones. @@ -16,8 +16,8 @@ def create! ActiveRecord::Base.transaction do permission_template = PermissionTemplate.find_by(source_id: result.id.to_s) || permissions_create_service.create_default(collection: result, - creating_user: creating_user) - create_workflows_for(permission_template: permission_template) + creating_user:) + create_workflows_for(permission_template:) # OVERRIDE: Remove call to #create_default_access_for, which granted # deposit access to all registered users for the Default Admin Set end @@ -27,23 +27,25 @@ def create! updated_admin_set end + private + def workflow_agents [ # OVERRIDE: replace #new with #find_by(:name) - Hyrax::Group.find_by(name: admin_group_name) + Sipity::Agent(Hyrax::Group.find_by(name: admin_group_name)) ].tap do |agent_list| # The default admin set does not have a creating user - agent_list << creating_user if creating_user + agent_list << Sipity::Agent(creating_user) if creating_user end end def create_workflows_for(permission_template:) - workflow_importer.call(permission_template: permission_template) + workflow_importer.call(permission_template:) # OVERRIDE: Extract and expand upon granting Workflow Roles into service object so it can be used in RolesService Hyrax::Workflow::PermissionGrantor - .grant_default_workflow_roles!(permission_template: permission_template, creating_user: creating_user) + .grant_default_workflow_roles!(permission_template:, creating_user:) Sipity::Workflow - .activate!(permission_template: permission_template, workflow_name: Hyrax.config.default_active_workflow_name) + .activate!(permission_template:, workflow_name: Hyrax.config.default_active_workflow_name) end end end diff --git a/app/services/hyrax/collection_types/create_service.rb b/app/services/hyrax/collection_types/create_service_decorator.rb similarity index 52% rename from app/services/hyrax/collection_types/create_service.rb rename to app/services/hyrax/collection_types/create_service_decorator.rb index 07891f9b37..52d37c5761 100644 --- a/app/services/hyrax/collection_types/create_service.rb +++ b/app/services/hyrax/collection_types/create_service_decorator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 +# OVERRIDE Hyrax v5.0.0rc2 # - Give the :collection_manager role MANAGE_ACCESS to all non-AdminSet CollectionTypes by default # - Give the :collection_editor role CREATE_ACCESS to all non-AdminSet CollectionTypes by default # - Exclude CREATE_ACCESS from ::Ability.registered_group_name (all registered users) if we are restricting permissions @@ -17,7 +17,7 @@ module CollectionTypes # # @see Hyrax:CollectionType # - class CreateService # rubocop:disable Metrics/ClassLength + module CreateServiceDecorator # rubocop:disable Metrics/ModuleLength DEFAULT_OPTIONS = { description: '', nestable: true, @@ -63,8 +63,6 @@ class CreateService # rubocop:disable Metrics/ClassLength end }.freeze - USER_COLLECTION_MACHINE_ID = Hyrax::CollectionType::USER_COLLECTION_MACHINE_ID - USER_COLLECTION_TITLE = Hyrax::CollectionType::USER_COLLECTION_DEFAULT_TITLE USER_COLLECTION_OPTIONS = { description: I18n.t('hyrax.collection_types.create_service.default_description'), nestable: true, @@ -110,95 +108,6 @@ class CreateService # rubocop:disable Metrics/ClassLength end }.freeze - ADMIN_SET_MACHINE_ID = Hyrax::CollectionType::ADMIN_SET_MACHINE_ID - ADMIN_SET_TITLE = Hyrax::CollectionType::ADMIN_SET_DEFAULT_TITLE - ADMIN_SET_OPTIONS = { - description: I18n.t('hyrax.collection_types.create_service.admin_set_description'), - nestable: false, - brandable: false, - discoverable: false, - sharable: true, - share_applies_to_new_works: true, - allow_multiple_membership: false, - require_membership: true, - assigns_workflow: true, - assigns_visibility: true, - badge_color: "#405060", - participants: [ - { - agent_type: Hyrax::CollectionTypeParticipant::GROUP_TYPE, - agent_id: ::Ability.admin_group_name, - access: Hyrax::CollectionTypeParticipant::MANAGE_ACCESS - }, - { - agent_type: Hyrax::CollectionTypeParticipant::GROUP_TYPE, - agent_id: ::Ability.admin_group_name, - access: Hyrax::CollectionTypeParticipant::CREATE_ACCESS - } - ] - }.freeze - - # @api public - # - # Create a new collection type. - # - # @param machine_id [String] - # @param title [String] short tag identifying the collection type - # @param options [Hash] options to override DEFAULT_OPTIONS - # @option options [String] :description a description to show the user when selecting the collection type - # @option options [Boolean] :nestable if true, collections of this type can be nested - # @option options [Boolean] :brandable if true, collections of this type can be branded - # @option options [Boolean] :discoverable if true, collections of this type can be marked Public - # and found in search results - # @option options [Boolean] :sharable if true, collections of this type can have participants added for - # :manage, :deposit, or :view access - # @option options [Boolean] :share_applies_to_new_works if true, share participant permissions are applied - # to new works created in the collection - # @option options [Boolean] :allow_multiple_membership if true, works can be members of multiple collections - # of this type - # @option options [Boolean] :require_membership if true, all works must belong to at least one collection - # of this type. When combined with allow_multiple_membership=false, works can belong to one and only one - # collection of this type. - # @option options [Boolean] :assigns_workflow if true, collections of this type can be used to assign - # a workflow to a work - # @option options [Boolean] :assigns_visibility if true, collections of this type can be used to assign - # initial visibility to a work - # @option options [String] :badge_color a color for the badge to show the user when selecting the collection type - # @return [Hyrax::CollectionType] the newly created collection type instance - def self.create_collection_type(machine_id:, title:, options: {}) - opts = DEFAULT_OPTIONS.merge(options).except(:participants) - ct = Hyrax::CollectionType.create!(opts.merge(machine_id: machine_id, title: title)) - participants = options[:participants].presence || DEFAULT_OPTIONS[:participants] - add_participants(ct.id, participants) - ct - end - - # @api public - # - # Create admin set collection type. - # - # @return [Hyrax::CollectionType] the newly created admin set collection type instance - def self.create_admin_set_type - create_collection_type( - machine_id: ADMIN_SET_MACHINE_ID, - title: ADMIN_SET_TITLE, - options: ADMIN_SET_OPTIONS - ) - end - - # @api public - # - # Create user collection type. - # - # @return [Hyrax::CollectionType] the newly created user collection type instance - def self.create_user_collection_type - create_collection_type( - machine_id: USER_COLLECTION_MACHINE_ID, - title: USER_COLLECTION_TITLE, - options: USER_COLLECTION_OPTIONS - ) - end - # @api public # # Add the default participants to a collection_type. @@ -208,7 +117,8 @@ def self.create_user_collection_type # The same values can be retrieved directly from a passed in ability. # If calling from Abilities, pass the ability. # If you try to get the ability from the user, you end up in an infinite loop. - def self.add_default_participants(collection_type_id) + # rubocop:disable Metrics/MethodLength + def add_default_participants(collection_type_id) return unless collection_type_id default_participants = [{ agent_type: Hyrax::CollectionTypeParticipant::GROUP_TYPE, agent_id: ::Ability.admin_group_name, @@ -233,54 +143,10 @@ def self.add_default_participants(collection_type_id) end add_participants(collection_type_id, default_participants) end - - ## - # @api public - # - # Add a participants to a collection_type. - # - # @param collection_type_id [Integer] the id of the collection type - # @param participants [Array<Hash>] each element holds agent_type, agent_id, - # and access for a participant to be added - # - # @raise [InvalidParticipantError] if a participant is missing an - # `agent_type`, `agent_id`, or `access`. - def self.add_participants(collection_type_id, participants) - raise(InvalidParticipantError, participants) unless - participants.all? { |p| p.key?(:agent_type) && p.key?(:agent_id) && p.key?(:access) } - - participants.each do |p| - Hyrax::CollectionTypeParticipant.create!( - hyrax_collection_type_id: collection_type_id, - agent_type: p.fetch(:agent_type), - agent_id: p.fetch(:agent_id), - access: p.fetch(:access) - ) - end - rescue InvalidParticipantError => error - Rails.logger.error "Participants not created for collection type " \ - " #{collection_type_id}: #{error.message}" - raise error - end - - ## - # An error class for the case that invalid/incomplete participants are - # added to a collection type. - class InvalidParticipantError < RuntimeError - attr_reader :participants - - def initialize(participants) - @participants = participants - end - - ## - # @return [String] - def message - @participants.map do |participant| - "#{participant[:agent_type]}, #{participant[:agent_id]}, #{participant[:access]}" - end.join("--\n") - end - end - end + end # rubocop:enable Metrics/ModuleLength end end + +Hyrax::CollectionTypes::CreateService.singleton_class.send(:prepend, Hyrax::CollectionTypes::CreateServiceDecorator) +Hyrax::CollectionTypes::CreateService::DEFAULT_OPTIONS = Hyrax::CollectionTypes::CreateServiceDecorator::DEFAULT_OPTIONS +Hyrax::CollectionTypes::CreateService::USER_COLLECTION_OPTIONS = Hyrax::CollectionTypes::CreateServiceDecorator::USER_COLLECTION_OPTIONS diff --git a/app/services/hyrax/collections/permissions_create_service_decorator.rb b/app/services/hyrax/collections/permissions_create_service_decorator.rb index 9324f41e74..63b392da41 100644 --- a/app/services/hyrax/collections/permissions_create_service_decorator.rb +++ b/app/services/hyrax/collections/permissions_create_service_decorator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# OVERRIDE Hyrax v3.4.2 Grants certain roles access to either all AdminSets or -# all Collections (depending on the role) at create time. +# OVERRIDE Hyrax v5.0.0rc2 Grants certain roles access to either all AdminSets or +# all Collections (depending on the role) at create time. module Hyrax module Collections module PermissionsCreateServiceDecorator @@ -58,9 +58,11 @@ def access_grants_attributes(collection_type:, creating_user:, grants:) access: Hyrax::PermissionTemplateAccess::VIEW } end + # rubocop:disable Lint/Void attribute_list + # rubocop:enable Lint/Void # OVERRIDE END - end + managers_of_collection_type(collection_type: collection_type) + grants + end + managers_of_collection_type(collection_type:) + grants end private :access_grants_attributes end diff --git a/app/services/hyrax/collections/permissions_service_decorator.rb b/app/services/hyrax/collections/permissions_service_decorator.rb index 7a021dd2fa..60a29773ed 100644 --- a/app/services/hyrax/collections/permissions_service_decorator.rb +++ b/app/services/hyrax/collections/permissions_service_decorator.rb @@ -12,7 +12,7 @@ module PermissionsServiceDecorator # to have a whole method JUST for that... maybe make #manage_access_to_collection # public or use #send to call the private method directly? def can_manage_collection?(collection_id:, ability:) - manage_access_to_collection?(collection_id: collection_id, ability: ability) + manage_access_to_collection?(collection_id:, ability:) end end end diff --git a/app/services/hyrax/file_set_derivatives_service_decorator.rb b/app/services/hyrax/file_set_derivatives_service_decorator.rb new file mode 100644 index 0000000000..b46929d0ee --- /dev/null +++ b/app/services/hyrax/file_set_derivatives_service_decorator.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to increase the size of thumbnails + +module Hyrax + module FileSetDerivativesServiceDecorator + # @see https://github.com/samvera/hydra-derivatives/blob/main/lib/hydra/derivatives/processors/video/config.rb#L59 + DEFAULT_VIDEO_SIZE = '320x240' + + def create_pdf_derivatives(filename) + Hydra::Derivatives::PdfDerivatives.create(filename, outputs: [{ label: :thumbnail, + format: 'jpg', + size: '676x986', + url: derivative_url('thumbnail'), + layer: 0 }]) + extract_full_text(filename, uri) + end + + def create_office_document_derivatives(filename) + Hydra::Derivatives::DocumentDerivatives.create(filename, outputs: [{ label: :thumbnail, + format: 'jpg', + size: '600x450>', + url: derivative_url('thumbnail'), + layer: 0 }]) + extract_full_text(filename, uri) + end + + def create_image_derivatives(filename) + # We're asking for layer 0, because otherwise pyramidal tiffs flatten all the layers together into the thumbnail + Hydra::Derivatives::ImageDerivatives.create(filename, outputs: [{ label: :thumbnail, + format: 'jpg', + size: '600x450>', + url: derivative_url('thumbnail'), + layer: 0 }]) + end + + # Ensures the video dimensions do not get altered when it is ingested + def create_video_derivatives(filename) + width = Array(file_set.width).first + height = Array(file_set.height).first + original_size = "#{width}x#{height}" + size = width.nil? || height.nil? ? DEFAULT_VIDEO_SIZE : original_size + + Hydra::Derivatives::Processors::Video::Processor.config.size_attributes = size + Hydra::Derivatives::VideoDerivatives.create(filename, + outputs: [{ label: :thumbnail, format: 'jpg', + url: derivative_url('thumbnail') }, + { label: 'webm', format: 'webm', + url: derivative_url('webm') }, + { label: 'mp4', format: 'mp4', + url: derivative_url('mp4') }]) + end + end +end + +Hyrax::FileSetDerivativesService.prepend(Hyrax::FileSetDerivativesServiceDecorator) diff --git a/app/services/hyrax/indexes_thumbnails.rb b/app/services/hyrax/indexes_thumbnails.rb deleted file mode 100644 index 6dd916c811..0000000000 --- a/app/services/hyrax/indexes_thumbnails.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 to make collection thumbnails uploadable -module Hyrax - module IndexesThumbnails - extend ActiveSupport::Concern - - included do - class_attribute :thumbnail_path_service - self.thumbnail_path_service = ThumbnailPathService - class_attribute :thumbnail_field - self.thumbnail_field = 'thumbnail_path_ss' - end - - # Adds thumbnail indexing to the solr document - def generate_solr_document - super.tap do |solr_doc| - index_thumbnails(solr_doc) - end - end - - # Write the thumbnail paths into the solr_document - # @param [Hash] solr_document the solr document to add the field to - def index_thumbnails(solr_document) - solr_document[thumbnail_field] = thumbnail_path - end - - # Returns the value for the thumbnail path to put into the solr document - def thumbnail_path - if object.class == Collection && UploadedCollectionThumbnailPathService.uploaded_thumbnail?(object) - UploadedCollectionThumbnailPathService.call(object) - else - self.class.thumbnail_path_service.call(object) - end - end - end -end diff --git a/app/services/hyrax/indexes_thumbnails_decorator.rb b/app/services/hyrax/indexes_thumbnails_decorator.rb new file mode 100644 index 0000000000..6620d5f979 --- /dev/null +++ b/app/services/hyrax/indexes_thumbnails_decorator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 to make collection thumbnails uploadable + +module Hyrax + module IndexesThumbnailsDecorator + # Returns the value for the thumbnail path to put into the solr document + def thumbnail_path + if object.try(:collection?) && UploadedCollectionThumbnailPathService.uploaded_thumbnail?(object) + UploadedCollectionThumbnailPathService.call(object) + else + super + end + end + end +end + +Hyrax::IndexesThumbnails.prepend(Hyrax::IndexesThumbnailsDecorator) diff --git a/app/services/hyrax/manifest_builder_service_decorator.rb b/app/services/hyrax/manifest_builder_service_decorator.rb index 2184205666..297cf9f9aa 100644 --- a/app/services/hyrax/manifest_builder_service_decorator.rb +++ b/app/services/hyrax/manifest_builder_service_decorator.rb @@ -1,25 +1,27 @@ # frozen_string_literal: true +# OVERRIDE Hyrax v5.0.0rc2 to change `&` to `&` in the Universal Viewer + module Hyrax module ManifestBuilderServiceDecorator private - ## - # @return [Hash<Array>] the Hash to be used by "label" to change `&` to `&` - # @see #loof - def sanitize_value(text) - return loof(text) unless text.is_a?(Hash) - text[text.keys.first] = text.values.flatten.map { |value| loof(value) } - text - end + ## + # @return [Hash<Array>] the Hash to be used by "label" to change `&` to `&` + # @see #loof + def sanitize_value(text) + return loof(text) unless text.is_a?(Hash) + text[text.keys.first] = text.values.flatten.map { |value| loof(value) } + text + end - ## - # @return [String] the String that gets unescaped since Loofah is too aggressive for example - # it changes to `&` to `&` which will be displayed in the Universal Viewer and manifest - # @see #sanitize_value - def loof(text) - CGI.unescapeHTML(Loofah.fragment(text.to_s).scrub!(:prune).to_s) - end + ## + # @return [String] the String that gets unescaped since Loofah is too aggressive for example + # it changes to `&` to `&` which will be displayed in the Universal Viewer and manifest + # @see #sanitize_value + def loof(text) + CGI.unescapeHTML(Loofah.fragment(text.to_s).scrub!(:prune).to_s) + end end end diff --git a/app/services/hyrax/permission_manager_decorator.rb b/app/services/hyrax/permission_manager_decorator.rb index 42732acdae..75efca6ae7 100644 --- a/app/services/hyrax/permission_manager_decorator.rb +++ b/app/services/hyrax/permission_manager_decorator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Hyrax - # This decorator is used to override logic found in Hyrax v3.4.1 + # This decorator is used to override logic found in Hyrax v5.0.0rc2 # # Because Hyku has converted the Hyrax::Group model from a PORO to a db-backed active record object, # we have to query for existing Hyrax groups instead of initializing empty ones. @@ -16,6 +16,8 @@ module Hyrax # Because of this, we also add queries for Role permissions in addition to Group permissions # as part of these overrides. module PermissionManagerDecorator + private + def update_groups_for(mode:, groups:) groups = groups.map(&:to_s) diff --git a/app/services/hyrax/quick_classification_query.rb b/app/services/hyrax/quick_classification_query.rb deleted file mode 100644 index 278c11b8ef..0000000000 --- a/app/services/hyrax/quick_classification_query.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hryax v3.4.0 -require_dependency Hyrax::Engine.root.join('app', 'services', 'hyrax', 'quick_classification_query').to_s - -Hyrax::QuickClassificationQuery.class_eval do - # OVERRIDE: only use work types that are enabled in the current tenant - # @param [::User] user the current user - # @param [#call] concern_name_normalizer (String#constantize) a proc that translates names to classes - # @param [Array<String>] models the options to display, defaults to everything. - def initialize(user, - models: Site.instance.available_works, - concern_name_normalizer: ->(str) { str.constantize }) - @user = user - @concern_name_normalizer = concern_name_normalizer - @models = models - end - - # OVERRIDE: only use work types that are enabled in the current tenant - # - # @return true if the requested concerns is same as all avaliable concerns - def all? - # OVERRIDE: use Site.instance.available_works instead of Hyrax.config.registered_curation_concern_types - models == Site.instance.available_works - end -end diff --git a/app/services/hyrax/quick_classification_query_decorator.rb b/app/services/hyrax/quick_classification_query_decorator.rb new file mode 100644 index 0000000000..34dfcbb761 --- /dev/null +++ b/app/services/hyrax/quick_classification_query_decorator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# OVERRIDE Hryax v5.0.0rc2 + +module Hyrax + module QuickClassificationQueryDecorator + # OVERRIDE: only use work types that are enabled in the current tenant + # @param [::User] user the current user + # @param [#call] concern_name_normalizer (String#constantize) a proc that translates names to classes + # @param [Array<String>] models the options to display, defaults to everything. + def initialize(user, models: Site.instance.available_works, **kwargs) + super(user, **kwargs.merge(models:)) + end + + # OVERRIDE: only use work types that are enabled in the current tenant + # + # @return true if the requested concerns is same as all avaliable concerns + def all? + # OVERRIDE: use Site.instance.available_works instead of Hyrax.config.registered_curation_concern_types + models == Site.instance.available_works + end + end +end + +Hyrax::QuickClassificationQuery.prepend(Hyrax::QuickClassificationQueryDecorator) diff --git a/app/services/hyrax/thumbnail_path_service.rb b/app/services/hyrax/thumbnail_path_service.rb deleted file mode 100644 index cccadc9a55..0000000000 --- a/app/services/hyrax/thumbnail_path_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 - use site defaults instead of app wide defaults -module Hyrax - class ThumbnailPathService - class << self - # @param [#id] object - to get the thumbnail for - # @return [String] a path to the thumbnail - def call(object) - return default_image if object.try(:thumbnail_id).blank? - - thumb = fetch_thumbnail(object) - - return default_image unless thumb - return call(thumb) unless thumb.file_set? - return_path(thumb) - end - - private - - def return_path(thumb) - if audio?(thumb) - audio_image - elsif thumbnail?(thumb) - thumbnail_path(thumb) - else - default_image - end - end - - def audio?(thumb) - service = thumb.respond_to?(:audio?) ? thumb : Hyrax::FileSetTypeService.new(file_set: thumb) - service.audio? - end - - def fetch_thumbnail(object) - return object if object.thumbnail_id == object.id - Hyrax.query_service.find_by(id: object.thumbnail_id) - rescue Valkyrie::Persistence::ObjectNotFoundError, Hyrax::ObjectNotFoundError - Rails.logger.error("Couldn't find thumbnail #{object.thumbnail_id} for #{object.id}") - nil - end - - # @return the network path to the thumbnail - # @param [FileSet] thumbnail the object that is the thumbnail - def thumbnail_path(thumbnail) - Hyrax::Engine.routes.url_helpers.download_path(thumbnail.id, - file: 'thumbnail') - end - - def default_image - Site.instance.default_work_image&.url || ActionController::Base.helpers.image_path('default.png') - end - - def audio_image - ActionController::Base.helpers.image_path 'audio.png' - end - - # @return true if there a file on disk for this object, otherwise false - # @param [FileSet] thumb - the object that is the thumbnail - def thumbnail?(thumb) - File.exist?(thumbnail_filepath(thumb)) - end - - # @param [FileSet] thumb - the object that is the thumbnail - def thumbnail_filepath(thumb) - Hyrax::DerivativePath.derivative_path_for_reference(thumb, 'thumbnail') - end - end - end -end diff --git a/app/services/hyrax/thumbnail_path_service_decorator.rb b/app/services/hyrax/thumbnail_path_service_decorator.rb new file mode 100644 index 0000000000..4cbc7d8b9a --- /dev/null +++ b/app/services/hyrax/thumbnail_path_service_decorator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 - use site defaults instead of app wide defaults + +module Hyrax + module ThumbnailPathServiceDecorator + def default_image + Site.instance.default_work_image&.url || super + end + end +end + +Hyrax::ThumbnailPathService.singleton_class.send(:prepend, Hyrax::ThumbnailPathServiceDecorator) diff --git a/app/services/hyrax/work_thumbnail_path_service.rb b/app/services/hyrax/work_thumbnail_path_service.rb deleted file mode 100644 index 55d4550966..0000000000 --- a/app/services/hyrax/work_thumbnail_path_service.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax 3.4.0 use site defaults over app default -module Hyrax - class WorkThumbnailPathService < Hyrax::ThumbnailPathService - class << self - def default_image - Site.instance.default_work_image&.url || ActionController::Base.helpers.image_path('work.png') - end - end - end -end diff --git a/app/services/hyrax/work_thumbnail_path_service_decorator.rb b/app/services/hyrax/work_thumbnail_path_service_decorator.rb new file mode 100644 index 0000000000..1a021dfee1 --- /dev/null +++ b/app/services/hyrax/work_thumbnail_path_service_decorator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v5.0.0rc2 use site defaults over app default + +module Hyrax + module WorkThumbnailPathServiceDecorator + def default_image + Site.instance.default_work_image&.url || super + end + end +end + +Hyrax::WorkThumbnailPathService.singleton_class.send(:prepend, Hyrax::WorkThumbnailPathServiceDecorator) diff --git a/app/services/hyrax/workflow/permission_grantor.rb b/app/services/hyrax/workflow/permission_grantor.rb index ed93c054ac..75b3a7bb03 100644 --- a/app/services/hyrax/workflow/permission_grantor.rb +++ b/app/services/hyrax/workflow/permission_grantor.rb @@ -12,7 +12,7 @@ module Workflow # @see RolesService class PermissionGrantor def self.grant_default_workflow_roles!(permission_template:, creating_user: nil) - new(permission_template: permission_template, creating_user: creating_user).call + new(permission_template:, creating_user:).call end attr_accessor :permission_template, :creating_user @@ -36,64 +36,64 @@ def call private - # Force creation of registered roles if they don't exist - def register_default_sipity_roles! - Sipity::Role[Hyrax::RoleRegistry::MANAGING] - Sipity::Role[Hyrax::RoleRegistry::APPROVING] - Sipity::Role[Hyrax::RoleRegistry::DEPOSITING] - end + # Force creation of registered roles if they don't exist + def register_default_sipity_roles! + Sipity::Role[Hyrax::RoleRegistry::MANAGING] + Sipity::Role[Hyrax::RoleRegistry::APPROVING] + Sipity::Role[Hyrax::RoleRegistry::DEPOSITING] + end - def grant_all_workflow_roles_to_creating_user_and_admins! - # The admin group should always receive workflow roles - workflow_agents = [Hyrax::Group.find_by!(name: ::Ability.admin_group_name)] - # The default admin set does not have a creating user - workflow_agents << creating_user if creating_user - workflow_agents |= Hyrax::Group.select { |g| g.has_site_role?(:admin) }.tap do |agent_list| - ::User.find_each do |u| - agent_list << u if u.has_role?(:admin, Site.instance) - end + def grant_all_workflow_roles_to_creating_user_and_admins! + # The admin group should always receive workflow roles + workflow_agents = [Hyrax::Group.find_by!(name: ::Ability.admin_group_name)] + # The default admin set does not have a creating user + workflow_agents << creating_user if creating_user + workflow_agents |= Hyrax::Group.select { |g| g.site_role?(:admin) }.tap do |agent_list| + ::User.find_each do |u| + agent_list << u if u.has_role?(:admin, Site.instance) end - - grant_workflow_roles!(workflow_agents: workflow_agents, role_filters: nil) end - def grant_workflow_roles_to_editors! - editor_sipity_roles = [Hyrax::RoleRegistry::APPROVING, Hyrax::RoleRegistry::DEPOSITING] - workflow_agents = Hyrax::Group.select { |g| g.has_site_role?(:work_editor) }.tap do |agent_list| - ::User.find_each do |u| - agent_list << u if u.has_role?(:work_editor, Site.instance) - end - end + grant_workflow_roles!(workflow_agents:, role_filters: nil) + end - grant_workflow_roles!(workflow_agents: workflow_agents, role_filters: editor_sipity_roles) + def grant_workflow_roles_to_editors! + editor_sipity_roles = [Hyrax::RoleRegistry::APPROVING, Hyrax::RoleRegistry::DEPOSITING] + workflow_agents = Hyrax::Group.select { |g| g.site_role?(:work_editor) }.tap do |agent_list| + ::User.find_each do |u| + agent_list << u if u.has_role?(:work_editor, Site.instance) + end end - def grant_workflow_roles_to_depositors! - depositor_sipity_role = [Hyrax::RoleRegistry::DEPOSITING] - workflow_agents = Hyrax::Group.select { |g| g.has_site_role?(:work_depositor) }.tap do |agent_list| - ::User.find_each do |u| - agent_list << u if u.has_role?(:work_depositor, Site.instance) - end - end + grant_workflow_roles!(workflow_agents:, role_filters: editor_sipity_roles) + end - grant_workflow_roles!(workflow_agents: workflow_agents, role_filters: depositor_sipity_role) + def grant_workflow_roles_to_depositors! + depositor_sipity_role = [Hyrax::RoleRegistry::DEPOSITING] + workflow_agents = Hyrax::Group.select { |g| g.site_role?(:work_depositor) }.tap do |agent_list| + ::User.find_each do |u| + agent_list << u if u.has_role?(:work_depositor, Site.instance) + end end - def grant_workflow_roles!(workflow_agents:, role_filters:) - role_set = if role_filters.present? - Sipity::Role.select { |role| role_filters.include?(role.name) } - else - Sipity::Role.all - end + grant_workflow_roles!(workflow_agents:, role_filters: depositor_sipity_role) + end + + def grant_workflow_roles!(workflow_agents:, role_filters:) + role_set = if role_filters.present? + Sipity::Role.select { |role| role_filters.include?(role.name) } + else + Sipity::Role.all + end - permission_template.available_workflows.each do |workflow| - role_set.each do |role| - Hyrax::Workflow::PermissionGenerator.call(roles: role, - workflow: workflow, - agents: workflow_agents) - end + permission_template.available_workflows.each do |workflow| + role_set.each do |role| + Hyrax::Workflow::PermissionGenerator.call(roles: role, + workflow:, + agents: workflow_agents) end end + end end end end diff --git a/app/services/hyrax/workflow/permission_query.rb b/app/services/hyrax/workflow/permission_query.rb deleted file mode 100644 index 8ac3f2ecd3..0000000000 --- a/app/services/hyrax/workflow/permission_query.rb +++ /dev/null @@ -1,450 +0,0 @@ -# frozen_string_literal: true - -# OVERRIDE Hyrax v3.4.2 Expand functionality for Groups with Roles Feature -# @see https://github.com/samvera/hyku/wiki/Groups-with-Roles-Feature -# rubocop:disable Metrics/ModuleLength -module Hyrax - module Workflow - # Welcome intrepid developer. You have stumbled into some complex data - # interactions. There are a lot of data collaborators regarding these tests. - # I would love this to be more in isolation, but that is not in the cards as - # there are at least 16 database tables interacting to ultimately answer the - # following question: - # - # * What actions can a given user take on an entity? - # - # Could there be more efficient queries? Yes. However, the composition of - # queries has proven to be a very powerful means of understanding and - # exploring the problem. - # - # @note There is an indication of public or private api. The intent of this - # is to differentiate what are methods that are the primary entry points - # as understood as of the commit that has the @api tag. However, these are - # public methods because they have been tested in isolation and are used - # to help compose the `@api public` methods. - # - # @todo Refactor the large ABC methods in this module. - module PermissionQuery - module_function - - def entity_responsibilities - @entity_responsibilities ||= Sipity::EntitySpecificResponsibility.arel_table - end - - def workflow_responsibilities - @workflow_responsibilities ||= Sipity::WorkflowResponsibility.arel_table - end - - def workflow_roles - @workflow_roles ||= Sipity::WorkflowRole.arel_table - end - - # @api public - # - # For the given :user and :entity return only workflow actions that meet all of the following: - # - # * available for the :entity's workflow state - # * permitted to be taken by one or more roles in which the user is assigned - # either at the workflow level or the entity level. - # - # * Actions to which the user Only actions permitted to the user - # - # @param user [User] - # @param entity [Object] an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowAction>] - def scope_permitted_workflow_actions_available_for_current_state(user:, entity:) - workflow_actions_scope = scope_workflow_actions_available_for_current_state(entity: entity) - workflow_state_actions_scope = scope_permitted_entity_workflow_state_actions(user: user, entity: entity) - workflow_actions_scope.where( - workflow_actions_scope.arel_table[:id].in( - workflow_state_actions_scope.arel_table.project( - workflow_state_actions_scope.arel_table[:workflow_action_id] - ).where(workflow_state_actions_scope.arel.constraints.reduce) - ) - ) - end - - # @api public - # - # Agents associated with the given :entity and how they are associated - # with the given. - # - # @param entity [Object] that can be converted into a Sipity::Entity - # @param role [Object] that can be converted into a Sipity::Role - # @return [ActiveRecord::Relation<Sipity::Agent>] augmented with - # - def scope_agents_associated_with_entity_and_role(entity:, role:) # rubocop:disable Metrics/AbcSize - entity = Sipity::Entity(entity) - role = Sipity::Role(role) - - agents = Sipity::Agent.arel_table - - agents_select_manager = agents.project( - :*, - Arel.sql("'#{Sipity::Agent::ENTITY_LEVEL_AGENT_RELATIONSHIP}'").as('agent_processing_relationship') - ).where( - agents[:id].in( - entity_responsibilities.project(entity_responsibilities[:agent_id]).join(workflow_roles).on( - workflow_roles[:id].eq(entity_responsibilities[:workflow_role_id]) - ).where( - entity_responsibilities[:entity_id].eq(entity.id).and( - workflow_roles[:role_id].eq(role.id) - ) - ) - ) - ).union( - agents.project( - :*, - Arel.sql("'#{Sipity::Agent::WORKFLOW_LEVEL_AGENT_RELATIONSHIP}'").as('agent_processing_relationship') - ).where( - agents[:id].in( - workflow_responsibilities.project(workflow_responsibilities[:agent_id]).join(workflow_roles).on( - workflow_roles[:id].eq(workflow_responsibilities[:workflow_role_id]) - ).where( - workflow_roles[:workflow_id].eq(entity.workflow_id).and( - workflow_roles[:role_id].eq(role.id) - ) - ) - ) - ) - ) - # I would love to use the following: - # `Agent.find_by_sql(agents_select_manager.to_sql)` - # - # However AREL is adding an opening and closing parenthesis to the query - # statement. So I needed to massage that output, as follows: - # - # ```ruby - # Agent.find_by_sql( - # agents_select_manager.to_sql.sub(/\A\s*\(\s*(.*)\s*\)\s*\Z/,'\1') - # ) - # ``` - # - # Instead I'm taking an example from: - # https://github.com/danshultz/mastering_active_record_sample_code/blob/a656c60ca7a2e27b5cd1aadbdf3bdc1814c37000/app/models/beer.rb#L77-L81 - # - # Note, I'm making a dynamic query with a result the same as the table - # name of the model that I'm using. - Sipity::Agent.from(agents.create_table_alias(agents_select_manager, agents.table_name)).all - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - - # @api public - # - # Roles associated with the given :entity - # @param entity [Object] that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::Role>] - def scope_roles_associated_with_the_given_entity(entity:) - entity = Sipity::Entity(entity) - return Sipity::Role.none unless entity - Sipity::Role.where( - Sipity::Role.arel_table[:id].in( - workflow_roles.project(workflow_roles[:role_id]).where( - workflow_roles[:workflow_id].eq(entity.workflow_id) - ) - ) - ) - end - - # @api public - # - # Is the user authorized to take the processing action on the given - # entity? - # - # @param user [User] - # @param entity an object that can be converted into a Sipity::Entity - # @param action an object that can be converted into a Sipity::WorkflowAction#name - # @return [Boolean] - def authorized_for_processing?(user:, entity:, action:) - action_name = Sipity::WorkflowAction.name_for(action) - scope_permitted_workflow_actions_available_for_current_state(user: user, entity: entity) - .find_by(Sipity::WorkflowAction.arel_table[:name].eq(action_name)).present? - end - - # @api public - # - # An ActiveRecord::Relation scope that meets the following criteria: - # - # * All of the Processing Agents directly associated with the given :user - # - # @param user [User] - # @return [ActiveRecord::Relation<Sipity::Agent>] - def scope_processing_agents_for(user:) - return Sipity::Agent.none if user.blank? - return Sipity::Agent.none unless user.persisted? - user_agent = Sipity::Agent(user) - group_agents = user.groups.map do |g| - Sipity::Agent(Hyrax::Group.new(name: g)) - end - Sipity::Agent.where(id: group_agents + [user_agent]) - end - - PermissionScope = Struct.new(:entity, :workflow) - private_constant :PermissionScope - - # @api public - # - # An ActiveRecord::Relation scope that meets the following criteria: - # - # * Sipity::Entity in a state in which I have access to based on: - # - The entity specific responsibility - # - For which I've been assigned a role - # - The workflow specific responsibility - # - For which I've been assigned a role - # - # @param [User] user - # - # @return [ActiveRecord::Relation<Sipity::Entity>] - # - def scope_entities_for_the_user(user:) # rubocop:disable Metrics/AbcSize - entities = Sipity::Entity.arel_table - workflow_state_actions = Sipity::WorkflowStateAction.arel_table - workflow_states = Sipity::WorkflowState.arel_table - workflow_state_action_permissions = Sipity::WorkflowStateActionPermission.arel_table - - user_agent_scope = scope_processing_agents_for(user: user) - user_agent_contraints = user_agent_scope.arel_table.project( - user_agent_scope.arel_table[:id] - ).where(user_agent_scope.arel.constraints) - - join_builder = lambda do |responsibility| - entities.project( - entities[:id] - ).join(workflow_state_actions).on( - workflow_state_actions[:originating_workflow_state_id].eq(entities[:workflow_state_id]) - ).join(workflow_state_action_permissions).on( - workflow_state_action_permissions[:workflow_state_action_id].eq(workflow_state_actions[:id]) - ).join(workflow_states).on( - workflow_states[:id].eq(workflow_state_actions[:originating_workflow_state_id]) - ).join(responsibility).on( - responsibility[:workflow_role_id].eq(workflow_state_action_permissions[:workflow_role_id]) - ) - end - - where_builder = ->(responsibility) { responsibility[:agent_id].in(user_agent_contraints) } - - entity_specific_joins = join_builder.call(entity_responsibilities) - workflow_specific_joins = join_builder.call(workflow_responsibilities) - - entity_specific_where = where_builder.call(entity_responsibilities).and( - entities[:id].eq(entity_responsibilities[:entity_id]) - ) - workflow_specific_where = where_builder.call(workflow_responsibilities) - - Sipity::Entity.where( - entities[:id].in(entity_specific_joins.where(entity_specific_where)) - .or(entities[:id].in(workflow_specific_joins.where(workflow_specific_where))) - ) - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - - # @api public - # - # An ActiveRecord::Relation scope that meets the following criteria: - # - # * Users that are directly associated with the given entity through one or - # more of the given roles within the entity's workflow - # * Users that are indirectly associated with the given entity by group - # and role. - # - # @param roles [Sipity::Role] - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<User>] - # - def scope_users_for_entity_and_roles(entity:, roles:) # rubocop:disable Metrics/AbcSize - entity = Sipity::Entity(entity) - role_ids = Array.wrap(roles).map { |role| Sipity::Role(role).id } - user_polymorphic_type = ::User.base_class - - user_table = ::User.arel_table - agent_table = Sipity::Agent.arel_table - - workflow_role_id_subquery = workflow_roles.project(workflow_roles[:id]).where( - workflow_roles[:workflow_id].eq(entity.workflow_id).and(workflow_roles[:role_id].in(role_ids)) - ) - - workflow_agent_id_subquery = workflow_responsibilities.project(workflow_responsibilities[:agent_id]).where( - workflow_responsibilities[:workflow_role_id].in(workflow_role_id_subquery) - ) - - entity_agent_id_subquery = entity_responsibilities.project(entity_responsibilities[:agent_id]).where( - entity_responsibilities[:workflow_role_id].in(workflow_role_id_subquery) - .and(entity_responsibilities[:entity_id].eq(entity.id)) - ) - - # Default to "integer" for adapters like Postgres and Sqlite, but cast - # to "signed" for Mysql. The type CAST causes a SQL syntax error for an - # unsupported type depending on which adapter is being used. - type = ActiveRecord::Base.connection.adapter_name.casecmp("mysql2").zero? ? "signed" : "integer" - cast = Arel::Nodes::NamedFunction.new "CAST", [agent_table[:proxy_for_id].as(type)] - - sub_query_for_user = agent_table.project(cast).where( - agent_table[:id].in(workflow_agent_id_subquery) - .or(agent_table[:id].in(entity_agent_id_subquery)) - ).where( - agent_table[:proxy_for_type].eq(user_polymorphic_type) - ) - - ::User.where(user_table[:id].in(sub_query_for_user)) - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - - def user_emails_for_entity_and_roles(entity:, roles:) - scope_users_for_entity_and_roles(entity: entity, roles: roles).pluck(:email) - end - - # @api public - # - # For the given :user and :entity, return an ActiveRecord::Relation that, - # if resolved, will be all of the assocated workflow roles for both the - # workflow responsibilities and the entity specific responsibilities. - # - # @param user [User] - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowRole>] - def scope_processing_workflow_roles_for_user_and_entity(user:, entity:) - entity = Sipity::Entity(entity) - workflow_scope = scope_processing_workflow_roles_for_user_and_workflow(user: user, workflow: entity.workflow) - - entity_specific_scope = scope_processing_workflow_roles_for_user_and_entity_specific(user: user, entity: entity) - Sipity::WorkflowRole.where( - workflow_scope.arel.constraints.reduce.or(entity_specific_scope.arel.constraints.reduce) - ) - end - - # @api private - # - # For the given :user and :workflow, return an ActiveRecord::Relation that, - # if resolved, will be all of the assocated workflow roles that are - # assigned to directly to the workflow. - # - # @param user [User] - # @param workflow [Sipity::Workflow] - # @return [ActiveRecord::Relation<Sipity::WorkflowRole>] - def scope_processing_workflow_roles_for_user_and_workflow(user:, workflow:) - agent_constraints = scope_processing_agents_for(user: user) - workflow_role_subquery = workflow_roles[:id].in( - workflow_responsibilities.project(workflow_responsibilities[:workflow_role_id]) - .where( - workflow_responsibilities[:agent_id].in( - agent_constraints.arel_table.project( - agent_constraints.arel_table[:id] - ).where(agent_constraints.arel.constraints) - ) - ) - ) - Sipity::WorkflowRole.where( - workflow_roles[:workflow_id].eq(workflow.id).and(workflow_role_subquery) - ) - end - - # @api private - # - # For the given :user and :entity, return an ActiveRecord::Relation that, - # if resolved, will be all of the assocated workflow roles that are - # assigned to specifically to the entity (and not the parent workflow). - # - # @param user [User] - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowRole>] - def scope_processing_workflow_roles_for_user_and_entity_specific(user:, entity:) - entity = Sipity::Entity(entity) - agent_scope = scope_processing_agents_for(user: user) - - Sipity::WorkflowRole.where( - workflow_roles[:id].in( - entity_responsibilities.project(entity_responsibilities[:workflow_role_id]) - .where( - entity_responsibilities[:agent_id].in( - agent_scope.arel_table.project( - agent_scope.arel_table[:id] - ).where( - agent_scope.arel.constraints.reduce.and(entity_responsibilities[:entity_id].eq(entity.id)) - ) - ) - ) - ) - ) - end - - # @api private - # - # For the given :user and :entity, return an ActiveRecord::Relation, - # that if resolved, will be collection of - # Sipity::WorkflowStateAction object to which the user has - # permission to do something. - # - # An ActiveRecord::Relation scope that meets the following criteria: - # - # * The actions are available for the given entity's current state - # * The actions are available for the given user based on their role. - # Either: - # - Directly via an agent associated with a user - # - Indirectly via an agent associated with a group - # - # @param user [User] - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowStateAction>] - # - def scope_permitted_entity_workflow_state_actions(user:, entity:) - entity = Sipity::Entity(entity) - workflow_state_actions = Sipity::WorkflowStateAction - permissions = Sipity::WorkflowStateActionPermission - role_scope = scope_processing_workflow_roles_for_user_and_entity(user: user, entity: entity) - - workflow_state_actions.where( - workflow_state_actions.arel_table[:originating_workflow_state_id].eq(entity.workflow_state_id).and( - workflow_state_actions.arel_table[:id].in( - permissions.arel_table.project( - permissions.arel_table[:workflow_state_action_id] - ).where( - permissions.arel_table[:workflow_role_id].in( - role_scope.arel_table.project(role_scope.arel_table[:id]).where( - role_scope.arel.constraints.reduce - ) - ) - ) - ) - ) - ) - end - - # @api public - # - # For the given :entity return an ActiveRecord::Relation that when - # resolved will be only the workflow actions that: - # - # * Are available for the entity's workflow_state - # - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowAction>] - def scope_workflow_actions_for_current_state(entity:) - entity = Sipity::Entity(entity) - state_actions_table = Sipity::WorkflowStateAction.arel_table - Sipity::WorkflowAction.where( - Sipity::WorkflowAction.arel_table[:id].in( - state_actions_table.project(state_actions_table[:workflow_action_id]) - .where(state_actions_table[:originating_workflow_state_id].eq(entity.workflow_state_id)) - ) - ) - end - - # @api private - # - # For the given :entity, return an ActiveRecord::Relation, that - # if resolved, that lists all of the actions available for the entity and - # its current state. - # - # * All actions that are associated with actions that do not have prerequsites - # * All actions that have prerequisites and all of those prerequisites are complete - # - # @param entity an object that can be converted into a Sipity::Entity - # @return [ActiveRecord::Relation<Sipity::WorkflowAction>] - def scope_workflow_actions_available_for_current_state(entity:) - workflow_actions_for_current_state = scope_workflow_actions_for_current_state(entity: entity) - Sipity::WorkflowAction.where(workflow_actions_for_current_state.arel.constraints.reduce) - end - end - end -end -# rubocop:enable Metrics/ModuleLength diff --git a/app/services/roles_service.rb b/app/services/roles_service.rb index c5c6a5de8e..3d4b57f678 100644 --- a/app/services/roles_service.rb +++ b/app/services/roles_service.rb @@ -71,15 +71,13 @@ def create_default_roles! return '`AccountElevator.switch!` into an Account before creating default Roles' if Site.instance.is_a?(NilSite) DEFAULT_ROLES.each do |role_name| - find_or_create_site_role!(role_name: role_name) + find_or_create_site_role!(role_name:) end end def create_default_hyrax_groups_with_roles! # Prevent Hyrax::Groups from being created in the public schema - if Site.instance.is_a?(NilSite) - return '`AccountElevator.switch!` into an Account before creating default Hyrax::Groups' - end + return '`AccountElevator.switch!` into an Account before creating default Hyrax::Groups' if Site.instance.is_a?(NilSite) default_hyrax_groups_with_roles = DEFAULT_HYRAX_GROUPS_WITH_ATTRIBUTES.deep_merge(DEFAULT_ROLES_FOR_DEFAULT_HYRAX_GROUPS) @@ -87,12 +85,12 @@ def create_default_hyrax_groups_with_roles! default_hyrax_groups_with_roles.each do |group_name, group_attrs| group_roles = group_attrs.delete(:roles) group = Hyrax::Group.find_or_create_by!(name: group_name) - group.update_attributes(group_attrs) + group.update(group_attrs) group_roles.each do |role_name| next if role_name.blank? - group.roles |= [find_or_create_site_role!(role_name: role_name)] + group.roles |= [find_or_create_site_role!(role_name:)] end end end @@ -100,6 +98,7 @@ def create_default_hyrax_groups_with_roles! # Because each collection role has some level of access to every Collection within a tenant, # creating a Hyrax::PermissionTemplateAccess record (combined with Ability#user_groups) # means all Collections will show up in Blacklight / Solr queries. + # rubocop:disable Metrics/MethodLength def create_collection_accesses! Collection.find_each do |c| pt = Hyrax::PermissionTemplate.find_or_create_by!(source_id: c.id) @@ -129,12 +128,14 @@ def create_collection_accesses! agent_id: 'collection_reader' ) - c.reset_access_controls! if pt.access_grants.count != original_access_grants_count + pt.reset_access_controls_for(collection: c) if pt.access_grants.count != original_access_grants_count end end + # rubocop:enable Metrics/MethodLength # Creating a Hyrax::PermissionTemplateAccess record (combined with Ability#user_groups) # will allow Works in all AdminSets to show up in Blacklight / Solr queries. + # rubocop:disable Metrics/MethodLength def create_admin_set_accesses! AdminSet.find_each do |as| pt = Hyrax::PermissionTemplate.find_or_create_by!(source_id: as.id) @@ -164,9 +165,10 @@ def create_admin_set_accesses! agent_id: 'work_editor' ) - as.reset_access_controls! if pt.access_grants.count != original_access_grants_count + pt.reset_access_controls_for(collection: as) if pt.access_grants.count != original_access_grants_count end end + # rubocop:enable Metrics/MethodLength # Because some of the collection roles have access to every Collection within a tenant, create a # Hyrax::CollectionTypeParticipant record for them on every Hyrax::CollectionType (except the AdminSet) @@ -239,7 +241,7 @@ def prune_stale_guest_users stale_guest_users = User.unscoped.where( 'guest = ? and updated_at < ?', true, - Time.current - 7.days + 7.days.ago ) progress = ProgressBar.create(total: stale_guest_users.count) @@ -249,9 +251,11 @@ def prune_stale_guest_users end end + # rubocop:disable Metrics/MethodLength def seed_superadmin! + # rubocop:disable Rails/UnknownEnv return 'Seed data should not be used in the production environment' if Rails.env.production? || Rails.env.staging? - + # rubocop:enable Rails/UnknownEnv user = User.where(email: 'admin@example.com').first_or_initialize do |u| if u.new_record? u.password = 'testing123' @@ -272,10 +276,13 @@ def seed_superadmin! user end + # rubocop:enable Metrics/MethodLength + # rubocop:disable Metrics/MethodLength def seed_qa_users! + # rubocop:disable Rails/UnknownEnv return 'Seed data should not be used in the production environment' if Rails.env.production? || Rails.env.staging? - + # rubocop:disable Rails/UnknownEnv ActiveRecord::Base.transaction do DEFAULT_ROLES.each do |role_name| user = User.where(email: "#{role_name}@example.com").first_or_initialize do |u| @@ -292,7 +299,7 @@ def seed_qa_users! unless user.has_role?(role_name, Site.instance) user.add_default_group_membership! - user.roles << find_or_create_site_role!(role_name: role_name) + user.roles << find_or_create_site_role!(role_name:) end end @@ -300,5 +307,6 @@ def seed_qa_users! end end end + # rubocop:enable Metrics/MethodLength end end diff --git a/app/services/uploaded_collection_thumbnail_path_service.rb b/app/services/uploaded_collection_thumbnail_path_service.rb index 18a3f662eb..3685f9bb93 100644 --- a/app/services/uploaded_collection_thumbnail_path_service.rb +++ b/app/services/uploaded_collection_thumbnail_path_service.rb @@ -7,14 +7,12 @@ def call(object) "/uploads/uploaded_collection_thumbnails/#{object.id}/#{object.id}_card.jpg" end - # rubocop:disable Metrics/LineLength, Rails/FilePath, Lint/StringConversionInInterpolation def uploaded_thumbnail?(collection) - File.exist?("#{Rails.root.to_s}/public/uploads/uploaded_collection_thumbnails/#{collection.id}/#{collection.id}_card.jpg") + File.exist?(File.join(upload_dir(collection), "#{collection.id}_card.jpg")) end def upload_dir(collection) - "#{Rails.root.to_s}/public/uploads/uploaded_collection_thumbnails/#{collection.id}" + Hyku::Application.path_for("public/uploads/uploaded_collection_thumbnails/#{collection.id}") end - # rubocop:enable Metrics/LineLength, Rails/FilePath, Lint/StringConversionInInterpolation end end diff --git a/app/views/_admin_util_links.html.erb b/app/views/_admin_util_links.html.erb index 808e1230e0..86a76ae03d 100644 --- a/app/views/_admin_util_links.html.erb +++ b/app/views/_admin_util_links.html.erb @@ -1,22 +1,24 @@ <% if user_signed_in? %> <% if can? :manage, Account %> - <ul class="nav navbar-nav"> - <li> - <%= link_to main_app.proprietor_accounts_path do %> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <%= link_to main_app.proprietor_accounts_path, class: 'nav-link' do %> <span class="fa fa-gears"></span> <%= t("hyku.proprietor.accounts.nav") %> <% end %> </li> - <li> - <%= link_to main_app.proprietor_users_path do %> + <li class="nav-item"> + <%= link_to main_app.proprietor_users_path, class: 'nav-link' do %> <span class="fa fa-users"></span> <%= t("hyku.proprietor.users.nav") %> <% end %> </li> </ul> <% end %> <% end %> -<ul id="user_utility_links" class="nav navbar-nav navbar-right"> +<ul id="user_utility_links" class="navbar-nav ml-auto"> <%= render 'shared/locale_picker' if available_translations.size > 1 %> <% if user_signed_in? %> - <li><%= link_to t("hyrax.toolbar.profile.logout"), main_app.destroy_user_session_path %></li> + <li class="nav-item"> + <%= link_to t("hyrax.toolbar.profile.logout"), main_app.destroy_user_session_path, class: 'nav-link' %> + </li> <% end %> </ul> diff --git a/app/views/_logo.html.erb b/app/views/_logo.html.erb index 3776707b48..08eca88bde 100644 --- a/app/views/_logo.html.erb +++ b/app/views/_logo.html.erb @@ -1,10 +1,10 @@ - <% if logo_image %> - <a id="logo" href="<%= hyrax.root_path %>" data-no-turbolink="true"> - <span class="glyphicon glyphicon-globe" role="img" aria-label="<%= application_name %>" aria-hidden="true"></span> - <%= image_tag logo_image, alt: block_for(name: 'logo_image_text') %> - </a> - <% else %> - <a id="logo" class="navbar-brand" href="<%= hyrax.root_path %>" data-no-turbolink="true"> - <span class="institution_name"><%= application_name %></span> - </a> - <% end %> +<% if logo_image %> + <a id="logo" href="<%= hyrax.root_path %>" data-no-turbolink="true"> + <i class="fa fa-globe" role="img" aria-label="<%= application_name %>" aria-hidden="true"></i> + <%= image_tag logo_image, alt: block_for(name: 'logo_image_text') %> + </a> +<% else %> + <a id="logo" class="navbar-brand" href="<%= hyrax.root_path %>" data-no-turbolink="true"> + <span class="institution_name"><%= application_name %></span> + </a> +<% end %> diff --git a/app/views/_masthead.html.erb b/app/views/_masthead.html.erb index 7c41b19293..bd3892fb84 100644 --- a/app/views/_masthead.html.erb +++ b/app/views/_masthead.html.erb @@ -1,22 +1,18 @@ -<nav id="masthead" class="navbar navbar-inverse navbar-static-top" role="navigation" aria-label="masthead"> - <div class="container-fluid"> - <!-- Brand and toggle get grouped for better mobile display --> - <div class="navbar-header"> - <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#top-navbar-collapse" aria-expanded="false"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <%= render '/logo' %> - </div> +<%# OVERRIDE Hyrax v5.0.0rc1 to render either admin or user util links %> - <div class="collapse navbar-collapse" id="top-navbar-collapse"> +<header aria-label="header" class="top-header"> + <nav id="masthead" class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between <%= placement_class %>" role="navigation" aria-label="masthead"> + <h1 class="sr-only"><%= application_name %></h1> + <%= render '/logo' %> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#top-navbar-collapse" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse justify-content-end" id="top-navbar-collapse"> <% if admin_host? %> <%= render '/admin_util_links' %> <% else %> <%= render '/user_util_links' %> <% end %> </div> - </div> -</nav> + </nav> +</header> diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 782fcb5f13..db04aca6d3 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -1,31 +1,29 @@ -<ul id="user_utility_links" class="nav navbar-nav navbar-right"> +<ul id="user_utility_links" class="navbar-nav ml-auto"> <%= render 'shared/locale_picker' if available_translations.size > 1 %> <% if user_signed_in? %> - <li> + <li class="nav-item"> <%= render_notifications(user: current_user) %> </li> - <li class="dropdown"> - <%= link_to hyrax.dashboard_profile_path(current_user), role: 'button', data: { toggle: 'dropdown' }, aria: { haspopup: true, expanded: false, controls: 'user-util-links' } do %> + <li class="nav-item dropdown"> + <%= link_to hyrax.dashboard_profile_path(current_user), class: 'nav-link dropdown-toggle', id: 'navbarDropdown', role: 'button', data: { toggle: 'dropdown' }, aria: { haspopup: true, expanded: false } do %> <span class="sr-only"><%= t("hyrax.toolbar.profile.sr_action") %></span> - <span class="hidden-xs"> <%= current_user.name %></span> + <%= current_user.name %> <span class="sr-only"> <%= t("hyrax.toolbar.profile.sr_target") %></span> - <span class="fa fa-user" aria-hidden="true"></span> - <span class="caret"></span> + <i class="fa fa-user" aria-hidden="true"></i> <% end %> - <ul id="user-util-links" class="dropdown-menu dropdown-menu-right" role="menu"> - <li><%= link_to t("hyrax.toolbar.dashboard.menu"), hyrax.dashboard_path %></li> - - <li class="divider"></li> - <% if Devise.mappings[:user]&.registerable? %> - <li><%= link_to t("hyku.toolbar.profile.edit_registration"), main_app.edit_user_registration_path %></li> + <div id="user-util-links" class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> + <a class="dropdown-item" href="<%= hyrax.dashboard_path %>"><%= t("hyrax.toolbar.dashboard.menu") %></a> + <div class="dropdown-divider"></div> + <% if Devise.mappings[:user]&.registerable? %> + <a class="dropdown-item" href="<%= main_app.edit_user_registration_path %>"><%= t("hyku.toolbar.profile.edit_registration") %></a> <% end %> - <li><%= link_to t("hyrax.toolbar.profile.logout"), main_app.destroy_user_session_path %></li> - </ul> - </li><!-- /.btn-group --> + <a class="dropdown-item" href="<%= main_app.destroy_user_session_path %>"><%= t("hyrax.toolbar.profile.logout") %></a> + </div> + </li> <% else %> - <li> - <%= link_to main_app.single_signon_index_path do %> - <span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> <%= t("hyrax.toolbar.profile.login") %> + <li class="nav-item"> + <%= link_to main_app.single_signon_index_path, class: 'nav-link' do %> + <i class="fa fa-sign-in" aria-hidden="true"></i> <%= t("hyrax.toolbar.profile.login") %> <% end %> </li> <% end %> diff --git a/app/views/account_sign_up/new.html.erb b/app/views/account_sign_up/new.html.erb index dcc07cd8f3..b636648132 100644 --- a/app/views/account_sign_up/new.html.erb +++ b/app/views/account_sign_up/new.html.erb @@ -4,7 +4,7 @@ <div class="row"> <div class="col-md-8"> - <%= form_for(@account, url: account_sign_up_path(@account), html: { class: 'form form-horizontal' }) do |f| %> + <%= form_for(@account, url: account_sign_up_path(@account), html: { class: 'form' }) do |f| %> <% if @account.errors.any? %> <div id="error_explanation" class="alert alert-danger"> <%= pluralize(@account.errors.count, "error") %> prohibited this repository from being saved: @@ -16,15 +16,15 @@ </div> <% end %> - <div class="form-group"> - <%= f.label :name, 'Short name', class: 'col-md-2 control-label' %> + <div class="form-group row"> + <%= f.label :name, 'Short name', class: 'col-md-2 col-form-label' %> <div class="col-md-10"> <%= f.text_field :name, class: 'form-control' %> - <span class="help-block">A single or hyphenated name used for technical aspects of the repository (e.g., "acme" or "acme-library").</span> + <small class="form-text text-muted">A single or hyphenated name used for technical aspects of the repository (e.g., "acme" or "acme-library").</small> </div> </div> - <%= f.submit class: 'btn btn-primary pull-right' %> + <%= f.submit class: 'btn btn-secondary float-right' %> <% end %> </div> </div> diff --git a/app/views/admin/accounts/edit.html.erb b/app/views/admin/accounts/edit.html.erb index e03fcb9fd7..533aa0a076 100644 --- a/app/views/admin/accounts/edit.html.erb +++ b/app/views/admin/accounts/edit.html.erb @@ -4,9 +4,9 @@ <div class="row"> <div class="col-md-12"> - <div class="panel panel-default account-form"> + <div class="card account-form"> <%= simple_form_for([:admin, @account], url: admin_account_path, :html => { class: 'form' }) do |f| %> - <div class="panel-body"> + <div class="card-body"> <% if @account.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@account.errors.count, "error") %> prohibited this account from being saved:</h2> @@ -24,11 +24,11 @@ </div> <% current_account.public_settings.each do |key, value| %> - <%= render 'shared/settings', f:f, key: key, value: value %> + <%= render 'shared/settings', f: f, key: key, value: value %> <% end %> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-secondary float-right' %> </div> </div> <% end %> diff --git a/app/views/admin/groups/_form.html.erb b/app/views/admin/groups/_form.html.erb index f7363b22ae..ad9af2a2be 100644 --- a/app/views/admin/groups/_form.html.erb +++ b/app/views/admin/groups/_form.html.erb @@ -5,15 +5,15 @@ target_path = admin_group_path(@group) end %> -<div class="panel panel-default"> +<div class="card"> <%= simple_form_for @group, url: target_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <%= f.input :humanized_name, label: t('hyku.admin.groups.label.humanized_name'), required: true %> <%= f.input :description, as: :text, label: t('hyku.admin.groups.label.description'), hint: '' %> </div> - <div class="panel-footer text-right"> + <div class="card-footer text-right"> <%= link_to t('simple_form.cancel'), admin_groups_path, class: 'btn btn-link action-cancel' %> - <%= f.submit class: 'btn btn-primary action-save' %> + <%= f.submit class: 'btn btn-secondary action-save' %> </div> <% end %> </div> diff --git a/app/views/admin/groups/_nav.html.erb b/app/views/admin/groups/_nav.html.erb index be08886683..17a25700c1 100644 --- a/app/views/admin/groups/_nav.html.erb +++ b/app/views/admin/groups/_nav.html.erb @@ -2,10 +2,10 @@ <div class="panel-tab-wrapper"> <ul class="nav nav-tabs"> <% navigation_presenter.tabs.each do |tab| %> - <%= content_tag :li, role: 'presentation', class: tab.css_class do %> - <%= link_to tab.name, tab.path %> - <% end %> + <li class="nav-item" role="presentation"> + <%= link_to tab.name, tab.path, class: "nav-link #{'active' if tab.css_class == 'active'}" %> + </li> <% end %> </ul> - <%= link_to t('hyku.admin.groups.nav.index'), admin_groups_path, class: 'pull-right nav-escape-link' %> + <%= link_to t('hyku.admin.groups.nav.index'), admin_groups_path, class: 'float-right nav-escape-link' %> </div> diff --git a/app/views/admin/groups/_per_page.html.erb b/app/views/admin/groups/_per_page.html.erb index a2182bc09d..4f9615dc96 100644 --- a/app/views/admin/groups/_per_page.html.erb +++ b/app/views/admin/groups/_per_page.html.erb @@ -5,7 +5,7 @@ <% if params[:page].present? %> <%= f.input :page, as: :hidden, input_html: { name: :page, value: params[:page] } %> <% end %> - <%= f.input :per, collection: [10, 25, 100], include_blank: false, required: false, selected: params[:per], input_html: { class: 'js-per-page__select', name: :per } %> + <%= f.input :per, collection: [10, 25, 100], include_blank: false, required: false, selected: params[:per], input_html: { class: 'js-per-page__select custom-select', name: :per } %> <%= t('hyku.admin.groups.label.per_page') %> - <%= f.submit class: 'btn btn-default js-per-page__submit' %> + <%= f.submit class: 'btn btn-secondary js-per-page__submit' %> <% end %> diff --git a/app/views/admin/groups/_search.html.erb b/app/views/admin/groups/_search.html.erb index 52fe58f832..26548adf5f 100644 --- a/app/views/admin/groups/_search.html.erb +++ b/app/views/admin/groups/_search.html.erb @@ -5,11 +5,11 @@ <% if params[:page].present? %> <%= f.input :page, as: :hidden, input_html: { name: :page, value: params[:page] } %> <% end %> - <div class="form-group list-scope"> - <%= f.input :q, required: false, input_html: { class: 'list-scope__query', name: :q, value: params[:q] }, wrapper_html: { class: 'list-scope__query-wrapper'} %> + <div class="form-group mb-2 list-scope"> + <%= f.input :q, required: false, input_html: { class: 'form-control list-scope__query', name: :q, value: params[:q] }, wrapper_html: { class: 'list-scope__query-wrapper'} %> <% if params[:q].present? %> - <%= link_to content_tag(:span, '×'.html_safe, :'aria-hidden' => true), @target, class: 'close list-scope__reset', :'aria-label' => t('hyku.admin.groups.action.search.clear') %> + <%= link_to content_tag(:span, '×'.html_safe, :'aria-hidden' => true), @target, class: 'btn btn-sm btn-secondary close list-scope__reset', :'aria-label' => t('hyku.admin.groups.action.search.clear') %> <% end %> </div> - <%= f.submit class: 'btn btn-primary list-scope__submit' %> + <%= f.submit class: 'btn btn-primary mb-2 list-scope__submit' %> <% end %> diff --git a/app/views/admin/groups/index.html.erb b/app/views/admin/groups/index.html.erb index 8a1101e356..94c4c43243 100644 --- a/app/views/admin/groups/index.html.erb +++ b/app/views/admin/groups/index.html.erb @@ -1,6 +1,6 @@ <% content_for :page_title, construct_page_title(t('hyku.admin.groups.title.index'), t('hyku.admin.title')) %> <% provide :page_header do %> - <%if can? :edit, Hyrax::Group %> + <% if can? :edit, Hyrax::Group %> <h1> <span class="fa fa-users"></span> <%= t('hyku.admin.groups.title.index') %> @@ -13,8 +13,8 @@ <% end %> <% end %> -<div class="panel panel-default group-listing"> - <div class="panel-heading"> +<div class="card group-listing"> + <div class="card-header"> <% group_count = Hyrax::Group.count %> <% if group_count == 1 %> <%= t('hyku.admin.groups.describe_singular_quantity_html') %> @@ -22,16 +22,16 @@ <%= t('hyku.admin.groups.describe_quantity_html', number: group_count ) %> <% end %> <% if can? :create, Hyrax::Group %> - <%= link_to t('hyku.admin.groups.action.create'), new_admin_group_path, class: 'btn btn-primary pull-right new-group' %> + <%= link_to t('hyku.admin.groups.action.create'), new_admin_group_path, class: 'btn btn-primary float-right new-group' %> <% end %> </div> - <div class="panel-body"> + <div class="card-body"> <div class="container-fluid full-width-container"> <div class="row"> - <div class="col-sm-6"> + <div class="col-md-6"> <%= render 'per_page', target: admin_groups_path %> </div> - <div class="col-sm-6 text-right"> + <div class="col-md-6 text-right"> <%= render 'search', target: admin_groups_path %> </div> </div> @@ -53,7 +53,7 @@ <tr id=<%= "#{group.name}" %>> <td> <b><%= group.humanized_name %></b> - <p class='help-block'> + <p class='form-text text-muted'> <%= group.description_label %> </p> </td> @@ -71,7 +71,7 @@ </td> <td> <% if can? :edit, Hyrax::Group %> - <%= link_to t('hyku.admin.groups.action.edit'), edit_admin_group_path(group), id: "edit-#{group.name}-group", class: 'btn btn-default' %> + <%= link_to t('hyku.admin.groups.action.edit'), edit_admin_group_path(group), id: "edit-#{group.name}-group", class: 'btn btn-secondary' %> <% end %> </td> </tr> @@ -80,7 +80,7 @@ </table> </div> - <nav class="pull-right"> + <nav class="float-right"> <%= paginate @groups %> </nav> </div> diff --git a/app/views/admin/groups/new.html.erb b/app/views/admin/groups/new.html.erb index c20ee46fb6..f2be4a36b4 100644 --- a/app/views/admin/groups/new.html.erb +++ b/app/views/admin/groups/new.html.erb @@ -7,19 +7,19 @@ <% end %> <div class="panel-tab-wrapper"> <ul class="nav nav-tabs"> - <li role="presentation" class="active"> - <a href="javascript:void(0);"><%= t('hyku.admin.groups.nav.attributes') %></a> + <li class="nav-item" role="presentation"> + <a class="nav-link active" href="javascript:void(0);"><%= t('hyku.admin.groups.nav.attributes') %></a> </li> - <li role="presentation" class="disabled"> - <a href="javascript:void(0);"><%= t('hyku.admin.groups.nav.members') %></a> + <li class="nav-item" role="presentation"> + <a class="nav-link disabled" href="javascript:void(0);"><%= t('hyku.admin.groups.nav.members') %></a> </li> - <li role="presentation" class="disabled"> - <a href="javascript:void(0);"><%= t('hyku.admin.groups.nav.roles') %></a> + <li class="nav-item" role="presentation"> + <a class="nav-link disabled" href="javascript:void(0);"><%= t('hyku.admin.groups.nav.roles') %></a> </li> - <li role="presentation" class="disabled"> - <a href="javascript:void(0);"><%= t('hyku.admin.groups.nav.delete') %></a> + <li class="nav-item" role="presentation"> + <a class="nav-link disabled" href="javascript:void(0);"><%= t('hyku.admin.groups.nav.delete') %></a> </li> </ul> - <%= link_to t('hyku.admin.groups.nav.index'), admin_groups_path, class: 'pull-right nav-escape-link' %> + <%= link_to t('hyku.admin.groups.nav.index'), admin_groups_path, class: 'float-right nav-escape-link' %> </div> <%= render 'form' %> diff --git a/app/views/admin/groups/remove.html.erb b/app/views/admin/groups/remove.html.erb index b5380ed6c2..b2a01abcfe 100644 --- a/app/views/admin/groups/remove.html.erb +++ b/app/views/admin/groups/remove.html.erb @@ -7,15 +7,15 @@ <% end %> <%= render 'nav' %> -<div class="panel panel-default"> - <div class="panel-body"> - <blockquote class="callout callout-danger callout-with-action"> +<div class="card"> + <div class="card-body"> + <blockquote class="callout callout-danger"> <h3><%= t('hyku.admin.groups.title.delete', group: @group.humanized_name) %></h3> <p> - <%= t('hyku.admin.groups.action.remove.description') %> + <%= t('hyku.admin.groups.action.remove.description') %> </p> <div class="callout-action"> - <%= link_to t('hyku.admin.groups.action.remove.submit'), admin_group_path(@group), class: "btn btn-danger action-delete #{'disabled' if @group.default_group?}", method: :delete, data: { confirm: t('hyku.admin.groups.action.remove.confirmation') } %> + <%= link_to t('hyku.admin.groups.action.remove.submit'), admin_group_path(@group), class: "btn btn-danger action-delete #{'disabled' if @group.default_group?}", method: :delete, data: { confirm: t('hyku.admin.groups.action.remove.confirmation') } %> </div> </blockquote> </div> diff --git a/app/views/admin/groups/roles.html.erb b/app/views/admin/groups/roles.html.erb index 2e75f697b3..b3aa2f89e8 100644 --- a/app/views/admin/groups/roles.html.erb +++ b/app/views/admin/groups/roles.html.erb @@ -7,77 +7,78 @@ <% end %> <%= render '/admin/groups/nav' %> -<div class='panel panel-default'> - <div class='panel-body'> +<div class='card'> + <div class='card-body'> <p class='lead'><%= t('hyku.admin.groups.action.members.description') %></p> - <!-- BEGIN Current Group Roles --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h3 class='panel-title'><%= t('hyku.admin.groups.roles.title.current_group_roles') %></h3> - </div> + <!-- BEGIN Current Group Roles --> + <div class='card'> + <div class='card-header'> + <h3 class='card-title'><%= t('hyku.admin.groups.roles.title.current_group_roles') %></h3> + </div> - <div class='panel-body'> - <div class='table-responsive'> - <table class='table table-striped datatable group-roles-table current-group-roles'> - <thead> - <th><%= t('hyku.admin.groups.roles.label.name') %></th> - <th><%= t('hyku.admin.groups.roles.label.description') %></th> - <th><%= t('hyku.admin.groups.roles.label.action') %></th> - </thead> + <div class='card-body'> + <div class='table-responsive'> + <table class='table table-striped datatable group-roles-table current-group-roles'> + <thead> + <th><%= t('hyku.admin.groups.roles.label.name') %></th> + <th><%= t('hyku.admin.groups.roles.label.description') %></th> + <th><%= t('hyku.admin.groups.roles.label.action') %></th> + </thead> - <tbody> - <% @group.roles&.each do |role| %> - <tr id=<%= "assigned-role-#{role.id}" %>> - <td><%= role.name.titleize %></td> - <td><%= role.description_label %> - <td> - <%= simple_form_for :remove_role_from_group, url: admin_group_role_path(group_id: @group.to_param, role_id: role.id), method: :delete, html: { class: 'form' } do |f| %> - <%= f.submit class: 'btn btn-danger', disabled: @group.name == ::Ability.admin_group_name && role.name == 'admin', id: "remove-role-#{role.id}-from-group" %> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> + <tbody> + <% @group.roles&.each do |role| %> + <tr id=<%= "assigned-role-#{role.id}" %>> + <td><%= role.name.titleize %></td> + <td><%= role.description_label %> + <td> + <%= simple_form_for :remove_role_from_group, url: admin_group_role_path(group_id: @group.to_param, role_id: role.id), method: :delete, html: { class: 'form' } do |f| %> + <%= f.submit class: 'btn btn-danger', disabled: @group.name == ::Ability.admin_group_name && role.name == 'admin', id: "remove-role-#{role.id}-from-group" %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> </div> </div> - </div> - <!-- END Current Group Roles --> + <!-- END Current Group Roles --> - <!-- BEGIN Add Role to Group --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h3 class='panel-title'><%= t('hyku.admin.groups.roles.title.add_group_roles') %></h3> - </div> + <!-- BEGIN Add Role to Group --> + <div class='card'> + <div class='card-header'> + <h3 class='card-title'><%= t('hyku.admin.groups.roles.title.add_group_roles') %></h3> + </div> - <div class='panel-body'> - <div class='table-responsive'> - <table class='table table-striped datatable group-roles-table add-group-roles'> - <thead> - <th><%= t('hyku.admin.groups.roles.label.name') %></th> - <th><%= t('hyku.admin.groups.roles.label.description') %></th> - <th><%= t('hyku.admin.groups.roles.label.action') %></th> - </thead> + <div class='card-body'> + <div class='table-responsive'> + <table class='table table-striped datatable group-roles-table add-group-roles'> + <thead> + <th><%= t('hyku.admin.groups.roles.label.name') %></th> + <th><%= t('hyku.admin.groups.roles.label.description') %></th> + <th><%= t('hyku.admin.groups.roles.label.action') %></th> + </thead> - <tbody> - <% @roles&.each do |role| %> - <tr id=<%= "available-role-#{role.id}" %>> - <td><%= role.name.titleize %></td> - <td><%= role.description_label %> - <td> - <%= simple_form_for :add_role_to_group, url: admin_group_roles_path(group_id: @group.to_param, role_id: role.id), method: :post, html: { class: 'form' } do |f| %> - <%= f.submit class: 'btn btn-success', id: "add-role-#{role.id}-to-group" %> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> + <tbody> + <% @roles&.each do |role| %> + <tr id=<%= "available-role-#{role.id}" %>> + <td><%= role.name.titleize %></td> + <td><%= role.description_label %> + <td> + <%= simple_form_for :add_role_to_group, url: admin_group_roles_path(group_id: @group.to_param, role_id: role.id), method: :post, html: { class: 'form' } do |f| %> + <%= f.submit class: 'btn btn-success', id: "add-role-#{role.id}-to-group" %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> </div> </div> + <!-- END Add Role to Group --> </div> - <!-- END Add Role to Group --> </div> <script> diff --git a/app/views/admin/groups/users.html.erb b/app/views/admin/groups/users.html.erb index 75da95dee6..4e0fbc8079 100644 --- a/app/views/admin/groups/users.html.erb +++ b/app/views/admin/groups/users.html.erb @@ -7,33 +7,33 @@ <% end %> <%= render '/admin/groups/nav' %> -<div class="panel panel-default group-user-listing"> - <div class="panel-body"> +<div class="card group-user-listing"> + <div class="card-body"> <p class="lead"><%= t('hyku.admin.groups.action.members.description') %></p> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><%= t('hyku.admin.groups.title.add_user') %></h3> + <div class="card"> + <div class="card-header"> + <h3 class="card-title"><%= t('hyku.admin.groups.title.add_user') %></h3> </div> - <div class="panel-body"> - <%= simple_form_for :user_search, url: hyrax.users_path, method: :get, html: { class: 'form-inline pull-left js-group-user-search' } do |f| %> + <div class="card-body"> + <%= simple_form_for :user_search, url: hyrax.users_path, method: :get, html: { class: 'form-inline float-left js-group-user-search' } do |f| %> <%= f.input :uq, name: :uq, required: false, input_html: { class: 'js-group-user-search__query', value: params[:uq] } %> - <%= f.submit class: 'btn btn-default js-group-user-search__submit' %> + <%= f.submit class: 'btn btn-secondary js-group-user-search__submit' %> <% end %> - <%= simple_form_for :add_user_to_group, url: admin_group_users_path(group_id: @group.to_param), method: :post, html: { class: 'form-inline pull-left js-group-user-add' } do |f| %> + <%= simple_form_for :add_user_to_group, url: admin_group_users_path(group_id: @group.to_param), method: :post, html: { class: 'form-inline float-left js-group-user-add' } do |f| %> <%= f.input :user_id, required: false, input_html: { class: 'js-group-user-add__id', name: :user_id } %> <%= f.submit class: 'btn btn-primary js-group-user-add__submit' %> <% end %> </div> </div> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><%= t('hyku.admin.groups.title.list_members') %></h3> + <div class="card"> + <div class="card-header"> + <h3 class="card-title"><%= t('hyku.admin.groups.title.list_members') %></h3> </div> - <div class="panel-body"> + <div class="card-body"> <div class="container-fluid full-width-container"> <div class="row"> <div class="col-sm-6"> @@ -85,7 +85,7 @@ </table> </div> - <nav class="pull-right"> + <nav class="float-right"> <%= paginate @users %> </nav> </div> diff --git a/app/views/admin/work_types/edit.html.erb b/app/views/admin/work_types/edit.html.erb index 4595d2660a..d325fb8f56 100644 --- a/app/views/admin/work_types/edit.html.erb +++ b/app/views/admin/work_types/edit.html.erb @@ -2,14 +2,14 @@ <h1><span class="fa fa-address-book"></span> <%= t('hyku.admin.work_types') %></h1> <% end %> -<div class="panel panel-default"> - <div class="panel-body"> +<div class="card"> + <div class="card-body"> <%= simple_form_for @site, url: '/admin/work_types' do |f| %> <% Hyrax.config.registered_curation_concern_types.each do |type| %> - <div class="checkbox"> - <label for="input-<%= type %>"> - <%= check_box_tag 'available_works[]', type, @site.available_works&.include?(type), id: "input-#{type}" %> - <span><%= type %></span><br /> + <div class="form-check"> + <input class="form-check-input" type="checkbox" value="<%= type %>" id="input-<%= type %>" name="available_works[]" <%= 'checked' if @site.available_works&.include?(type) %>> + <label class="form-check-label" for="input-<%= type %>"> + <%= type %> </label> </div> <% end %> diff --git a/app/views/advanced/_advanced_search_help.html.erb b/app/views/advanced/_advanced_search_help.html.erb new file mode 100644 index 0000000000..52ef3a695c --- /dev/null +++ b/app/views/advanced/_advanced_search_help.html.erb @@ -0,0 +1,20 @@ +<%# OVERRIDE Blacklight Advanced Search v7.0.0 to add Search by date %> + +<div class='card card-default'> + <div class="card-body"> + <h4 class="card-title">Search tips</h4> + <ul class="advanced-help"> + <li>Select "match all" to require all fields.</li> + <li>Select "match any" to find at least one field.</li> + <li>Combine keywords and attributes to find specific items.</li> + <%# OVERRIDE begin %> + <li>Search by date with format: YYYYMMDD, YYYYMM or YYYY. Do not use "-", "/", or any other special characters.</li> + <%# OVERRIDE end %> + <li>Use quotation marks to search as a phrase.</li> + <li>Use "+" before a term to make it required. (Otherwise results matching only some of your terms may be included).</li> + <li>Use "-" before a word or phrase to exclude.</li> + <li>Use "OR", "AND", and "NOT" to create complex boolean logic. You can use parentheses in your complex expressions.</li> + <li>Truncation and wildcards are not supported - word-stemming is done automatically.</li> + </ul> + </div> +</div> diff --git a/app/views/blacklight_range_limit/_range_limit_panel.html.erb b/app/views/blacklight_range_limit/_range_limit_panel.html.erb new file mode 100644 index 0000000000..738e6e3d76 --- /dev/null +++ b/app/views/blacklight_range_limit/_range_limit_panel.html.erb @@ -0,0 +1,125 @@ +<%- # requires solr_config local passed in + field_config = range_config(field_name) + label = facet_field_label(field_name) + + input_label_range_begin = field_config[:input_label_range_begin] || t("blacklight.range_limit.range_begin", field_label: label) + input_label_range_end = field_config[:input_label_range_end] || t("blacklight.range_limit.range_end", field_label: label) + maxlength = field_config[:maxlength] +-%> + + +<%# NOTE(dewey4iv): leaving the styling here for now so that Christy can test out what she wants this to look like %> +<style> + .year-input { + font-size: 1em; + } + + .limit_content.range_limit { + margin: .5em; + } + .limit_content.range_limit .range_input_wrapper { + margin: 5px 0; + } + + .limit_content.range_limit .range_input_wrapper span { + display: block; + line-height: 1; + } + + .limit_content.range_limit .range_input_wrapper:nth-child(2) { + margin-bottom: 25px; + } +</style> + +<div class="limit_content range_limit"> + <% if has_selected_range_limit?(field_name) %> + <ul class="current list-unstyled facet-values"> + <li class="selected"> + <span class="facet-label"> + <span class="selected"><%= range_display(field_name) %></span> + <%= link_to remove_range_param(field_name), :class=>"remove", :title => t('blacklight.range_limit.remove_limit') do %> + <span class="glyphicon glyphicon-remove"></span> + <span class="sr-only">[<%= t('blacklight.range_limit.remove_limit') %>]</span> + <% end %> + </span> + <span class="selected facet-count"><%= number_with_delimiter(@response.total) %></span> + </li> + </ul> + + <% end %> + + <% unless selected_missing_for_range_limit?(field_name) %> + <%= form_tag search_action_path, :method => :get, class: [BlacklightRangeLimit.classes[:form], "range_#{field_name}"].join(' ') do %> + <%= render_hash_as_hidden_fields(search_state.params_for_search.except(:page)) %> + + <!-- + we need to include a dummy search_field parameter if none exists, + to trick blacklight into displaying actual search results instead + of home page. Not a great solution, but easiest for now. + --> + <% unless params.has_key?(:search_field) %> + <%= hidden_field_tag("search_field", "dummy_range") %> + <% end %> + + <div class="range_input_wrapper"> + <span class="year-input">Between year:</span> + <%= render_range_input(field_name, :begin, input_label_range_begin, maxlength) %> + </div> + <div class="range_input_wrapper"> + <span class="year-input">and year:</span> + <%= render_range_input(field_name, :end, input_label_range_end, maxlength) %> + </div> + <%= submit_tag t('blacklight.range_limit.submit_limit'), class: "#{BlacklightRangeLimit.classes[:submit]} btn btn-default btn-block" %> + <% end %> + <% end %> + + <!-- no results profile if missing is selected --> + <% unless selected_missing_for_range_limit?(field_name) %> + <!-- + you can hide this if you want, but it has to be on page if you want + JS slider and calculated facets to show up, JS sniffs it. + --> + <div class="profile"> + <% if stats_for_field?(field_name) %> + <!-- No stats information found for field in search response --> + <% end %> + + <% if (min = range_results_endpoint(field_name, :min)) && + (max = range_results_endpoint(field_name, :max)) %> + <p class="range subsection <%= "slider_js" unless field_config[:slider_js] == false %>"> + <!-- Current results range from <span class="min"><%= range_results_endpoint(field_name, :min) %></span> to <span class="max"><%= range_results_endpoint(field_name, :max) %></span> --> + </p> + + <% if field_config[:segments] != false %> + <div class="distribution subsection <%= 'chart_js' unless field_config[:chart_js] == false %>"> + <!-- + if we already fetched segments from solr, display them + here. Otherwise, display a link to fetch them, which JS + will AJAX fetch. + --> + <% if solr_range_queries_to_a(field_name).length > 0 %> + + <%= render(:partial => "blacklight_range_limit/range_segments", :locals => {:solr_field => field_name}) %> + + <% else %> + <%= link_to('View distribution', main_app.url_for(search_state.to_h.merge(action: 'range_limit', range_field: field_name, range_start: min, range_end: max)), :class => "load_distribution") %> + <% end %> + </div> + <% end %> + <% end %> + + <% if (stats = stats_for_field(field_name)) %> + <ul class="missing list-unstyled facet-values subsection"> + <li> + <span class="facet-label"> + <%= link_to BlacklightRangeLimit.labels[:missing], add_range_missing(field_name) %> + </span> + <span class="facet-count"> + <%= number_with_delimiter(stats["missing"]) %> + </span> + </li> + </ul> + <% end %> + </div> + <% end %> +</div> diff --git a/app/views/catalog/_index_gallery_collection_wrapper.html.erb b/app/views/catalog/_index_gallery_collection_wrapper.html.erb index e55b72ff39..339cb6b895 100644 --- a/app/views/catalog/_index_gallery_collection_wrapper.html.erb +++ b/app/views/catalog/_index_gallery_collection_wrapper.html.erb @@ -1,5 +1,5 @@ -<%# OVERRIDE Hyrax 3.4.1: make collection thumbnail a link so it matches works %> -<div class="document col-xs-6 col-md-3"> +<%# OVERRIDE Hyrax v5.0.0rc2: make collection thumbnail a link so it matches works %> +<div class="document col-6 col-md-3"> <div class="thumbnail"> <% value = collection_thumbnail(document, {}, counter: document_counter_with_offset(document_counter)) %> <%= link_to value, generate_work_url(document.to_h, request)%> diff --git a/app/views/catalog/_index_header_default.html.erb b/app/views/catalog/_index_header.html.erb similarity index 83% rename from app/views/catalog/_index_header_default.html.erb rename to app/views/catalog/_index_header.html.erb index 89d92ad8cc..ca310a3d73 100644 --- a/app/views/catalog/_index_header_default.html.erb +++ b/app/views/catalog/_index_header.html.erb @@ -1,9 +1,9 @@ -<%# OVERRIDE blacklight-gallery v0.12.0: - TODO: commented out because bookmarks fail for shared search tenant. +<%# OVERRIDE Blacklight v7.35.0: + TODO: commented out because bookmarks fail for shared search tenant. If a bookmark is saved in shared tenant, it errors on subsequent views. %> <%# header bar for doc items in index view -%> -<div class="documentHeader row"> +<header class="documentHeader row"> <%# main title container for doc partial view How many bootstrap columns need to be reserved for bookmarks control depends on size. @@ -12,13 +12,15 @@ <% # bookmark functions for items/docs -%> <%#= render_index_doc_actions document, wrapping_class: "index-document-functions col-sm-3 col-lg-2" %> <% end %> + <h3 class="index_title document-title-heading <%= document_actions.present? ? "col-sm-9 col-lg-10" : "col-md-12" %>"> <% if counter = document_counter_with_offset(document_counter) %> <span class="document-counter"> <%= t('blacklight.search.documents.counter', counter: counter) %> </span> <% end %> - <%= link_to_document document, document_show_link_field(document), counter: counter %> + <%= link_to_document document, counter: counter %> </h3> + <%= document_actions %> -</div> +</header> diff --git a/app/views/catalog/_index_header_list_collection.html.erb b/app/views/catalog/_index_header_list_collection.html.erb index d6860cc46c..62a809d258 100644 --- a/app/views/catalog/_index_header_list_collection.html.erb +++ b/app/views/catalog/_index_header_list_collection.html.erb @@ -1,4 +1,8 @@ -<div class="search-results-title-row"> - <h4 class="search-result-title"><%= link_to document.title_or_label, generate_work_url(document.to_h, request) %></h4> +<%# OVERRIDE Hyrax v5.0.0rc2 %> + +<div class="search-results-title-row col-12 d-flex flex-row align-items-center pb-2"> + <%# OVERRIDE begin %> + <h3 class="search-result-title mb-0 pr-3"><%= link_to document.title_or_label, generate_work_url(document.to_h, request) %></h3> + <%# OVERRIDE end %> <%= Hyrax::CollectionPresenter.new(document, current_ability).collection_type_badge %> </div> diff --git a/app/views/catalog/_index_header_list_default.html.erb b/app/views/catalog/_index_header_list_default.html.erb index a215dc20c3..bad9d9e9dc 100644 --- a/app/views/catalog/_index_header_list_default.html.erb +++ b/app/views/catalog/_index_header_list_default.html.erb @@ -1,10 +1,10 @@ -<%# OVERRIDE Hyrax 3.4.0 to support shared search %> +<%# OVERRIDE Hyrax v5.0.0rc2 to support shared search %> <% model = document.hydra_model %> -<div class="search-results-title-row"> - <% if model == Hyrax::PcdmCollection || model < Hyrax::PcdmCollection %> - <h4 class="search-result-title"><%= link_to document.title_or_label, generate_work_url(document, request) %></h4> - <%= Hyrax::CollectionPresenter.new(document, current_ability).collection_type_badge %> - <% else %> - <h4 class="search-result-title"><%= link_to document.title_or_label, generate_work_url(document, request) %></h4> - <% end %> +<div class="search-results-title-row col-12 d-flex flex-row align-items-center pb-2"> + <% if model == Hyrax::PcdmCollection || model < Hyrax::PcdmCollection %> + <h4 class="search-result-title"><%= link_to document.title_or_label, generate_work_url(document, request) %></h4> + <%= Hyrax::CollectionPresenter.new(document, current_ability).collection_type_badge %> + <% else %> + <h4 class="search-result-title"><%= link_to document.title_or_label, generate_work_url(document, request) %></h4> + <% end %> </div> diff --git a/app/views/catalog/_index_masonry_default.html.erb b/app/views/catalog/_index_masonry_default.html.erb deleted file mode 100644 index dfec1309cc..0000000000 --- a/app/views/catalog/_index_masonry_default.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%# OVERRIDE blacklight-gallery: for shared search URLs %> -<%= link_to document.title_or_label, generate_work_url(document.to_h, request) %> diff --git a/app/views/catalog/_thumbnail_list_collection.html.erb b/app/views/catalog/_thumbnail_list_collection.html.erb index d06f1a4888..65b33703a1 100644 --- a/app/views/catalog/_thumbnail_list_collection.html.erb +++ b/app/views/catalog/_thumbnail_list_collection.html.erb @@ -1,5 +1,5 @@ <%# Override Hyrax 3.4.1 to remove thumbnail link supression and use default collection thumbnail %> -<div class="col-md-2"> +<div class="col-md-3"> <% value = collection_thumbnail(document, {}, counter: document_counter_with_offset(document_counter)) %> <%= link_to value, generate_work_url(document.to_h, request)%> </div> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index d03d87c178..ce6aad1b87 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -1,20 +1,20 @@ <h2 class="text-center">Forgot your password?</h2> -<div class="row center-block"> - <div class="col-md-6 col-md-offset-3"> +<div class="row justify-content-center"> + <div class="col-md-6"> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> <div class="form-group"> <%= f.label :email %><br /> - <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %> </div> <div class="form-group text-center"> - <%= f.submit "Send me reset password instructions", class: 'btn btn-primary' %> + <%= f.submit "Send me reset password instructions", class: 'btn btn-primary' %> </div> <% end %> <div class="text-center"> <%= render "devise/shared/links" %> </div> </div> -</div> \ No newline at end of file +</div> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 9f9b48c793..3165776732 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,19 +1,21 @@ -<div class="row col-md-9 account-form registration"> +<div class="row justify-content-center"> + <div class="col-md-9 account-form registration"> <h2><%= t(".sign_up_header") %></h2> - <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| %> - <%= f.error_notification %> + <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: '' }) do |f| %> + <%= f.error_notification %> - <div class="form-inputs"> - <%= f.input :display_name, required: true, wrapper: :inline %> - <%= f.input :email, required: true, autofocus: true, wrapper: :inline %> - <%= f.input :password, required: true, wrapper: :inline %> - <%= f.input :password_confirmation, required: true, wrapper: :inline %> - </div> + <div class="form-inputs"> + <%= f.input :display_name, required: true, wrapper: :inline %> + <%= f.input :email, required: true, autofocus: true, wrapper: :inline %> + <%= f.input :password, required: true, wrapper: :inline %> + <%= f.input :password_confirmation, required: true, wrapper: :inline %> + </div> - <div class="form-actions"> - <%= render 'devise/shared/links' %> - <%= f.button :submit, t(".sign_up") %> - </div> + <div class="form-actions"> + <%= render 'devise/shared/links' %> + <%= f.button :submit, t(".sign_up") %> + </div> <% end %> + </div> </div> diff --git a/app/views/devise/registrations/registrations/new.html.erb b/app/views/devise/registrations/registrations/new.html.erb index 4ed469233b..34dec31687 100644 --- a/app/views/devise/registrations/registrations/new.html.erb +++ b/app/views/devise/registrations/registrations/new.html.erb @@ -1,26 +1,24 @@ -<h2 class="text-center pt-40 pb-40"><%= t(".sign_up_header") %></h2> -<div class="row center-block"> - <div class="col-md-8 col-md-offset-2"> - <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| %> - <small> +<h2 class="text-center pt-5 pb-5"><%= t(".sign_up_header") %></h2> +<div class="row justify-content-center"> + <div class="col-md-8"> + <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form' }) do |f| %> + <small> <%= f.error_notification %> - </small> + </small> - <div class="form-inputs"> - <%= f.input :display_name, required: true, wrapper: :inline %> - <%= f.input :email, required: true, autofocus: true, wrapper: :inline %> - <%= f.input :password, required: true, wrapper: :inline %> - <%= f.input :password_confirmation, required: true, wrapper: :inline %> - </div> + <div class="form-inputs"> + <%= f.input :display_name, required: true, wrapper: :inline %> + <%= f.input :email, required: true, autofocus: true, wrapper: :inline %> + <%= f.input :password, required: true, wrapper: :inline %> + <%= f.input :password_confirmation, required: true, wrapper: :inline %> + </div> - <div class="form-group text-center"> - <%= render 'devise/shared/links' %> - </div> - <div class="text-center pb-40"> - <%= f.button :submit, t(".sign_up") %> - </div> + <div class="form-group text-center"> + <%= render 'devise/shared/links' %> + </div> + <div class="text-center pb-5"> + <%= f.button :submit, t(".sign_up") %> </div> <% end %> </div> </div> - diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 090e2cbd87..73c0cc605e 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,31 +1,30 @@ +<h2 class="text-center">Log in</h2> +<div class="row justify-content-center"> + <div class="col-md-6"> + <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> + <div class="form-group"> + <%= f.label :email %><br /> + <%= f.email_field :email, autofocus: true, class: "form-control" %> + </div> - <h2 class="text-center">Log in</h2> - <div class="row center-block"> - <div class="col-md-6 col-md-offset-3"> - <%= form_for(resource, as: resource_name, url: session_path(resource_name), class: "form-horizontal") do |f| %> - <div class="form-group"> - <%= f.label :email %><br /> - <%= f.email_field :email, autofocus: true, class: "form-control" %> - </div> - - <div class="form-group"> - <%= f.label :password, class: "control-label" %><br /> - <%= f.password_field :password, autocomplete: "off", class: "form-control" %> - </div> - - <% if devise_mapping.rememberable? -%> - <div class="form-group text-center"> - <%= f.check_box :remember_me %> - <%= f.label :remember_me, class: "control-label" %> - </div> - <% end -%> + <div class="form-group"> + <%= f.label :password, class: "control-label" %><br /> + <%= f.password_field :password, autocomplete: "off", class: "form-control" %> + </div> + <% if devise_mapping.rememberable? -%> <div class="form-group text-center"> - <%= f.submit "Log in", class: 'btn btn-primary' %> + <%= f.check_box :remember_me %> + <%= f.label :remember_me, class: "control-label" %> </div> - <% end %> - <div class="text-center"> - <%= render "devise/shared/links" %> + <% end -%> + + <div class="form-group text-center"> + <%= f.submit "Log in", class: 'btn btn-primary' %> </div> + <% end %> + <div class="text-center"> + <%= render "devise/shared/links" %> </div> - </div> \ No newline at end of file + </div> +</div> diff --git a/app/views/hyrax/admin/admin_sets/_form_participant_table.html.erb b/app/views/hyrax/admin/admin_sets/_form_participant_table.html.erb index 07903899ca..d355978e5a 100644 --- a/app/views/hyrax/admin/admin_sets/_form_participant_table.html.erb +++ b/app/views/hyrax/admin/admin_sets/_form_participant_table.html.erb @@ -1,38 +1,40 @@ -<%# OVERRIDE Hyrax v3.4.2 Filter out Role accesses as they should never be removed %> -<h3><%= t(".#{access}.title") %></h3> -<p><%= t(".#{access}.help") %></p> -<%# OVERRIDE: use custom method to filter by access as well as filter out rolify Roles %> -<% if @form.filter_access_grants_by_access(filter).any? %> - <%# OVERRIDE: use custom access-specific class in order to differentiate between access tables %> - <table class='table table-striped share-status <%= "#{access}-table" %>'> - <thead> - <tr> - <th><%= t(".#{access}.agent_name") %></th> - <th><%= t(".#{access}.type") %></th> - <th><%= t(".#{access}.action") %></th> - </tr> - </thead> - <tbody> - <%# OVERRIDE: use custom method to filter by access as well as filter out rolify Roles %> - <% @form.filter_access_grants_by_access(filter).each do |g| %> +<%# OVERRIDE Hyrax v5.0.0rc2 Filter out Role accesses as they should never be removed %> +<div class="mb-4"> + <h3 class="h4"><%= t(".#{access}.title") %></h3> + <p><%= t(".#{access}.help") %></p> + <%# OVERRIDE: use custom method to filter by access as well as filter out rolify Roles %> + <% if @form.filter_access_grants_by_access(filter).any? %> + <%# OVERRIDE: use custom access-specific class in order to differentiate between access tables %> + <table class='table table-striped share-status <%= "#{access}-table" %>'> + <thead> <tr> - <td data-agent="<%= g.agent_id %>"><%= g.label %></td> - <td><%= g.agent_type.titleize %></td> - <td> - <%# OVERRIDE: remove disabled class from button to fix styling %> - <% if g.admin_group? && g.access == Hyrax::PermissionTemplateAccess::MANAGE %> - <%= button_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-danger', disabled: true, title: t('hyrax.admin.admin_sets.form.permission_destroy_errors.admin_group') %> - <% else %> - <%= button_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-danger' %> - <% end %> - </td> + <th><%= t(".#{access}.agent_name") %></th> + <th><%= t(".#{access}.type") %></th> + <th><%= t(".#{access}.action") %></th> </tr> - <% end %> - </tbody> - </table> -<% else %> - <p><em><%= t(".#{access}.empty") %></em></p> -<% end %> -<%= button_tag t('.allow_all_registered'), - class: 'btn btn-info', - data: { behavior: 'add-registered-users' } if access == 'depositors' %> + </thead> + <tbody> + <%# OVERRIDE: use custom method to filter by access as well as filter out rolify Roles %> + <% @form.filter_access_grants_by_access(filter).each do |g| %> + <tr> + <td data-agent="<%= g.agent_id %>"><%= g.label %></td> + <td><%= g.agent_type.titleize %></td> + <td> + <%# OVERRIDE: remove disabled class from button to fix styling %> + <% if g.admin_group? && g.access == Hyrax::PermissionTemplateAccess::MANAGE %> + <%= button_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger disabled', disabled: true, title: t('hyrax.admin.admin_sets.form.permission_destroy_errors.admin_group') %> + <% else %> + <%= button_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger' %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + <% else %> + <p><em><%= t(".#{access}.empty") %></em></p> + <% end %> + <%= button_tag t('.allow_all_registered'), + class: 'btn btn-primary', + data: { behavior: 'add-registered-users' } if access == 'depositors' %> +</div> diff --git a/app/views/hyrax/admin/admin_sets/_form_participants.html.erb b/app/views/hyrax/admin/admin_sets/_form_participants.html.erb index 5e33a06c1b..60c684a83b 100644 --- a/app/views/hyrax/admin/admin_sets/_form_participants.html.erb +++ b/app/views/hyrax/admin/admin_sets/_form_participants.html.erb @@ -1,10 +1,10 @@ -<%# OVERRIDE Hyrax v3.4.2 Use Hyrax::Groups for groups select box %> +<%# OVERRIDE Hyrax v5.0.0rc2 Use Hyrax::Groups for groups select box %> <div id="participants" class="tab-pane"> - <div class="panel panel-default labels"> - <div class="panel-body"> - <h2><%= t('.add_participants') %></h2> + <div class="card labels edit-sharing-tab"> + <div class="card-body"> + <h2 class="h3"><%= t('.add_participants') %></h2> <% access_options = options_for_select([['Manager', 'manage'], ['Depositor', 'deposit'], ['Viewer', 'view']]) %> - <%= simple_form_for @form.permission_template, + <%= simple_form_for collection_permission_template_form_for(form: @form), url: [hyrax, :admin, @form, :permission_template], html: { id: 'group-participants-form' } do |f| %> <div class="clearfix spacer"> @@ -27,14 +27,14 @@ { prompt: "Select a role..." }, class: 'form-control' %> - <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info' %> + <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-secondary ml-2' %> </div> </div> <% end %> </div> <% end %> - <%= simple_form_for @form.permission_template, + <%= simple_form_for collection_permission_template_form_for(form: @form), url: [hyrax, :admin, @form, :permission_template], html: { id: 'user-participants-form' } do |f| %> <%= f.fields_for 'access_grants_attributes', @@ -53,19 +53,19 @@ { prompt: "Select a role..." }, class: 'form-control' %> - <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info' %> - <p class="help-block"><%= t('hyrax.admin.admin_sets.form.note') %></p> + <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-secondary ml-2' %> + <p class="form-text"><%= t('hyrax.admin.admin_sets.form.note') %></p> </div> </div> <% end %> <% end %> - <fieldset class="admin-set-participants"> - <legend><%= t(".current_participants") %></legend> + <h2 class="h3"><%= t(".current_participants") %></h2> + <fieldset class="admin-set-participants section-collection-sharing"> <%= render 'form_participant_table', access: 'managers', filter: :manage? %> <%= render 'form_participant_table', access: 'depositors', filter: :deposit? %> <%= render 'form_participant_table', access: 'viewers', filter: :view? %> </fieldset> - </div> + </div><!-- /.card-body --> </div> </div> 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 c92d3b4c08..a5a0ee11e6 100644 --- a/app/views/hyrax/admin/appearances/_banner_image_form.html.erb +++ b/app/views/hyrax/admin/appearances/_banner_image_form.html.erb @@ -1,19 +1,20 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <% require_image = @form.banner_image? ? false : true %> <%# Upload Banner Image %> <%= f.input :banner_image, as: :file, wrapper: :vertical_file_input, required: require_image, hint: t('hyrax.admin.appearances.show.forms.banner_image.hint') %> <%= f.input :banner_image_text, required: true, as: :text, label: 'Banner image alt text' %> - <%= image_tag @form.banner_image.url, class: "img-responsive" if @form.banner_image? %> + <%= image_tag @form.banner_image.url, class: "img-fluid" if @form.banner_image? %> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right' %> </div> <% end %> + <% if @form.banner_image? %> - <div class="panel-footer"> + <div class="card-footer"> <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> <%= f.submit 'Remove banner image', class: 'btn btn-danger', name: :remove_banner_image %> <% end %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_color_input.html.erb b/app/views/hyrax/admin/appearances/_color_input.html.erb index b8a7783b89..7b735a4c5a 100644 --- a/app/views/hyrax/admin/appearances/_color_input.html.erb +++ b/app/views/hyrax/admin/appearances/_color_input.html.erb @@ -9,7 +9,7 @@ <div class='col-lg-2'> <%= link_to 'Restore Default', '#color', - class: "btn btn-default restore-default-color #{'with-color-hint' if !missing_translation(hint)}", + class: "btn btn-secondary restore-default-color #{'with-color-hint' if !missing_translation(hint)}", data: { default_target: color_name } %> </div> </div> diff --git a/app/views/hyrax/admin/appearances/_custom_css_form.html.erb b/app/views/hyrax/admin/appearances/_custom_css_form.html.erb index 6fdea57c65..f4f5d980f0 100644 --- a/app/views/hyrax/admin/appearances/_custom_css_form.html.erb +++ b/app/views/hyrax/admin/appearances/_custom_css_form.html.erb @@ -1,5 +1,5 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <%= f.input :custom_css_block, required: false, as: :text, label: 'Add Custom CSS Below' %> <script> var custom_css_block = document.getElementById('admin_appearance_custom_css_block') @@ -12,7 +12,7 @@ }); </script> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right', data: { confirm: t('hyrax.admin.appearances.show.forms.custom_css.confirm') } %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right', data: { confirm: t('hyrax.admin.appearances.show.forms.custom_css.confirm') } %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_default_colors_form.html.erb b/app/views/hyrax/admin/appearances/_default_colors_form.html.erb index 9a8251957e..323ac98fc0 100644 --- a/app/views/hyrax/admin/appearances/_default_colors_form.html.erb +++ b/app/views/hyrax/admin/appearances/_default_colors_form.html.erb @@ -1,11 +1,11 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body defaultable-colors"> - <% @form.class::DEFAULT_COLORS.each do |color_name, hex| %> + <div class="card-body defaultable-colors"> + <% @form.default_colors.each do |color_name, hex| %> <%= render 'color_input', f: f, color_name: color_name, hex: hex %> <% end %> </div> - <div class="panel-footer"> - <%= link_to 'Restore All Defaults', '#color', class: 'btn btn-default restore-all-default-colors' %> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= link_to 'Restore All Defaults', '#color', class: 'btn btn-secondary restore-all-default-colors' %> + <%= f.submit class: 'btn btn-primary float-right' %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb b/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb index 3df04aba30..24abeac39e 100644 --- a/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb +++ b/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb @@ -1,17 +1,17 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body defaultable-fonts"> - <% df = @form.class::DEFAULT_FONTS %> + <div class="card-body defaultable-fonts"> + <% df = @form.default_fonts %> <% font = f.object.body_font %> <%= f.input :body_font, label: 'Select Body Font', required: false, input_html: { class: 'font-fields', data: { default_value: df['body_font'] } } %> - <%= link_to 'Restore Default', '#font', class: 'btn btn-default restore-default-font', data: { default_target: 'body_font' } %> + <%= link_to 'Restore Default', '#font', class: 'btn btn-secondary restore-default-font', data: { default_target: 'body_font' } %> <%= f.input :headline_font, label: 'Select Header Font', required: false, input_html: { class: 'font-fields', data: { default_value: df['headline_font'] } } %> - <%= link_to 'Restore Default', '#font', class: 'btn btn-default restore-default-font', data: { default_target: 'headline_font' } %> + <%= link_to 'Restore Default', '#font', class: 'btn btn-secondary restore-default-font', data: { default_target: 'headline_font' } %> </div> - <div class="panel-footer"> - <%= link_to 'Restore All Defaults', '#font', class: 'btn btn-default restore-all-default-fonts' %> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= link_to 'Restore All Defaults', '#font', class: 'btn btn-secondary restore-all-default-fonts' %> + <%= f.submit class: 'btn btn-primary float-right' %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_default_images_form.html.erb b/app/views/hyrax/admin/appearances/_default_images_form.html.erb index 8e409fff59..dc2da00e36 100644 --- a/app/views/hyrax/admin/appearances/_default_images_form.html.erb +++ b/app/views/hyrax/admin/appearances/_default_images_form.html.erb @@ -1,20 +1,20 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <%= f.input :default_collection_image, as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.default_images.hint'), required: false %> <%= f.input :default_collection_image_text, as: :text, label: 'Default collection image alt text', required: false %> - <%= image_tag @form.default_collection_image.url, class: "img-responsive" if @form.default_collection_image? %> + <%= image_tag @form.default_collection_image.url, class: "img-fluid" if @form.default_collection_image? %> <%= f.input :default_work_image, as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.default_images.hint'), required: false %> <%= f.input :default_work_image_text, as: :text, label: 'Default work image alt text', required: false %> - <%= image_tag @form.default_work_image.url, class: "img-responsive" if @form.default_work_image? %> + <%= image_tag @form.default_work_image.url, class: "img-fluid" if @form.default_work_image? %> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right' %> </div> <% end %> <% if @form.default_collection_image? || @form.default_work_image? %> - <div class="panel-footer"> + <div class="card-footer"> <% if @form.default_collection_image? %> <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> <%= f.submit 'Remove default collection image', class: 'btn btn-danger', name: :remove_default_collection_image %> @@ -26,4 +26,4 @@ <% end %> <% end %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_directory_image_form.html.erb b/app/views/hyrax/admin/appearances/_directory_image_form.html.erb index cc29927ee3..18f6b25fba 100644 --- a/app/views/hyrax/admin/appearances/_directory_image_form.html.erb +++ b/app/views/hyrax/admin/appearances/_directory_image_form.html.erb @@ -1,19 +1,19 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <% require_image = @form.directory_image? ? false : true %> <%# Upload Directory Image %> <%= f.input :directory_image, as: :file, wrapper: :vertical_file_input, required: require_image, hint: t('hyrax.admin.appearances.show.forms.directory_image.hint') %> <%= f.input :directory_image_text, required: true, as: :text, label: 'Directory image alt text' %> - <%= image_tag @form.directory_image.url, class: "img-responsive" if @form.directory_image? %> + <%= image_tag @form.directory_image.url, class: "img-fluid" if @form.directory_image? %> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right' %> </div> <% end %> <% if @form.directory_image? %> - <div class="panel-footer"> + <div class="card-footer"> <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> <%= f.submit 'Remove directory image', class: 'btn btn-danger', name: :remove_directory_image %> <% end %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_favicon_form.html.erb b/app/views/hyrax/admin/appearances/_favicon_form.html.erb index 1b526ff10f..157a2c404a 100644 --- a/app/views/hyrax/admin/appearances/_favicon_form.html.erb +++ b/app/views/hyrax/admin/appearances/_favicon_form.html.erb @@ -1,17 +1,17 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <% require_image = @form.favicon? ? false : true %> <%= f.input :favicon, as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.favicon.hint') %> - <%= image_tag @form.favicon.url, class: "img-responsive" if @form.favicon? %> + <%= image_tag @form.favicon.url, class: "img-fluid" if @form.favicon? %> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right' %> </div> <% end %> <% if @form.favicon? %> - <div class="panel-footer"> + <div class="card-footer"> <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> <%= f.submit 'Remove favicon', class: 'btn btn-danger', name: :remove_favicon %> <% end %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_logo_image_form.html.erb b/app/views/hyrax/admin/appearances/_logo_image_form.html.erb index 3b21357f4a..2d1bc9be42 100644 --- a/app/views/hyrax/admin/appearances/_logo_image_form.html.erb +++ b/app/views/hyrax/admin/appearances/_logo_image_form.html.erb @@ -1,19 +1,19 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %> - <div class="panel-body"> + <div class="card-body"> <% require_image = @form.logo_image? ? false : true %> <%# Upload Logo Image %> <%= f.input :logo_image, as: :file, wrapper: :vertical_file_input, required: require_image, hint: t('hyrax.admin.appearances.show.forms.logo_image.hint') %> <%= f.input :logo_image_text, required: true, as: :text, label: 'Logo image alt text' %> - <%= image_tag @form.logo_image.url, class: "img-responsive" if @form.logo_image? %> + <%= image_tag @form.logo_image.url, class: "img-fluid" if @form.logo_image? %> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer"> + <%= f.submit class: 'btn btn-primary float-right' %> </div> <% end %> <% if @form.logo_image? %> - <div class="panel-footer"> + <div class="card-footer"> <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> <%= f.submit 'Remove logo image', class: 'btn btn-danger', name: :remove_logo_image %> <% end %> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_theme_form.html.erb b/app/views/hyrax/admin/appearances/_theme_form.html.erb index d79a03c139..9d0a0d6623 100644 --- a/app/views/hyrax/admin/appearances/_theme_form.html.erb +++ b/app/views/hyrax/admin/appearances/_theme_form.html.erb @@ -1,5 +1,5 @@ <%= simple_form_for @form.site, url: main_app.site_path(@form.site) do |f| %> - <div class="panel-body"> + <div class="card-body"> <div class="row"> <div class="col-sm-6"> <% home_options = @home_theme_names.map { |g| [g[0], g[1], {'data-image' => image_path("themes/#{g[1]}/#{g[1]}.jpg")}] } %> @@ -23,12 +23,12 @@ </ul> </div> <div id="home-wireframe" class="col-sm-6"> - <%= image_tag "themes/default_home/default_home", class: "img-responsive" %> + <%= image_tag "themes/default_home/default_home", class: "img-fluid" %> </div> </div> <hr /> <div class="row"> - <div class="col-sm-6"> + <div class="col-md-6"> <% search_options = @search_themes.map { |s| [s[0], s[1], { 'data-image' => image_path("themes/search/#{s[1]}.jpg") }] } %> <%= f.input :search_theme, label: 'Search Results Page Theme' do %> <%= f.select :search_theme, search_options, {}, { prompt: 'Select search results page theme...', class: 'form-control' } %> @@ -38,8 +38,8 @@ <li>This will select a default view for the search results page. Users can select their preferred views on the search results page that will override this selection.</li> </ul> </div> - <div id="search-wireframe" class="col-sm-6"> - <%= image_tag "themes/search/list_view.jpg", class: "img-responsive" %> + <div id="search-wireframe" class="col-md-6"> + <%= image_tag "themes/search/list_view.jpg", class: "img-fluid" %> </div> </div> <hr /> @@ -56,12 +56,12 @@ </ul> </div> <div id="show-wireframe" class="col-sm-6"> - <%= image_tag "themes/default_show/default_show", class: "img-responsive" %> + <%= image_tag "themes/default_show/default_show", class: "img-fluid" %> </div> </div> <hr /> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer text-right"> + <%= f.submit class: 'btn btn-secondary' %> </div> <% end %> diff --git a/app/views/hyrax/admin/appearances/show.html.erb b/app/views/hyrax/admin/appearances/show.html.erb index 8194f2890f..cd37788ab1 100644 --- a/app/views/hyrax/admin/appearances/show.html.erb +++ b/app/views/hyrax/admin/appearances/show.html.erb @@ -4,88 +4,88 @@ <div class="row"> <div class="col-md-12"> - <div class="panel panel-default tabs" id="admin-set-controls"> + <div class="card tabs" id="admin-set-controls"> <ul class="nav nav-tabs" role="tablist"> - <li class="active"> - <a href="#logo_image" role="tab" data-toggle="tab"><%= t('.tabs.logo') %></a> + <li class="nav-item"> + <a class="nav-link active" href="#logo_image" role="tab" data-toggle="tab"><%= t('.tabs.logo') %></a> </li> - <li> - <a href="#favicon" role="tab" data-toggle="tab"><%= t('.tabs.favicon') %></a> + <li class="nav-item"> + <a class="nav-link" href="#favicon" role="tab" data-toggle="tab"><%= t('.tabs.favicon') %></a> </li> - <li> - <a href="#banner_image" role="tab" data-toggle="tab"><%= t('.tabs.banner_image') %></a> + <li class="nav-item"> + <a class="nav-link" href="#banner_image" role="tab" data-toggle="tab"><%= t('.tabs.banner_image') %></a> </li> - <li> - <a href="#directory_image" role="tab" data-toggle="tab"><%= t('.tabs.directory_image') %></a> + <li class="nav-item"> + <a class="nav-link" href="#directory_image" role="tab" data-toggle="tab"><%= t('.tabs.directory_image') %></a> </li> - <li> - <a href="#default_images" role="tab" data-toggle="tab"><%= t('.tabs.default_images') %></a> + <li class="nav-item"> + <a class="nav-link" href="#default_images" role="tab" data-toggle="tab"><%= t('.tabs.default_images') %></a> </li> - <li> - <a href="#color" role="tab" data-toggle="tab"><%= t('.tabs.colors') %></a> + <li class="nav-item"> + <a class="nav-link" href="#color" role="tab" data-toggle="tab"><%= t('.tabs.colors') %></a> </li> - <li> - <a href="#font" role="tab" data-toggle="tab"><%= t('.tabs.fonts') %></a> + <li class="nav-item"> + <a class="nav-link" href="#font" role="tab" data-toggle="tab"><%= t('.tabs.fonts') %></a> </li> - <li> - <a href="#css" role="tab" data-toggle="tab"><%= t('.tabs.custom_css') %></a> + <li class="nav-item"> + <a class="nav-link" href="#css" role="tab" data-toggle="tab"><%= t('.tabs.custom_css') %></a> </li> - <li> - <a href="#themes" role="tab" data-toggle="tab"><%= t('.tabs.themes') %></a> + <li class="nav-item"> + <a class="nav-link" href="#themes" role="tab" data-toggle="tab"><%= t('.tabs.themes') %></a> </li> </ul> <div class="tab-content"> <div id="logo_image" class="tab-pane active"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'logo_image_form' %> </div> </div> <div id="favicon" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'favicon_form' %> </div> </div> <div id="banner_image" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'banner_image_form' %> </div> </div> <div id="directory_image" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'directory_image_form' %> </div> </div> <div id="default_images" class="tab-pane" data-alert="<%= I18n.t('hyrax.admin.appearances.show.forms.default_images.alert') %>"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'default_images_form' %> </div> </div> <div id="color" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'default_colors_form' %> </div> </div> <div id="font" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'default_fonts_form' %> </div> </div> <div id="css" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'custom_css_form' %> </div> </div> <div id="themes" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card"> <%= render 'theme_form' %> </div> </div> diff --git a/app/views/hyrax/admin/collection_types/_form_participants.html.erb b/app/views/hyrax/admin/collection_types/_form_participants.html.erb index 52e3b06588..7e0c7bdc4f 100644 --- a/app/views/hyrax/admin/collection_types/_form_participants.html.erb +++ b/app/views/hyrax/admin/collection_types/_form_participants.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 Make group field a select instead of text input %> +<%# OVERRIDE Hyrax v5.0.0rc2 Make group field a select instead of text input %> <h3><%= t('.add_participants') %></h3> <p><%= t('.instructions') %></p> <% access_options = options_for_select([ @@ -14,9 +14,9 @@ html: { id: 'group-participants-form' }, as: :collection_type_participant do |f| %> <div class="form-inline add-participants-form"> - <label class="col-md-2 col-xs-4 control-label"><%= t('.add_group') %>:</label> + <label class="col-md-2 col-4 col-form-label"><%= t('.add_group') %>:</label> - <div class="col-md-10 col-xs-8 form-group"> + <div class="col-md-10 col-8 form-group"> <%= f.hidden_field :hyrax_collection_type_id, value: @collection_type_participant.hyrax_collection_type_id %> <%= f.hidden_field :agent_type, value: Hyrax::CollectionTypeParticipant::GROUP_TYPE %> <%# OVERRIDE: change from text_field to select %> @@ -43,9 +43,9 @@ html: { id: 'user-participants-form' }, as: :collection_type_participant do |f| %> <div class="form-inline add-participants-form"> - <label class="col-md-2 col-xs-4 control-label"><%= t('.add_user') %>:</label> + <label class="col-md-2 col-4 col-form-label"><%= t('.add_user') %>:</label> - <div class="col-md-10 col-xs-8 form-group"> + <div class="col-md-10 col-8 form-group"> <%= f.hidden_field :hyrax_collection_type_id, value: @collection_type_participant.hyrax_collection_type_id %> <%= f.hidden_field :agent_type, value: Hyrax::CollectionTypeParticipant::USER_TYPE %> <%= f.text_field :agent_id, diff --git a/app/views/hyrax/admin/stats/show.html.erb b/app/views/hyrax/admin/stats/show.html.erb index 273caa4a74..4996fe330e 100644 --- a/app/views/hyrax/admin/stats/show.html.erb +++ b/app/views/hyrax/admin/stats/show.html.erb @@ -1,21 +1,29 @@ <% content_for :page_header do %> <h1><span class="fa fa-edit"></span> Statistics for <%= application_name %></h1> <% end %> - - <div class="panel panel-default tabs"> + + <div class="card tabs"> <!-- Nav tabs --> <ul id="statistics-tabs" class="nav nav-tabs" role="tablist"> - <li role="presentation" class="active"><a href="#collections" aria-controls="collections" role="tab" data-toggle="tab">Collections</a></li> - <li role="presentation"><a href="#works" aria-controls="works" role="tab" data-toggle="tab">Works</a></li> - <li role="presentation"><a href="#downloads" aria-controls="downloads" role="tab" data-toggle="tab">Downloads</a></li> - <li role="presentation"><a href="#users" aria-controls="users" role="tab" data-toggle="tab">Users</a></li> + <li class="nav-item"> + <a class="nav-link active" href="#collections" role="tab" data-toggle="tab">Collections</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#works" role="tab" data-toggle="tab">Works</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#downloads" role="tab" data-toggle="tab">Downloads</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#users" role="tab" data-toggle="tab">Users</a> + </li> </ul> <!-- Tab panes --> <div class="tab-content"> - <div role="tabpanel" class="tab-pane active" id="collections"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="tab-pane active" id="collections"> + <div class="card labels"> + <div class="card-body"> <h2>Collections over time</h2> <%= graph_tag('collection-graph', [Hyrax::Statistics::Collections::OverTime.new.points], { @@ -34,8 +42,8 @@ </div> </div> <div role="tabpanel" class="tab-pane" id="works"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="card labels"> + <div class="card-body"> <h2>Works over time</h2> <%= graph_tag('works-graph', [Hyrax::Statistics::Works::OverTime.new.points], { @@ -62,21 +70,20 @@ </div> </div> <div role="tabpanel" class="tab-pane" id="downloads"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="card labels"> + <div class="card-body"> ... </div> </div> </div> <div role="tabpanel" class="tab-pane" id="users"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="card labels"> + <div class="card-body"> <%= render "hyrax/admin/stats/stats_by_date" %> <%= render "hyrax/admin/stats/top_data" %> </div> </div> </div> - </div> <script> Blacklight.onLoad(function() { var stats = require('statistics_tab_manager'); @@ -84,5 +91,5 @@ new stats.Tab('#works'); }); </script> - + </div> </div> diff --git a/app/views/hyrax/admin/users/index.html.erb b/app/views/hyrax/admin/users/index.html.erb index dc12474c72..9d5d33dbed 100644 --- a/app/views/hyrax/admin/users/index.html.erb +++ b/app/views/hyrax/admin/users/index.html.erb @@ -9,14 +9,14 @@ <% end %> <% if can? :create, User %> - <div class="panel panel-default users-invite"> - <div class="panel-heading"> + <div class="card users-invite"> + <div class="card-header"> <%= t('.invite_users') %> </div> - <div class="panel-body"> + <div class="card-body"> <%# user_invitation_path is provided by devise_invitable %> - <%= simple_form_for :user, url: main_app.user_invitation_path, html: { class: 'form-inline pull-left' } do |f| %> + <%= simple_form_for :user, url: main_app.user_invitation_path, html: { class: 'form-inline d-inline-flex' } do |f| %> <div class="form-group"> <%= f.hint :email %> <%= f.label :email, class: "control-label", required: false %> @@ -33,12 +33,12 @@ </div> <% end %> -<div class="panel panel-default users-listing"> - <div class="panel-heading"> +<div class="card users-listing"> + <div class="card-header"> <%= t('hyrax.admin.users.index.describe_users_html', count: @presenter.user_count) %> </div> - <div class="panel-body"> + <div class="card-body"> <div class="table-responsive"> <table class="table table-striped datatable"> <thead> diff --git a/app/views/hyrax/admin/workflow_roles/index.html.erb b/app/views/hyrax/admin/workflow_roles/index.html.erb index 4d642f0ec8..1c99b06825 100644 --- a/app/views/hyrax/admin/workflow_roles/index.html.erb +++ b/app/views/hyrax/admin/workflow_roles/index.html.erb @@ -1,19 +1,19 @@ -<%# OVERRIDE Hyrax v3.4.2 Add workflow roles to groups %> +<%# OVERRIDE Hyrax v5.0.0rc2 Add workflow roles to groups %> <% provide :page_header do %> <h1><span class="fa fa-users" aria-hidden="true"></span> <%= t("hyrax.admin.workflow_roles.header") %></h1> <% end %> <div class="row"> <div class="col-md-12"> - <div class="panel panel-default"> - <div class='panel-body'> + <div class="card"> + <div class='card-body'> <%# OVERRIDE: New form for adding workflow roles to groups %> <!-- BEGIN Assign Role to Group --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h2 class='panel-title h2'><%= t('.new_group_role') %></h2> + <div class='card'> + <div class='card-header'> + <h2 class='card-title h2'><%= t('.new_group_role') %></h2> </div> - <div class='panel-body'> + <div class='card-body'> <%= simple_form_for Hyrax::Forms::WorkflowResponsibilityGroupForm.new, url: hyrax.admin_workflow_roles_path, html: { id: :new_sipity_group_workflow_responsibility } do |f| %> <%= f.input :group_id, as: :select, collection: f.object.group_options, label_method: :humanized_name, value_method: :id %> <%= f.input :workflow_role_id, as: :select, collection: f.object.workflow_role_options, input_html: { id: :sipity_workflow_responsibility_group_workflow_role_id } %> @@ -24,11 +24,11 @@ <!-- END Assign Role to Group --> <!-- BEGIN Assign Role to User --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h2 class='panel-title h2'><%= t('.new_user_role') %></h2> + <div class='card'> + <div class='card-header'> + <h2 class='card-title h2'><%= t('.new_user_role') %></h2> </div> - <div class='panel-body'> + <div class='card-body'> <%= simple_form_for Hyrax::Forms::WorkflowResponsibilityForm.new, url: hyrax.admin_workflow_roles_path do |f| %> <%= f.input :user_id, as: :select, collection: f.object.user_options %> <%= f.input :workflow_role_id, as: :select, collection: f.object.workflow_role_options %> @@ -44,15 +44,15 @@ <div class="row"> <div class="col-md-12"> - <div class="panel panel-default"> - <div class='panel-body'> + <div class="card"> + <div class='card-body'> <%# OVERRIDE: Add section for current group roles %> <!-- BEGIN Current Group Roles --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h2 class='panel-title h2'><%= t('.current_group_roles') %></h2> + <div class='card'> + <div class='card-header'> + <h2 class='card-title h2'><%= t('.current_group_roles') %></h2> </div> - <div class='panel-body'> + <div class='card-body'> <table class='table table-striped datatable group-workflow-roles-table'> <thead> <th><%= t('.header.group_name') %></th> @@ -89,11 +89,11 @@ <!-- END Current Group Roles --> <!-- BEGIN Current User Roles --> - <div class='panel panel-default'> - <div class='panel-heading'> - <h2 class='panel-title h2'><%= t('.current_user_roles') %></h2> + <div class='card'> + <div class='card-header'> + <h2 class='card-title h2'><%= t('.current_user_roles') %></h2> </div> - <div class='panel-body'> + <div class='card-body'> <table class='table table-striped datatable user-workflow-roles-table'> <thead> <th><%= t('.header.email') %></th> diff --git a/app/views/hyrax/base/_form_share.html.erb b/app/views/hyrax/base/_form_share.html.erb index 732cbe199f..b9988386e2 100644 --- a/app/views/hyrax/base/_form_share.html.erb +++ b/app/views/hyrax/base/_form_share.html.erb @@ -1,7 +1,7 @@ -<%# OVERRIDE: Hyrax 2.5.1 to use Hyrax::Groups for groups select box %> +<%# OVERRIDE: Hyrax v5.0.0rc2 to use Hyrax::Groups for groups select box %> <p><%= t('.directions') %></p> -<h2><%= t('.add_sharing') %></h2> +<h2 class="h3 mt-4"><%= t('.add_sharing') %></h2> <% depositor = f.object.depositor %> @@ -29,7 +29,7 @@ <label for="new_group_permission_skel" class="sr-only">Access type to grant</label> <%= select_tag 'new_group_permission_skel', options_for_select(Hyrax.config.permission_options), class: 'form-control' %> - <button class="btn btn-default" id="add_new_group_skel"> + <button class="btn btn-secondary ml-2" id="add_new_group_skel"> <span>Add<span class="sr-only"> this group</span></span> </button> <br /><span id="directory_group_result"></span> @@ -45,7 +45,7 @@ <%= text_field_tag 'new_user_name_skel', nil %> <label for="new_user_permission_skel" class="sr-only">Access type to grant</label> <%= select_tag 'new_user_permission_skel', options_for_select(Hyrax.config.permission_options), class: 'form-control' %> - <button class="btn btn-default" id="add_new_user_skel"> + <button class="btn btn-secondary ml-2" id="add_new_user_skel"> <span>Add<span class="sr-only"> this <%= t('hyrax.account_label') %></span></span> </button> <br /> <span id="directory_user_result"></span> diff --git a/app/views/hyrax/base/_show_actions.html.erb b/app/views/hyrax/base/_show_actions.html.erb index b726f7dba8..07151faa23 100644 --- a/app/views/hyrax/base/_show_actions.html.erb +++ b/app/views/hyrax/base/_show_actions.html.erb @@ -1,21 +1,21 @@ -<%# OVERRIDE Hyrax v3.4.2 Adjust permission checks and add analytics button %> +<%# OVERRIDE Hyrax v5.0.0rc2 Adjust permission checks and add analytics button %> <div class="show-actions"> <% if presenter.editor? %> - <%= link_to "Edit", edit_polymorphic_path([main_app, presenter]), class: 'btn btn-default' %> + <%= link_to t('.edit'), edit_polymorphic_path([main_app, presenter]), class: 'btn btn-secondary' %> <%# OVERRIDE: wrap Delete button in #can? check %> <% if current_ability.can?(:delete, presenter.solr_document) %> <%= link_to "Delete", [main_app, presenter], class: 'btn btn-danger', data: { confirm: "Delete this #{presenter.human_readable_type}?" }, method: :delete %> <% end %> <% if presenter.member_presenters.size > 1 %> - <%= link_to t("hyrax.file_manager.link_text"), polymorphic_path([main_app, :file_manager, presenter]), class: 'btn btn-default' %> + <%= link_to t("hyrax.file_manager.link_text"), polymorphic_path([main_app, :file_manager, presenter]), class: 'btn btn-secondary' %> <% end %> <% if presenter.valid_child_concerns.length > 0 %> <div class="btn-group"> - <button type="button" class="btn btn-default dropdown-toggle" type="button" id="dropdown-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <button type="button" class="btn btn-secondary dropdown-toggle" type="button" id="dropdown-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Attach Child <span class="caret"></span></button> <ul class="dropdown-menu"> <% presenter.valid_child_concerns.each do |concern| %> - <li> + <li class="dropdown-item"> <%= link_to "Attach #{concern.human_readable_type}", polymorphic_path([main_app, :new, :hyrax, :parent, concern.model_name.singular.to_sym], parent_id: presenter.id) %> </li> <% end %> @@ -26,22 +26,22 @@ <% if presenter.show_deposit_for?(collections: @user_collections) %> <input type="checkbox" style="display:none" name="batch_document_ids[]" id="batch_document_<%= presenter.id %>" value="<%= presenter.id %>" class="batch_document_selector" checked="checked" /> <%= button_tag t('hyrax.dashboard.my.action.add_to_collection'), - class: 'btn btn-default submits-batches submits-batches-add', + class: 'btn btn-secondary submits-batches submits-batches-add', data: { toggle: "modal", target: "#collection-list-container" } %> <% end %> <% if presenter.work_featurable? %> <%= link_to "Feature", hyrax.featured_work_path(presenter, format: :json), data: { behavior: 'feature' }, - class: presenter.display_unfeature_link? ? 'btn btn-default collapse' : 'btn btn-default' %> + class: presenter.display_unfeature_link? ? 'btn btn-secondary collapse' : 'btn btn-secondary' %> <%= link_to "Unfeature", hyrax.featured_work_path(presenter, format: :json), data: { behavior: 'unfeature' }, - class: presenter.display_feature_link? ? 'btn btn-default collapse' : 'btn btn-default' %> + class: presenter.display_feature_link? ? 'btn btn-secondary collapse' : 'btn btn-secondary' %> <% end %> <% if Hyrax.config.analytics? %> <% # turbolinks needs to be turned off or the page will use the cache and the %> <% # analytics graph will not show unless the page is refreshed. %> - <%= link_to t('.analytics'), presenter.stats_path, id: 'stats', class: 'btn btn-default', data: { turbolinks: false } %> + <%= link_to t('.analytics'), presenter.stats_path, id: 'stats', class: 'btn btn-secondary', data: { turbolinks: false } %> <% end %> </div> diff --git a/app/views/hyrax/base/show.html.erb b/app/views/hyrax/base/show.html.erb index 01ccf18ca8..47e5b202f1 100644 --- a/app/views/hyrax/base/show.html.erb +++ b/app/views/hyrax/base/show.html.erb @@ -8,11 +8,11 @@ <div class="col-sm-12"> <%= render 'work_type', presenter: @presenter %> </div> - <div itemscope itemtype="http://schema.org/CreativeWork" class="col-xs-12"> + <div itemscope itemtype="http://schema.org/CreativeWork" class="col-12"> <%= render 'work_title', presenter: @presenter %> <%= render 'show_actions', presenter: @presenter %> - <div class="panel panel-default"> - <div class="panel-body"> + <div class="card"> + <div class="card-body"> <div class="row"> <%= render 'workflow_actions_widget', presenter: @presenter %> <% if @presenter.iiif_viewer? %> @@ -31,22 +31,22 @@ </div> </div> </div> - </div><!-- /panel --> + </div><!-- /card --> - <div class="panel panel-default"> - <div class="panel-heading"> - <h1 class="panel-title"><%= t('hyrax.base.show.relationships') %></h1> + <div class="card"> + <div class="card-header"> + <h2 class="card-title"><%= t('hyrax.base.show.relationships') %></h2> </div> - <div class="panel-body"> + <div class="card-body"> <%= render 'relationships', presenter: @presenter %> </div> </div> - <div class="panel panel-default"> - <div class="panel-heading"> - <h1 class="panel-title"><%= t('.items') %></h1> + <div class="card"> + <div class="card-header"> + <h2 class="card-title"><%= t('.items') %></h2> </div> - <div class="panel-body"> + <div class="card-body"> <%= render 'items', presenter: @presenter %> </div> </div> diff --git a/app/views/hyrax/contact_form/new.html.erb b/app/views/hyrax/contact_form/new.html.erb index 18ddba15cb..b1c6d774c6 100644 --- a/app/views/hyrax/contact_form/new.html.erb +++ b/app/views/hyrax/contact_form/new.html.erb @@ -1,4 +1,4 @@ -<%# Hyrax 3.4.2 override to add negative captcha %> +<%# OVERRIDE Hyrax v5.0.0rc2 override to add negative captcha %> <% provide :page_title, I18n.t('hyrax.contact_form.title') %> <div class="alert alert-info"> @@ -17,36 +17,36 @@ <% em = '' %> <% end %> -<%= form_for @contact_form, url: hyrax.contact_form_index_path, - html: { class: 'form-horizontal' } do |f| %> +<%= form_for @contact_form, url: hyrax.contact_form_index_path, html: { class: 'needs-validation' } do |f| %> <%= raw negative_captcha(@captcha) %> - <%= f.text_field :contact_method, class: 'hide' %> - <div class="form-group"> - <%= f.label :category, t('hyrax.contact_form.type_label'), class: "col-sm-2 control-label" %> + <%= f.text_field :contact_method, class: 'd-none' %> + + <div class="form-group row"> + <%= f.label :category, t('hyrax.contact_form.type_label'), class: "col-sm-2 col-form-label" %> <div class="col-sm-10"> <%= f.select 'category', options_for_select(contact_form_issue_type_options), { include_blank: t('hyrax.contact_form.select_type') }, {class: 'form-control', required: true } %> </div> </div> - <div class="form-group"> - <%= negative_label_tag(@captcha, :name, t('hyrax.contact_form.name_label'), class: "col-sm-2 control-label" ) %> - <div class="col-sm-10"><%= negative_text_field_tag(@captcha, :name, value: nm, class: 'form-control', required: true ) %></div> + <div class="form-group row"> + <%= negative_label_tag(@captcha, :name, t('hyrax.contact_form.name_label'), class: "col-sm-2 col-form-label") %> + <div class="col-sm-10"><%= negative_text_field_tag(@captcha, :name, value: nm, class: 'form-control', required: true) %></div> </div> - <div class="form-group"> - <%= negative_label_tag(@captcha, :email, t('hyrax.contact_form.email_label'), class: "col-sm-2 control-label" ) %> - <div class="col-sm-10"><%= negative_text_field_tag(@captcha, :email, value: em, class: 'form-control', required: true ) %></div> + <div class="form-group row"> + <%= negative_label_tag(@captcha, :email, t('hyrax.contact_form.email_label'), class: "col-sm-2 col-form-label") %> + <div class="col-sm-10"><%= negative_text_field_tag(@captcha, :email, value: em, class: 'form-control', required: true) %></div> </div> - <div class="form-group"> - <%= f.label :subject, t('hyrax.contact_form.subject_label'), class: "col-sm-2 control-label" %> + <div class="form-group row"> + <%= f.label :subject, t('hyrax.contact_form.subject_label'), class: "col-sm-2 col-form-label" %> <div class="col-sm-10"><%= f.text_field :subject, class: 'form-control', required: true %></div> </div> - <div class="form-group"> - <%= negative_label_tag(@captcha, :message, t('hyrax.contact_form.message_label'), class: "col-sm-2 control-label" ) %> - <div class="col-sm-10"><%= negative_text_area_tag(@captcha, :message, rows: 4, class: 'form-control', required: true ) %></div> + <div class="form-group row"> + <%= negative_label_tag(@captcha, :message, t('hyrax.contact_form.message_label'), class: "col-sm-2 col-form-label") %> + <div class="col-sm-10"><%= negative_text_area_tag(@captcha, :message, rows: 4, class: 'form-control', required: true) %></div> </div> <%= f.submit value: t('hyrax.contact_form.button_label'), class: "btn btn-primary" %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/content_blocks/_form.html.erb b/app/views/hyrax/content_blocks/_form.html.erb index ccf22f70f1..e9375879db 100644 --- a/app/views/hyrax/content_blocks/_form.html.erb +++ b/app/views/hyrax/content_blocks/_form.html.erb @@ -1,26 +1,44 @@ -<%# Copied from Hyrax v2.9.0 to add home_text content block form - Adding themes %> +<%# Copied from Hyrax v5.0.0rc2 to add home_text content block form - Adding themes %> <%# imported from hyrax 3.0.0.pre.rc1 %> <%= render "shared/nav_safety_modal" %> -<div class="panel panel-default tabs"> +<div class="card tabs"> <ul class="nav nav-tabs" role="tablist"> - <li class="active"> - <a href="#announcement_text" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t(:'hyrax.content_blocks.tabs.announcement_text') %></a> + <li id="announcement-nav-item" class="nav-item"> + <a href="#announcement_text" role="tab" data-toggle="tab" class="nav-link active nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.announcement_text') %> + </a> </li> - <li> - <a href="#marketing" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t(:'hyrax.content_blocks.tabs.marketing_text') %></a> + <li id="marketing-nav-item" class="nav-item"> + <a href="#marketing" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.marketing_text') %> + </a> </li> - <li> - <a href="#home_text" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t(:'hyrax.content_blocks.tabs.home_text') %></a> + <li id="home-text-nav-item" class="nav-item"> + <a href="#home_text" role="tab" data-toggle="tab" class="nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.home_text') %> + </a> </li> - <li> - <a href="#researcher" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t(:'hyrax.content_blocks.tabs.featured_researcher') %></a> + <li id="researcher-nav-item" class="nav-item"> + <a href="#researcher" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.featured_researcher') %> + </a> + </li> + <li id="homepage-about-heading-nav-item" class="nav-item"> + <a href="#homepage_about_section_heading" role="tab" data-toggle="tab" class="nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.homepage_about_section_heading') %> + </a> + </li> + <li id="homepage-about-content-nav-item" class="nav-item"> + <a href="#homepage_about_section_content" role="tab" data-toggle="tab" class="nav-safety-confirm"> + <%= t(:'hyrax.content_blocks.tabs.homepage_about_section_content') %> + </a> </li> </ul> <div class="tab-content"> - <div id="announcement_text" class="tab-pane active"> - <div class="panel panel-default labels"> + <div id="announcement_text" class="tab-pane show active"> + <div class="card labels"> <%= simple_form_for ContentBlock.for(:announcement), url: hyrax.content_block_path(ContentBlock.for(:announcement)), html: {class: 'nav-safety'} do |f| %> - <div class="panel-body"> + <div class="card-body"> <div class="field form-group"> <%= f.label :announcement %><br /> <%# the following line was changed from hyrax to give some context for what this context block does %> @@ -28,17 +46,17 @@ <%= f.text_area :announcement, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> </div> </div> - <div class="panel-footer"> - <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-default pull-right' %> - <%= f.button :submit, class: 'btn btn-primary pull-right' %> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> </div> <% end %> </div> </div> <div id="marketing" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card labels"> <%= simple_form_for ContentBlock.for(:marketing), url: hyrax.content_block_path(ContentBlock.for(:marketing)), html: {class: 'nav-safety'} do |f| %> - <div class="panel-body"> + <div class="card-body"> <div class="field form-group"> <%= f.label :marketing %><br /> <%# the following line was changed from hyrax to give some context for what this context block does %> @@ -46,18 +64,18 @@ <%= f.text_area :marketing, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> </div> </div> - <div class="panel-footer"> - <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-default pull-right' %> - <%= f.button :submit, class: 'btn btn-primary pull-right' %> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> </div> <% end %> </div> </div> <%# Copied from Hyrax v2.9.0 to add home_text content block form - Adding themes %> <div id="home_text" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card labels"> <%= simple_form_for ContentBlock.for(:home_text), url: hyrax.content_block_path(ContentBlock.for(:home_text)), html: {class: 'nav-safety'} do |f| %> - <div class="panel-body"> + <div class="card-body"> <div class="field form-group"> <%= f.label :home_text %><br /> <%# the following line was added to hyku for home page text for themes that use or allow home page text %> @@ -65,17 +83,17 @@ <%= f.text_area :home_text, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> </div> </div> - <div class="panel-footer"> - <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-default pull-right' %> - <%= f.button :submit, class: 'btn btn-primary pull-right' %> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> </div> <% end %> </div> </div> <div id="researcher" class="tab-pane"> - <div class="panel panel-default labels"> + <div class="card labels"> <%= simple_form_for ContentBlock.for(:researcher), url: hyrax.content_block_path(ContentBlock.for(:researcher)), html: {class: 'nav-safety'} do |f| %> - <div class="panel-body"> + <div class="card-body"> <div class="field form-group"> <%= f.label :researcher %><br /> <%# the following line was changed from hyrax to give some context for what this context block does %> @@ -83,9 +101,45 @@ <%= f.text_area :researcher, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> </div> </div> - <div class="panel-footer"> - <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-default pull-right' %> - <%= f.button :submit, class: 'btn btn-primary pull-right' %> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> + </div> + <% end %> + </div> + </div> + <div id="homepage_about_section_heading" class="tab-pane"> + <div class="card labels"> + <%= simple_form_for ContentBlock.for(:homepage_about_section_heading), url: hyrax.content_block_path(ContentBlock.for(:homepage_about_section_heading)), html: {class: 'nav-safety'} do |f| %> + <div class="card-body"> + <div class="field form-group"> + <%= f.label :homepage_about_section_heading %><br /> + <%# the following line was changed from hyrax to give some context for what this context block does %> + <p class="content-block-instructions"><%= t(:'hyrax.content_blocks.instructions.homepage_about_section_heading_instructions') %></p> + <%= f.text_area :homepage_about_section_heading, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> + </div> + </div> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> + </div> + <% end %> + </div> + </div> + <div id="homepage_about_section_content" class="tab-pane"> + <div class="card labels"> + <%= simple_form_for ContentBlock.for(:homepage_about_section_content), url: hyrax.content_block_path(ContentBlock.for(:homepage_about_section_content)), html: {class: 'nav-safety'} do |f| %> + <div class="card-body"> + <div class="field form-group"> + <%= f.label :homepage_about_section_content %><br /> + <%# the following line was changed from hyrax to give some context for what this context block does %> + <p class="content-block-instructions"><%= t(:'hyrax.content_blocks.instructions.homepage_about_section_content_instructions') %></p> + <%= f.text_area :homepage_about_section_content, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> + </div> + </div> + <div class="card-footer d-flex justify-content-end"> + <%= f.button :submit, class: 'btn btn-primary text-white mr-2' %> + <%= link_to t(:'hyrax.content_blocks.cancel'), hyrax.admin_admin_sets_path, class: 'btn btn-light' %> </div> <% end %> </div> diff --git a/app/views/hyrax/dashboard/_sidebar.html.erb b/app/views/hyrax/dashboard/_sidebar.html.erb index c91d9f0f4b..4e9797e946 100644 --- a/app/views/hyrax/dashboard/_sidebar.html.erb +++ b/app/views/hyrax/dashboard/_sidebar.html.erb @@ -1,8 +1,10 @@ +<%# OVERIDE Hyrax v5.0.0rc2 to use Hyku::MenuPresenter and add title attribute to link %> + <% menu = Hyku::MenuPresenter.new(self) %> <div class="sidebar-toggle"><span class="fa fa-chevron-circle-right"></span></div> <nav aria-label="sidebar-nav"> - <ul class="nav nav-pills nav-stacked"> - <li> + <ul class="nav nav-pills flex-column"> + <li class="nav-item"> <div class="profile"> <div class="profile-image"> <%= image_tag current_user.avatar.url(:thumb), width: 100 if current_user.avatar.present? %> @@ -12,8 +14,10 @@ </div> </div> </li> - <li> - <%= link_to hyrax.dashboard_path, title: t('hyrax.admin.sidebar.dashboard') do %> + <li class="nav-item"> + <%= link_to hyrax.dashboard_path, + class: "nav-link", + title: t('hyrax.admin.sidebar.dashboard') do %> <span class="fa fa-home" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.dashboard') %></span> <% end %> </li> diff --git a/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb b/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb index 33306d3755..7c78b27a4e 100644 --- a/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb +++ b/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb @@ -1,5 +1,5 @@ <% thumbnail_path = SolrDocument.find(@collection.id).thumbnail_path %> - <% if thumbnail_path.include?("uploaded_collection_thumbnails") and File.exist? Rails.root.join("public#{::SolrDocument.find(@collection.id).thumbnail_path}") %> + <% if thumbnail_path.include?("uploaded_collection_thumbnails") and File.exist? Hyku::Application.path_for("public#{::SolrDocument.find(@collection.id).thumbnail_path}") %> <%= image_tag(thumbnail_path, class: "current-thumbnail") %> <p>Current image: <strong><%= @thumbnail_filename %></strong></p> <% else %> diff --git a/app/views/hyrax/dashboard/collections/_form.html.erb b/app/views/hyrax/dashboard/collections/_form.html.erb index 6915e45905..23a8a16814 100644 --- a/app/views/hyrax/dashboard/collections/_form.html.erb +++ b/app/views/hyrax/dashboard/collections/_form.html.erb @@ -1,34 +1,42 @@ -<%# OVERRIDE Hyrax 2.9.1 to make it say 'Default thumbnail' in the thumbnail select box instead of 'undefined' %> +<%# OVERRIDE Hyrax v5.0.0rc2 to make it say 'Default thumbnail' in the thumbnail select box instead of 'undefined' %> <%= render "shared/nav_safety_modal" %> -<div class="panel panel-default tabs" id="collection-edit-controls"> - <ul class="nav nav-tabs" role="tablist"> - <li class="active"> - <a href="#description" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t('.tabs.description') %></a> +<div class="tabs mt-4" id="collection-edit-controls"> + <ul class="nav nav-tabs" id="dashboard-collection-tab" role="tablist"> + <li class="nav-item"> + <a href="#description" role="tab" data-toggle="tab" class="nav-link active nav-safety-confirm"> + <%= t('.tabs.description') %> + </a> </li> <% if @form.persisted? %> <% if collection_brandable?(collection: @collection) %> - <li> - <a href="#branding" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t('.tabs.branding') %></a> + <li class="nav-item"> + <a href="#branding" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm"> + <%= t('.tabs.branding') %> + </a> </li> <% end %> <% if collection_discoverable?(collection: @collection) %> - <li> - <a href="#discovery" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t('.tabs.discovery') %></a> + <li class="nav-item"> + <a href="#discovery" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm"> + <%= t('.tabs.discovery') %> + </a> </li> <% end %> <% if collection_sharable?(collection: @collection) %> - <li> - <a href="#sharing" role="tab" data-toggle="tab" class="nav-safety-confirm"><%= t('.tabs.sharing') %></a> + <li class="nav-item"> + <a href="#sharing" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm"> + <%= t('.tabs.sharing') %> + </a> </li> <% end %> <% end %> </ul> <%= simple_form_for @form, url: [hyrax, :dashboard, @form], html: { class: 'editor nav-safety', data: { behavior: 'collection-form', 'param-key' => @form.model_name.param_key } } do |f| %> - <div class="tab-content"> - <div id="description" class="tab-pane active"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="tab-content card" id="dashboard-collection-tab-content"> + <div id="description" class="tab-pane show active"> + <div class="labels"> + <div class="card-body"> <div id="base-terms"> <% f.object.primary_terms.each do |term| %> @@ -58,7 +66,7 @@ <% if f.object.display_additional_fields? %> <%= link_to t('hyrax.collection.form.additional_fields'), '#extended-terms', - class: 'btn btn-default additional-fields', + class: 'btn btn-secondary additional-fields', data: { toggle: 'collapse' }, role: "button", 'aria-expanded'=> "false", @@ -88,8 +96,8 @@ <% if @form.persisted? %> <% if collection_brandable?(collection: @collection) %> <div id="branding" class="tab-pane"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="card labels"> + <div class="card-body"> <%= render 'form_branding', f: f %> </div> </div> @@ -98,8 +106,8 @@ <% if collection_discoverable?(collection: @collection) %> <div id="discovery" class="tab-pane"> - <div class="panel panel-default labels"> - <div class="panel-body"> + <div class="card labels"> + <div class="card-body"> <%= render 'form_discovery', f: f %> </div> </div> @@ -108,8 +116,8 @@ <% if collection_sharable?(collection: @collection) %> <div id="sharing" class="tab-pane"> - <div class="panel panel-default labels" id="collection_permissions" data-param-key="<%= f.object.model_name.param_key %>"> - <div class="panel-body"> + <div class="card labels" id="collection_permissions" data-param-key="<%= f.object.model_name.param_key %>"> + <div class="card-body"> <%= render 'form_share', f: f %> </div> </div> @@ -117,7 +125,7 @@ <% end %> <% end %> - <div class="panel-footer"> + <div class="card-footer"> <% if @collection.persisted? %> <%= f.submit t(:'hyrax.collection.select_form.update'), class: 'btn btn-primary', onclick: "confirmation_needed = false;", id: "update_submit", name: "update_collection" %> <%= link_to t(:'helpers.action.cancel'), hyrax.dashboard_collection_path(@collection), class: 'btn btn-link' %> diff --git a/app/views/hyrax/dashboard/collections/_form_branding.html.erb b/app/views/hyrax/dashboard/collections/_form_branding.html.erb index 38bf18487a..14df747e2f 100644 --- a/app/views/hyrax/dashboard/collections/_form_branding.html.erb +++ b/app/views/hyrax/dashboard/collections/_form_branding.html.erb @@ -1,4 +1,4 @@ - <!-- OVERRIDE Hyrax v3.4.1 to add text for collection banner image + <!-- OVERRIDE Hyrax v5.0.0rc2 to add text for collection banner image also added image tags to show logo images... js templates branding to show them seems to be broken uses Hyrax::Forms::CollectionForm --> @@ -15,29 +15,30 @@ <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> <div class="row fileupload-buttonbar"> - <div class="col-sm-3"> + <div class="col-4"> <!-- The fileinput-button span is used to style the file input field as button --> <span class="btn btn-success fileinput-button"> - <span class="glyphicon glyphicon-plus"></span> + <span class="fa fa-plus"></span> <span><%= t('.choose_file') %></span> <input type="file" name="files[]" single /> </span> - </div> <!-- end col-xs-4 --> + </div> <!-- end col-4 --> <!-- The global progress state --> - <div class="col-xs-8 fileupload-progress branding-banner-progress fade"> + <div class="col-8 fileupload-progress branding-banner-progress fade"> <!-- The global progress bar --> - <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100"> - <div class="progress-bar progress-bar-success" style="width:0%;"></div> + <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"> + <div class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width:0%;"></div> </div> <!-- The extended global progress state --> <div class="progress-extended"> </div> - </div> <!-- end col-xs-5 fileupload-progress fade --> + </div> <!-- end col-8 fileupload-progress fade --> </div> <!-- end row fileupload-buttonbar --> <div class="row branding-banner-list"> - <div class="col-xs-12"> + <div class="col-12"> <div class="container"> + <%# Where the request to display the branding comes from. %> <% if f.object.banner_info[:file] %> <div id="banner"> <div class="row branding-banner-row"> @@ -55,7 +56,7 @@ <div class="col-sm-2"> <button class="btn btn-danger delete branding-banner-remove" data-type="DELETE" data-url="/" onclick=$("#banner").remove();> - <span class="glyphicon glyphicon-remove"></span> + <span class="fa fa-times"></span> <span class="controls-remove-text"><%= t('.remove') %></span> <span class="sr-only"> <%= t('.previous') %> @@ -68,7 +69,7 @@ <% if f.object.banner_info[:relative_path] %> <div class="banner-image"> <i><%= image_tag(f.object.banner_info[:relative_path], - height: "100", + size: "800x100", alt: f.object.banner_info[:alttext].presence || f.object.banner_info[:file]) %></i> </div> <% end %> @@ -97,28 +98,28 @@ <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> <div class="row fileupload-buttonbar"> - <div class="col-xs-4"> + <div class="col-4"> <!-- The fileinput-button span is used to style the file input field as button --> <span class="btn btn-success fileinput-button"> - <span class="glyphicon glyphicon-plus"></span> + <span class="fa fa-plus"></span> <span><%= t('.choose_file') %></span> <input type="file" name="files[]" single /> </span> - </div> <!-- end col-xs-4 --> + </div> <!-- end col-4 --> <!-- The global progress state --> - <div class="col-xs-8 fileupload-progress branding-logo-progress fade"> + <div class="col-8 fileupload-progress branding-logo-progress fade"> <!-- The global progress bar --> - <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100"> - <div class="progress-bar progress-bar-success" style="width:0%;"></div> + <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"> + <div class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width:0%;"></div> </div> <!-- The extended global progress state --> <div class="progress-extended"> </div> - </div> <!-- end col-xs-8 fileupload-progress branding-logo-progress fade --> + </div> <!-- end col-8 fileupload-progress branding-logo-progress fade --> </div> <!-- end row fileupload-buttonbar --> <div class="row branding-logo-list"> - <div class="col-xs-12"> + <div class="col-12"> <div class="container"> <% i = 0 %> <% f.object.logo_info.each_with_index do |linfo, i| %> @@ -144,7 +145,7 @@ <div class="col-sm-2"> <button class="btn btn-danger delete branding-logo-remove" data-type="DELETE" data-url="/" onclick=$("#logorow_<%= i %>").remove();> - <span class="glyphicon glyphicon-remove"></span> + <span class="fa fa-times"></span> <span class="controls-remove-text"><%= t('.remove') %></span> <span class="sr-only"> <%= t('.previous') %> @@ -159,7 +160,7 @@ <!-- The global file processing state --> <span class="fileupload-process"></span> - </div> <!-- end col-xs-12 --> + </div> <!-- end col-12 --> </div> <!-- end row branding-logo-list --> </div> <!-- end fileuploadlogo --> -</div> <!-- end set-access-controls --> \ No newline at end of file +</div> <!-- end set-access-controls --> diff --git a/app/views/hyrax/dashboard/collections/_form_discovery.html.erb b/app/views/hyrax/dashboard/collections/_form_discovery.html.erb index 5225832e6c..2163c824f3 100644 --- a/app/views/hyrax/dashboard/collections/_form_discovery.html.erb +++ b/app/views/hyrax/dashboard/collections/_form_discovery.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 +<%# OVERRIDE Hyrax v5.0.0rc2 - Handle new :manage_discovery ability - Reformat input tags for legibility %> @@ -9,55 +9,64 @@ <%# OVERRIDE: add conditional title %> <div class="form-group" <% if cannot?(:manage_discovery, @collection) %>title="<%= t('permissions.collections.cannot.manage_discovery') %>"<% end %>> - <label class="radio"> + <div class="custom-control custom-radio"> <input type="radio" id="visibility_open" name="<%= f.object_name %>[visibility]" value="<%= Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC %>" + class="custom-control-input" <% if @collection.open_access? %> - checked="true" + checked="checked" <% end %> <%# OVERRIDE: add conditional disable %> <% if cannot?(:manage_discovery, @collection) %> disabled="disabled" <% end %> /> - <strong><%= t('hyrax.visibility.open.text') %></strong> - <%= t('hyrax.visibility.open.note_html') %> - </label> + <label class="custom-control-label" for="visibility_open"> + <strong><%= t('hyrax.visibility.open.text') %></strong> - <%= t('hyrax.visibility.open.note_html') %> + </label> + </div> - <label class="radio"> + <div class="custom-control custom-radio"> <input type="radio" id="visibility_registered" name="<%= f.object_name %>[visibility]" value="<%= Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED %>" + class="custom-control-input" <% if @collection.authenticated_only_access? %> - checked="true" + checked="checked" <% end %> <%# OVERRIDE: add conditional disable %> <% if cannot?(:manage_discovery, @collection) %> disabled="disabled" <% end %> /> - <strong><%= t('hyrax.visibility.authenticated.text', institution: institution_name) %></strong> - <%= t('hyrax.visibility.authenticated.note_html', institution: institution_name) %> - </label> + <label class="custom-control-label" for="visibility_registered"> + <strong><%= t('hyrax.visibility.authenticated.text', institution: institution_name) %></strong> - <%= t('hyrax.visibility.authenticated.note_html', institution: institution_name) %> + </label> + </div> - <label class="radio"> + <div class="custom-control custom-radio"> <input type="radio" id="visibility_restricted" name="<%= f.object_name %>[visibility]" value="<%= Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE %>" + class="custom-control-input" <% if @collection.private_access? %> - checked="true" + checked="checked" <% end %> <%# OVERRIDE: add conditional disable %> <% if cannot?(:manage_discovery, @collection) %> disabled="disabled" <% end %> /> - <strong><%= t('hyrax.visibility.restricted.text') %></strong>- <%= t('hyrax.visibility.restricted.note_html') %> - </label> + <label class="custom-control-label" for="visibility_restricted"> + <strong><%= t('hyrax.visibility.restricted.text') %></strong> - <%= t('hyrax.visibility.restricted.note_html') %> + </label> + </div> </div> </div> diff --git a/app/views/hyrax/dashboard/collections/_form_share.html.erb b/app/views/hyrax/dashboard/collections/_form_share.html.erb index f5671e8fbf..0d9cf722b3 100644 --- a/app/views/hyrax/dashboard/collections/_form_share.html.erb +++ b/app/views/hyrax/dashboard/collections/_form_share.html.erb @@ -1,91 +1,90 @@ -<%# OVERRIDE Hyrax v3.4.2 Use Hyrax::Groups for groups select box %> +<%# OVERRIDE Hyrax v5.0.0rc2 Use Hyrax::Groups for groups select box %> <% access_options = options_for_select([ - [t('hyrax.dashboard.collections.form_share.access_options.manager'), 'manage'], - [t('hyrax.dashboard.collections.form_share.access_options.depositor'), 'deposit'], - [t('hyrax.dashboard.collections.form_share.access_options.viewer'), 'view']]) %> + [t('hyrax.dashboard.collections.form_share.access_options.manager'), t('manage')], + [t('hyrax.dashboard.collections.form_share.access_options.depositor'), t('deposit')], + [t('hyrax.dashboard.collections.form_share.access_options.viewer'), t('view')]]) %> <div id="participants" class="tab-pane"> - <div class="panel panel-default labels edit-sharing-tab"> - <div class="panel-body"> + <div class="edit-sharing-tab"> + <section class="section-add-sharing"> + <p><%= t('.note') %></p> + <h3><%= t('.add_sharing') %></h3> - <section class="section-add-sharing"> - <p><%= t('.note') %></p> - <h3><%= t('.add_sharing') %></h3> + <!-- Add group form --> + <div class="form-add-sharing-wrapper" data-id="<%= @form.id %>"> + <%= simple_form_for collection_permission_template_form_for(form: @form), + url: [hyrax, :dashboard, @form, :permission_template], + html: { id: 'group-participants-form' } do |f| %> - <!-- Add group form --> - <div class="form-add-sharing-wrapper" data-id="<%= @form.id %>"> - <%= simple_form_for @form.permission_template, - url: [hyrax, :dashboard, @form, :permission_template], - html: { id: 'group-participants-form' } do |f| %> - <div class="clearfix spacer"> - <%= f.fields_for 'access_grants_attributes', - f.object.access_grants.build(agent_type: 'group'), - index: 0 do |builder| %> + <div class="form-inline add-sharing-form sharing-row-form"> + <%= f.fields_for 'access_grants_attributes', + f.object.access_grants.build(agent_type: 'group'), + index: 0 do |builder| %> - <div class="form-inline add-sharing-form"> - <label class="col-md-2 col-xs-4 control-label"><%= t('.add_group') %>:</label> - <div class="col-md-10 col-xs-8 form-group"> - <%= builder.hidden_field :agent_type %> + <div class="form-group mr-2"> + <label class="mr-2"><%= t('.add_group') %>:</label> + <%= builder.hidden_field :agent_type %> + <%= builder.text_field :agent_id, + placeholder: t('.search_for_a_group'), + class: 'form-control search-input' %> + </div> + <div class="form-group"> + <label class="mr-2">as</label> <%# OVERRIDE: Change select to use Hyrax::Groups %> <%= builder.select :agent_id, Hyrax::Group.all.map { |g| [g.humanized_name, g.name] }, - { prompt: "Select a group..." }, + { prompt: t('.select_a_group') }, class: 'form-control' %> - as - <%= builder.select :access, - access_options, - { prompt: "Select a role..." }, - class: 'form-control' %> - - <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info edit-collection-add-sharing-button', :disabled => true %> - </div> - </div> - <% end %> - </div> - <% end %> - </div> + </div> + <% end %> + <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info edit-collection-add-sharing-button ml-2', :disabled => true %> - <!-- Add user form --> - <div class="form-add-sharing-wrapper" data-id="<%= @form.id %>"> - <%= simple_form_for @form.permission_template, - url: [hyrax, :dashboard, @form, :permission_template], - html: { id: 'user-participants-form' } do |f| %> - <div class="clearfix spacer"> + </div><!-- /.form-inline --> + <% end %> + </div> - <%= f.fields_for 'access_grants_attributes', - f.object.access_grants.build(agent_type: 'user'), - index: 0 do |builder| %> + <!-- Add user form --> + <div class="form-add-sharing-wrapper" data-id="<%= @form.id %>"> + <%= simple_form_for collection_permission_template_form_for(form: @form), + url: [hyrax, :dashboard, @form, :permission_template], + html: { id: 'user-participants-form' } do |f| %> - <div class="form-inline add-users"> - <label class="col-md-2 col-xs-4 control-label"><%= t('.add_user') %>:</label> - <div class="col-md-10 col-xs-8 form-group"> - <%= builder.hidden_field :agent_type %> - <%= builder.text_field :agent_id, - placeholder: "Search for a user..." %> - as - <%= builder.select :access, - access_options, - { prompt: "Select a role..." }, - class: 'form-control' %> + <div class="form-inline sharing-row-form add-users"> + <%= f.fields_for 'access_grants_attributes', + f.object.access_grants.build(agent_type: 'user'), + index: 0 do |builder| %> - <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info edit-collection-add-sharing-button', :disabled => true %> - </div> + <div class="form-group"> + <label class="mr-2"><%= t('.add_user') %>:</label> + <%= builder.hidden_field :agent_type %> + <%= builder.text_field :agent_id, + placeholder: t('.search_for_a_user') %> + </div> + <div class="form-group"> + <label class="mx-2">as</label> + <%= builder.select :access, + access_options, + { prompt: t('.select_a_role') }, + class: 'form-control' %> </div> - <% end %> - </div> - <% end %> - </div> + <% end %> + + <%= f.submit t('helpers.submit.hyrax_permission_template_access.create'), class: 'btn btn-info edit-collection-add-sharing-button ml-2', :disabled => true %> + + </div> <!-- /.form-inline --> + <% end %> + </div> + + <p class="form-text mt-2"><em><%= t('hyrax.admin.admin_sets.form.note') %></em></p> + </section> - <p class="help-block"><%= t('hyrax.admin.admin_sets.form.note') %></p> - </section> + <h2 class="h3"><%= t(".current_shared") %></h2> + <section class="section-collection-sharing"> - <section class="section-collection-sharing"> - <legend><%= t(".current_shared") %></legend> - <%= render 'form_share_table', access: 'managers', filter: :manage? %> - <%= render 'form_share_table', access: 'depositors', filter: :deposit? %> - <%= render 'form_share_table', access: 'viewers', filter: :view? %> - </section> - </div> + <%= render 'form_share_table', access: 'managers', filter: :manage? %> + <%= render 'form_share_table', access: 'depositors', filter: :deposit? %> + <%= render 'form_share_table', access: 'viewers', filter: :view? %> + </section> </div> </div> diff --git a/app/views/hyrax/dashboard/collections/_form_share_table.html.erb b/app/views/hyrax/dashboard/collections/_form_share_table.html.erb index 32b6f3dbce..e610153c73 100644 --- a/app/views/hyrax/dashboard/collections/_form_share_table.html.erb +++ b/app/views/hyrax/dashboard/collections/_form_share_table.html.erb @@ -1,36 +1,39 @@ -<%# OVERRIDE Hyrax v3.4.2 Filter out Role accesses as they should never be removed %> -<h3><%= t(".#{access}.title") %></h3> -<p><%= t(".#{access}.help") %></p> -<p><%= t(".#{access}.help_with_works", type_title: @collection.collection_type.title) if @collection.share_applies_to_new_works? && access != 'depositors' %></p> -<%# OVERRIDE: use custom filtering method to filter out access_grants for Collection Roles %> -<% if @form.filter_access_grants_by_access(filter).any? %> - <%# OVERRIDE: use custom access-specific class in order to differentiate between access tables %> - <table class='table table-striped share-status <%= "#{access}-table" %>'> - <thead> - <tr> - <th><%= t(".#{access}.agent_name") %></th> - <th><%= t(".#{access}.type") %></th> - <th><%= t(".#{access}.action") %></th> - </tr> - </thead> - <tbody> - <%# OVERRIDE: use custom filtering method to filter out access_grants for Collection Roles %> - <% @form.filter_access_grants_by_access(filter).each do |g| %> - <tr> - <td data-agent="<%= g.agent_id %>"><%= g.label %></td> - <td><%= g.agent_type.titleize %></td> - <td> - <%# OVERRIDE: only disable button if it is for the admin group's MANAGE access %> - <% if g.admin_group? && g.access == Hyrax::PermissionTemplateAccess::MANAGE %> - <%= link_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger disabled', disabled: true, title: t('hyrax.admin.admin_sets.form.permission_destroy_errors.admin_group') %> - <% else %> - <%= link_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger' %> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> -<% else %> - <p><em><%= t(".#{access}.empty") %></em></p> -<% end %> +<%# OVERRIDE Hyrax v5.0.0rc2 Filter out Role accesses as they should never be removed %> +<div class="mb-4"> + + <h3 class="h4"><%= t(".#{access}.title") %></h3> + <p><%= t(".#{access}.help") %></p> + <p><%= t(".#{access}.help_with_works", type_title: @collection_type.title) if @collection_type.share_applies_to_new_works? && access != 'depositors' %></p> + <%# OVERRIDE: use custom filtering method to filter out access_grants for Collection Roles %> + <% if @form.filter_access_grants_by_access(filter).any? %> + <%# OVERRIDE: use custom access-specific class in order to differentiate between access tables %> + <table class='table table-striped share-status <%= "#{access}-table" %>'> + <thead> + <tr> + <th><%= t(".#{access}.agent_name") %></th> + <th><%= t(".#{access}.type") %></th> + <th><%= t(".#{access}.action") %></th> + </tr> + </thead> + <tbody> + <%# OVERRIDE: use custom filtering method to filter out access_grants for Collection Roles %> + <% @form.filter_access_grants_by_access(filter).each do |g| %> + <tr> + <td data-agent="<%= g.agent_id %>"><%= g.label %></td> + <td><%= g.agent_type.titleize %></td> + <td> + <%# OVERRIDE: only disable button if it is for the admin group's MANAGE access %> + <% if g.admin_group? && g.access == Hyrax::PermissionTemplateAccess::MANAGE %> + <%= link_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger disabled', disabled: true, title: t('hyrax.admin.admin_sets.form.permission_destroy_errors.admin_group') %> + <% else %> + <%= link_to t(".#{access}.remove"), hyrax.admin_permission_template_access_path(g), method: :delete, class: 'btn btn-sm btn-danger' %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + <% else %> + <p><em><%= t(".#{access}.empty") %></em></p> + <% end %> +</div> diff --git a/app/views/hyrax/dashboard/collections/_list_collections.html.erb b/app/views/hyrax/dashboard/collections/_list_collections.html.erb index 2aa00e9078..e09609066e 100644 --- a/app/views/hyrax/dashboard/collections/_list_collections.html.erb +++ b/app/views/hyrax/dashboard/collections/_list_collections.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax 3.5.0 to add appropriate alt tag %> +<%# OVERRIDE Hyrax v5.0.0rc2 to add appropriate alt tag %> <% # used by All and Managed Collections tabs %> <% id = collection_presenter.id %> @@ -37,7 +37,7 @@ <%# Expand arrow %> <a href="#" class="small show-more" title="Click for more details"> - <i id="expand_<%= id %>" class="glyphicon glyphicon-chevron-right" aria-hidden="true"></i> + <i id="expand_<%= id %>" class="fa fa-chevron-right" aria-hidden="true"></i> <span class="sr-only"> <%= "#{t("hyrax.dashboard.my.sr.detail_label")} #{collection_presenter.title_or_label}" %></span> </a> </div> diff --git a/app/views/hyrax/dashboard/collections/_show_add_items_actions.html.erb b/app/views/hyrax/dashboard/collections/_show_add_items_actions.html.erb index b8d696ba86..eec369b219 100644 --- a/app/views/hyrax/dashboard/collections/_show_add_items_actions.html.erb +++ b/app/views/hyrax/dashboard/collections/_show_add_items_actions.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 Restrict who can deposit new works through collections %> +<%# OVERRIDE Hyrax v5.0.0rc2 Restrict who can deposit new works through collections %> <h2 class="sr-only"><%= t('hyrax.collection.actions.header') %></h2> <div class="text-right"> <%# OVERRIDE: add check for :manage_items_in_collection permission %> @@ -8,7 +8,7 @@ <%= link_to t('hyrax.collection.actions.add_new_work.label'), '#', title: t('hyrax.collection.actions.add_new_work.desc'), - data: { behavior: "select-work", target: "#worktypes-to-create", 'create-type' => 'single', add_works_to_collection: presenter.id }, + data: { behavior: "select-work", toggle: 'modal', target: "#worktypes-to-create", 'create-type' => 'single', add_works_to_collection: presenter.id }, class: 'btn btn-primary deposit-new-work-through-collection' %> <%# OVERRIDE: add #create_any_work_types? check %> <% elsif @presenter.create_any_work_types? # simple link to the first work type %> 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 9536b69a88..4b1bdeea9b 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 @@ -1,20 +1,22 @@ -<%# OVERRIDE Hyrax v3.4.2 Restrict who can remove works from collections %> +<%# OVERRIDE Hyrax v5.0.0rc2 Restrict who can remove works from collections %> <% id = document.id %> <tr id="document_<%= id %>"> <td>  <% if current_user and document.depositor != current_user.user_key %> - <i class="glyphicon glyphicon-share-alt" /> + <i class="fa fa-share" /> <% end %> </td> <td> <div class="media"> - <%= link_to [main_app, document], "class" => "media-left", "aria-hidden" => "true" do %> - <%= render_thumbnail_tag document, { class: "hidden-xs file_listing_thumbnail", alt: document.title_or_label }, { suppress_link: true } %> + <%= link_to [main_app, document], "class" => "mr-2", "aria-hidden" => "true" do %> + <%= document_presenter(document)&.thumbnail&.thumbnail_tag( + { class: "d-none d-md-block file_listing_thumbnail", alt: document.title_or_label }, { suppress_link: true } + ) %> <% end %> <div class="media-body"> <p class="media-heading"> <strong><%= link_to document.title_or_label, [main_app, document], id: "src_copy_link#{id}", class: "#{'document-title' if document.title_or_label == document.label}" %></strong> - <a href="#" class="small" title="Click for more details"><i id="expand_<%= id %>" class="glyphicon glyphicon-chevron-right"></i></a> + <a href="#" class="small" title="Click for more details"><i id="expand_<%= id %>" class="fa fa-chevron-right"></i></a> </p> <%= render_other_collection_links(document, @presenter.id) %> </div> @@ -26,21 +28,21 @@ <%= render_visibility_link(document) %> </td> <%# OVERRIDE: add check for :manage_items_in_collection permission %> - <% if current_user && can?(:edit, @collection) && can?(:manage_items_in_collection, @collection) %> + <% if current_user && can?(:edit, @collection) && can?(:manage_items_in_collection, @collection) %> <td class="text-center"> - <%= button_for_remove_from_collection @collection, document, label: "Remove", btn_class: "btn-danger btn-xs" %> + <%= button_for_remove_from_collection @collection, document, label: "Remove", btn_class: "btn-danger btn-sm" %> </td> <% end %> </tr> <tr id="detail_<%= id %>"> <!-- document detail"> --> <td colspan="6"> <dl class="expanded-details row"> - <dt class="col-xs-3 col-lg-2"><%= t('.creator') %></dt> - <dd class="col-xs-9 col-lg-4"><%= document.creator.to_a.to_sentence %></dd> - <dt class="col-xs-3 col-lg-2"><%= t('.depositor') %></dt> - <dd class="col-xs-9 col-lg-4"><%= link_to_profile document.depositor %></dd> - <dt class="col-xs-3 col-lg-2"><%= t('.edit_access') %></dt> - <dd class="col-xs-9 col-lg-10"> + <dt class="col-3 col-lg-2"><%= t('.creator') %></dt> + <dd class="col-9 col-lg-4"><%= document.creator.to_a.to_sentence %></dd> + <dt class="col-3 col-lg-2"><%= t('.depositor') %></dt> + <dd class="col-9 col-lg-4"><%= link_to_profile document.depositor %></dd> + <dt class="col-3 col-lg-2"><%= t('.edit_access') %></dt> + <dd class="col-9 col-lg-10"> <% if document.edit_groups.present? %> Groups: <%= document.edit_groups.join(', ') %> <br /> diff --git a/app/views/hyrax/dashboard/collections/_show_parent_collection_row.html.erb b/app/views/hyrax/dashboard/collections/_show_parent_collection_row.html.erb index 57712e092d..8eea2e28f7 100644 --- a/app/views/hyrax/dashboard/collections/_show_parent_collection_row.html.erb +++ b/app/views/hyrax/dashboard/collections/_show_parent_collection_row.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 Restrict who can remove parent collections from subcollections %> +<%# OVERRIDE Hyrax v5.0.0rc2 Restrict who can remove parent collections from subcollections %> <li data-id="<%= id %>" data-parent-id="<%= document.id %>" @@ -10,10 +10,10 @@ <% if can? :edit, id %> <%# OVERRIDE: add check for :manage_items_in_collection %> <% if can?(:edit, document.id) && can?(:manage_items_in_collection, document.id) %> - <button class="btn btn-xs btn-danger remove-from-collection-button"><%= t('hyrax.collections.show.buttons.remove_from_collection') %></button> + <button class="btn btn-sm btn-danger remove-from-collection-button"><%= t('hyrax.collections.show.buttons.remove_from_collection') %></button> <% else %> <%= link_to "#", - class: 'btn btn-xs btn-danger remove-parent-from-collection-deny-button', + class: 'btn btn-sm btn-danger remove-parent-from-collection-deny-button', title: t('hyrax.collections.show.buttons.remove_from_collection') do %> <%= t('hyrax.collections.show.buttons.remove_from_collection') %> <% end %> diff --git a/app/views/hyrax/dashboard/collections/_subcollection_list.html.erb b/app/views/hyrax/dashboard/collections/_subcollection_list.html.erb index a26bd9a82d..da9a286a52 100644 --- a/app/views/hyrax/dashboard/collections/_subcollection_list.html.erb +++ b/app/views/hyrax/dashboard/collections/_subcollection_list.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 Restrict who can remove subcollections from collection %> +<%# OVERRIDE Hyrax v5.0.0rc2 Restrict who can remove subcollections from collection %> <% if @subcollection_docs.nil? || @subcollection_docs.empty? %> <div class="alert alert-warning" role="alert"><%= t('hyrax.collections.show.no_visible_subcollections') %></div> <% else %> @@ -15,7 +15,7 @@ </div> <%# OVERRIDE: add :manage_items_in_collection check %> <% if can?(:edit, document.id) && can?(:manage_items_in_collection, id) %> - <button class="btn btn-xs btn-danger remove-subcollection-button"><%= t('hyrax.collections.show.buttons.remove_this_sub_collection') %></button> + <button class="btn btn-sm btn-danger remove-subcollection-button"><%= t('hyrax.collections.show.buttons.remove_this_sub_collection') %></button> <% end %> </div> </li> @@ -25,5 +25,5 @@ <% end %> <% if can? :edit, id %> - <%= render 'modal_remove_sub_collection' %> +<%= render 'modal_remove_sub_collection' %> <% end %> diff --git a/app/views/hyrax/dashboard/sidebar/_activity.html.erb b/app/views/hyrax/dashboard/sidebar/_activity.html.erb index a75c4eb07a..770f0b4492 100644 --- a/app/views/hyrax/dashboard/sidebar/_activity.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_activity.html.erb @@ -1,85 +1,97 @@ - <li class="h5"><%= t('hyrax.admin.sidebar.activity') %></li> +<%# OVERRIDE Hyrax v5.0.0rc2 to add title attribute to nav links %> +<li class="h5 nav-item"><%= t('hyrax.admin.sidebar.activity') %></li> - <li> - <%= menu.collapsable_section t('hyrax.admin.sidebar.repository_activity'), - icon_class: "fa fa-line-chart", - id: 'collapseRepositoryActivity', - open: menu.repository_activity_section?, - title: t('hyrax.admin.sidebar.repository_activity') do %> - <%= menu.nav_link(hyrax.dashboard_path, - title: t('hyrax.admin.sidebar.activity_summary')) do %> - <span class="fa fa-dashboard"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.activity_summary') %></span> - <% end %> - <% if menu.show_admin_menu_items? %> - <%= menu.nav_link(main_app.status_path, - title: t('hyrax.admin.sidebar.system_status')) do %> - <span class="fa fa-flag"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.system_status') %></span> - <% end %> +<li class="nav-item"> + <%= menu.collapsable_section t('hyrax.admin.sidebar.repository_activity'), + icon_class: "fa fa-line-chart", + id: 'collapseRepositoryActivity', + open: menu.repository_activity_section?, + title: t('hyrax.admin.sidebar.repository_activity') do %> + <%= menu.nav_link(hyrax.dashboard_path, + class: "nav-link", + title: t('hyrax.admin.sidebar.activity_summary')) do %> + <span class="fa fa-dashboard"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.activity_summary') %></span> + <% end %> + <% if menu.show_admin_menu_items? %> + <%= menu.nav_link(main_app.status_path, + class: "nav-link", + title: t('hyrax.admin.sidebar.system_status')) do %> + <span class="fa fa-flag"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.system_status') %></span> <% end %> <% end %> - </li> + <% end %> +</li> - <li> - <%= menu.collapsable_section t('hyrax.admin.sidebar.user_activity'), - icon_class: "fa fa-line-chart", - id: 'collapseUserActivity', - open: menu.user_activity_section?, - title: t('hyrax.admin.sidebar.user_activity') do %> - <%= menu.nav_link(hyrax.dashboard_profile_path(current_user), - also_active_for: hyrax.edit_dashboard_profile_path(current_user), - title: t('hyrax.admin.sidebar.profile')) do %> - <span class="fa fa-id-card" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.profile') %></span> - <% end %> +<li class="nav-item"> + <%= menu.collapsable_section t('hyrax.admin.sidebar.user_activity'), + icon_class: "fa fa-line-chart", + id: 'collapseUserActivity', + open: menu.user_activity_section?, + title: t('hyrax.admin.sidebar.user_activity') do %> + <%= menu.nav_link(hyrax.dashboard_profile_path(current_user), + class: "nav-link", + also_active_for: hyrax.edit_dashboard_profile_path(current_user), + title: t('hyrax.admin.sidebar.profile')) do %> + <span class="fa fa-id-card" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.profile') %></span> + <% end %> - <%= menu.nav_link(hyrax.notifications_path, - title: t('hyrax.admin.sidebar.notifications')) do %> - <span class="fa fa-bell" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.notifications') %></span> - <% end %> + <%= menu.nav_link(hyrax.notifications_path, + class: "nav-link", + title: t('hyrax.admin.sidebar.notifications')) do %> + <span class="fa fa-bell" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.notifications') %></span> + <% end %> - <%= menu.nav_link(hyrax.transfers_path, - title: t('hyrax.admin.sidebar.transfers')) do %> - <span class="fa fa-arrows-h" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.transfers') %></span> - <% end %> + <%= menu.nav_link(hyrax.transfers_path, + class: "nav-link", + title: t('hyrax.admin.sidebar.transfers')) do %> + <span class="fa fa-arrows-h" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.transfers') %></span> + <% end %> - <% if Flipflop.proxy_deposit? %> - <%= menu.nav_link(hyrax.depositors_path, - title: t('hyrax.dashboard.manage_proxies')) do %> - <span class="fa fa-users" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.dashboard.manage_proxies') %></span> - <% end %> + <% if Flipflop.proxy_deposit? %> + <%= menu.nav_link(hyrax.depositors_path, + class: "nav-link", + title: t('hyrax.dashboard.manage_proxies')) do %> + <span class="fa fa-users" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.dashboard.manage_proxies') %></span> <% end %> <% end %> - </li> + <% end %> +</li> - <% if can? :read, :admin_dashboard %> +<% if can? :read, :admin_dashboard %> + <li class="nav-item"> <%= menu.nav_link(hyrax.admin_stats_path, + class: "nav-link", title: t('hyrax.admin.sidebar.statistics')) do %> <span class="fa fa-bar-chart" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.statistics') %></span> <% end %> - <% end %> - - <% if current_ability.can_create_any_work? && Hyrax.config.analytics? %> - <li> - <%= menu.collapsable_section t('hyrax.admin.sidebar.analytics'), - icon_class: "fa fa-pie-chart", - id: 'collapseAnalytics', - open: menu.analytics_reporting_section?, - title: t('hyrax.admin.sidebar.analytics') do %> - <% if can? :read, :admin_dashboard %> - <%= menu.nav_link(hyrax.admin_analytics_collection_reports_path, - onclick: "dontChangeAccordion(event);", - title: t('hyrax.admin.sidebar.collections_report')) do %> - <span class="fa fa-folder-open" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.collections_report') %></span> - <% end %> - <% end %> + </li> +<% end %> - <%= menu.nav_link(hyrax.admin_analytics_work_reports_path, +<% if current_ability.can_create_any_work? && Hyrax.config.analytics? %> + <li class="nav-item"> + <%= menu.collapsable_section t('hyrax.admin.sidebar.analytics'), + icon_class: "fa fa-pie-chart", + id: 'collapseAnalytics', + open: menu.analytics_reporting_section?, + title: t('hyrax.admin.sidebar.analytics') do %> + <% if can? :read, :admin_dashboard %> + <%= menu.nav_link(hyrax.admin_analytics_collection_reports_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", - title: t('hyrax.admin.sidebar.works_report')) do %> - <span class="fa fa-file" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.works_report') %></span> + title: t('hyrax.admin.sidebar.collections_report')) do %> + <span class="fa fa-folder-open" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.collections_report') %></span> <% end %> + <% end %> + <%= menu.nav_link(hyrax.admin_analytics_work_reports_path, + class: "nav-link", + onclick: "dontChangeAccordion(event);", + title: t('hyrax.admin.sidebar.works_report')) do %> + <span class="fa fa-file" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.works_report') %></span> <% end %> - </li> - <% end %> - <%= render 'hyrax/dashboard/sidebar/menu_partials', menu: menu, section: :activity %> + <% end %> + </li> +<% end %> + +<%= render 'hyrax/dashboard/sidebar/menu_partials', menu: menu, section: :activity %> diff --git a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb index ec2bce02c5..85184aab07 100644 --- a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb @@ -1,63 +1,76 @@ +<%# OVERRIDE Hyrax v5.0.0rc2 add accoun settings and title attributes %> + <% if menu.show_configuration? %> - <li class="h5"><%= t('hyrax.admin.sidebar.configuration') %></li> + <li class="h5 nav-item"><%= t('hyrax.admin.sidebar.configuration') %></li> <% if can? :manage, Site %> - <li> + <li class="nav-item"> <%= menu.collapsable_section t('hyrax.admin.sidebar.settings'), icon_class: "fa fa-cog", id: 'collapseSettings', open: menu.settings_section?, title: t('hyrax.admin.sidebar.settings') do %> <%= menu.nav_link(main_app.edit_admin_account_path, + class: "nav-link", title: t('hyrax.admin.sidebar.account')) do %> <span class="fa fa-gear"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.account') %></span> <% end %> - <%= menu.nav_link(main_app.identity_providers_path) do %> - <span class="fa fa-key"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.identity_providers') %></span> + <%= menu.nav_link(main_app.identity_providers_path, class: "nav-link") do %> + <span class="fa fa-key"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.identity_provider') %></span> <% end %> <%= menu.nav_link(main_app.edit_site_labels_path, + class: "nav-link", title: t('hyrax.admin.sidebar.labels')) do %> <span class="fa fa-institution"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.labels') %></span> <% end %> <% if can?(:update, :appearance) %> <%= menu.nav_link(hyrax.admin_appearance_path, + class: "nav-link", title: t('hyrax.admin.sidebar.appearance')) do %> <span class="fa fa-paint-brush" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.appearance') %></span> <% end %> <% end %> <% if can?(:manage, :collection_types) %> <%= menu.nav_link(hyrax.admin_collection_types_path, + class: "nav-link", title: t('hyrax.admin.sidebar.collection_types')) do %> <span class="fa fa-folder-open" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.collection_types') %></span> <% end %> <% end %> <% if can?(:manage, Hyrax::Feature) %> <%= menu.nav_link(hyrax.edit_pages_path, + class: "nav-link", title: t('hyrax.admin.sidebar.pages')) do %> <span class="fa fa-file-text-o" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.pages') %></span> <% end %> <%= menu.nav_link(hyrax.edit_content_blocks_path, + class: "nav-link", title: t('hyrax.admin.sidebar.content_blocks')) do %> <span class="fa fa-square-o" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.content_blocks') %></span> <% end %> <%= menu.nav_link(hyrax.admin_features_path, + class: "nav-link", title: t('hyrax.admin.sidebar.technical')) do %> <span class="fa fa-wrench" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.technical') %></span> <% end %> <%= menu.nav_link('/admin/work_types/edit', + class: "nav-link", title: t('hyku.admin.work_types')) do %> <span class="fa fa-address-book"></span> <span class="sidebar-action-text"><%= t('hyku.admin.work_types') %></span> <% end %> <% end %> + <% end %> </li> <% end %> + <% if can?(:manage, Sipity::WorkflowResponsibility) %> <%= menu.nav_link(hyrax.admin_workflow_roles_path, + class: "nav-link", title: t('hyrax.admin.sidebar.workflow_roles')) do %> <span class="fa fa-users" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.workflow_roles') %></span> <% end %> - <% end # end of configuration block %> - <%= render 'hyrax/dashboard/sidebar/menu_partials', menu: menu, section: :configuration %> - <% end %> + <% end # end of configuration block %> + + <%= render 'hyrax/dashboard/sidebar/menu_partials', menu: menu, section: :configuration %> <% end %> diff --git a/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb b/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb index 556de0170c..fde8c553d8 100644 --- a/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb @@ -1,7 +1,8 @@ <%# override from bulkrax to make bulkrax a feature flipper %> -<li class="h5"><%= t('hyrax.admin.sidebar.repository_objects') %></li> +<li class="h5 nav-item"><%= t('hyrax.admin.sidebar.repository_objects') %></li> <%= menu.nav_link(hyrax.my_collections_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", also_active_for: hyrax.dashboard_collections_path, title: t('hyrax.admin.sidebar.collections')) do %> @@ -9,6 +10,7 @@ <% end %> <%= menu.nav_link(hyrax.my_works_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", also_active_for: hyrax.dashboard_works_path, title: t('hyrax.admin.sidebar.works')) do %> @@ -17,11 +19,13 @@ <% if ENV.fetch('HYKU_BULKRAX_ENABLED', 'true') == 'true' %> <%= menu.nav_link(bulkrax.importers_path, + class: "nav-link", title: t('bulkrax.admin.sidebar.importers')) do %> <span class="fa fa-cloud-upload" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('bulkrax.admin.sidebar.importers') %></span> <% end %> <%= menu.nav_link(bulkrax.exporters_path, + class: "nav-link", title: t('bulkrax.admin.sidebar.exporters')) do %> <span class="fa fa-cloud-download" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('bulkrax.admin.sidebar.exporters') %></span> <% end %> diff --git a/app/views/hyrax/dashboard/sidebar/_tasks.html.erb b/app/views/hyrax/dashboard/sidebar/_tasks.html.erb index 30a932867a..d10a645918 100644 --- a/app/views/hyrax/dashboard/sidebar/_tasks.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_tasks.html.erb @@ -1,9 +1,12 @@ +<%# OVERRIDE Hyrax v5.0.0rc2 to add Groups and title attributes %> + <% if menu.show_task? %> - <li class="h5"><%= t('hyrax.admin.sidebar.tasks') %></li> + <li class="h5 nav-item"><%= t('hyrax.admin.sidebar.tasks') %></li> <% end %> <% if can? :review, :submissions %> <%= menu.nav_link(hyrax.admin_workflows_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", title: t('hyrax.admin.sidebar.workflow_review')) do %> <span class="fa fa-flag" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.workflow_review') %></span> @@ -12,6 +15,7 @@ <% if can? :read, User %> <%= menu.nav_link(hyrax.admin_users_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", title: t('hyrax.admin.sidebar.users')) do %> <span class="fa fa-user" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.users') %></span> @@ -20,6 +24,7 @@ <% if can? :read, Hyrax::Group %> <%= menu.nav_link(main_app.admin_groups_path, + class: "nav-link", title: t('hyrax.admin.sidebar.manage_groups')) do %> <span class="fa fa-users"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.manage_groups') %></span> <% end %> @@ -27,12 +32,14 @@ <% if can? :read, :admin_dashboard %> <%= menu.nav_link(hyrax.embargoes_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", title: t('hyrax.embargoes.index.manage_embargoes')) do %> <span class="fa fa-flag" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.embargoes.index.manage_embargoes') %></span> <% end %> <%= menu.nav_link(hyrax.leases_path, + class: "nav-link", onclick: "dontChangeAccordion(event);", title: t('hyrax.leases.index.manage_leases')) do %> <span class="fa fa-flag" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.leases.index.manage_leases') %></span> diff --git a/app/views/hyrax/dashboard/works/_list_works.html.erb b/app/views/hyrax/dashboard/works/_list_works.html.erb index 4c0405c593..1b4c075603 100644 --- a/app/views/hyrax/dashboard/works/_list_works.html.erb +++ b/app/views/hyrax/dashboard/works/_list_works.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax 3.5.0 to add appropriate alt tag %> +<%# OVERRIDE Hyrax v5.0.0rc2 to add appropriate alt tag %> <tr id="document_<%= document.id %>"> <td> @@ -7,8 +7,13 @@ </td> <td> <div class='media'> - <%= link_to [main_app, document], class: 'media-left' do %> - <%= render_thumbnail_tag document, { class: 'hidden-xs file_listing_thumbnail', alt: block_for(name: 'default_work_image_text') || "#{document.title_or_label} #{t('hyrax.homepage.admin_sets.thumbnail')}" }, { suppress_link: true } %> + <%= link_to [main_app, document], class: 'mr-2' do %> + <%# OVERRIDE begin %> + <%= document_presenter(document)&.thumbnail&.thumbnail_tag( + { class: 'd-none d-md-block file_listing_thumbnail', alt: block_for(name: 'default_work_image_text') || "#{document.title_or_label} #{t('hyrax.homepage.admin_sets.thumbnail')}" }, + { suppress_link: true } + ) %> + <%# OVERRIDE end %> <% end %> <div class='media-body'> diff --git a/app/views/hyrax/homepage/_explore_collections.html.erb b/app/views/hyrax/homepage/_explore_collections.html.erb index 3a93a347ca..77e343781e 100644 --- a/app/views/hyrax/homepage/_explore_collections.html.erb +++ b/app/views/hyrax/homepage/_explore_collections.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE from Hyrax 2.9.0 to add featured collection functionality %> +<%# OVERRIDE from Hyrax v5.0.0rc2 to add featured collection functionality %> <% featured_collection = f.object.presenter %> <tr> @@ -7,7 +7,7 @@ <div> <%= link_to [hyrax, featured_collection] do %> <%= render_thumbnail_tag(featured_collection, - { class: 'img-responsive hidden-xs file_listing_thumbnail', alt: "#{featured_collection.title_or_label} #{ t('hyrax.homepage.admin_sets.thumbnail')}" }, + { class: 'img-fluid d-none d-sm-block file_listing_thumbnail', alt: "#{featured_collection.title_or_label} #{ t('hyrax.homepage.admin_sets.thumbnail')}" }, {suppress_link: true} ) %> <% end %> @@ -17,7 +17,6 @@ <%= featured_collection.title.first %> <% end %> </div> - </div> </div> </td> </tr> diff --git a/app/views/hyrax/homepage/_featured_collection_fields.html.erb b/app/views/hyrax/homepage/_featured_collection_fields.html.erb index 16b89026bd..70b1cfc490 100644 --- a/app/views/hyrax/homepage/_featured_collection_fields.html.erb +++ b/app/views/hyrax/homepage/_featured_collection_fields.html.erb @@ -2,16 +2,16 @@ <div class="sr-only"> <%= t('hyrax.homepage.featured_collections.document.title_label') %> </div> - <div class= 'homepage-work-thumbnail'> + <div class='homepage-work-thumbnail'> + <%= link_to [hyrax, featured] do %> + <%= render_thumbnail_tag(featured.solr_document, {suppress_link: true}) %> + <% end %> + </div> + <div> + <h5> <%= link_to [hyrax, featured] do %> - <%= render_thumbnail_tag(featured.solr_document, {suppress_link: true}) %> - <% end %> - </div> - <div> - <h5> - <%= link_to [hyrax, featured] do %> - <%= featured.title.first %> - <% end %> - </h5> - </div> -</div> \ No newline at end of file + <%= featured.title.first %> + <% end %> + </h5> + </div> +</div> diff --git a/app/views/hyrax/homepage/_featured_collection_section.html.erb b/app/views/hyrax/homepage/_featured_collection_section.html.erb index 728832826d..509ba7e877 100644 --- a/app/views/hyrax/homepage/_featured_collection_section.html.erb +++ b/app/views/hyrax/homepage/_featured_collection_section.html.erb @@ -1,18 +1,18 @@ - <% if @featured_collection_list.empty? %> - <p id='no-collections'><%= t('hyrax.homepage.featured_collections.no_collections') %></p> - <% elsif can? :update, FeaturedCollection %> +<% if @featured_collection_list.empty? %> + <p id='no-collections'><%= t('hyrax.homepage.featured_collections.no_collections') %></p> +<% elsif can? :update, FeaturedCollection %> <%= form_for [main_app, @featured_collection_list] do |f| %> - <div class="panel-group dd" id="ff"> + <div class="card-group dd" id="ff"> <ol id="featured_works"> <%= f.fields_for :featured_collections do |featured| %> <%= render 'sortable_featured_collections', f: featured %> <% end %> </ol> </div> - <%= f.submit("Save order", class: 'btn btn-default') %> + <%= f.submit("Save order", class: 'btn btn-secondary') %> <% end %> - <% else %> - <table class="table table-striped collection-highlights"> +<% else %> + <table class="table table-striped collection-highlights"> <tbody> <%= form_for [main_app, @featured_collection_list] do |f| %> <%= f.fields_for :featured_collections do |featured| %> @@ -21,12 +21,11 @@ <% end %> </tbody> </table> - <% end %> +<% end %> <ul class="list-inline collection-highlights-list"> <li> <%= link_to t('hyrax.homepage.admin_sets.link'), - main_app.search_catalog_path(f: { human_readable_type_sim: ["Collection"]}), - class: 'btn btn-default' %> + main_app.search_catalog_path(f: { human_readable_type_sim: ["Collection"]}), + class: 'btn btn-secondary' %> </li> </ul> - diff --git a/app/views/hyrax/homepage/_featured_fields.html.erb b/app/views/hyrax/homepage/_featured_fields.html.erb index 4c5e2e6309..3a16f1dca4 100644 --- a/app/views/hyrax/homepage/_featured_fields.html.erb +++ b/app/views/hyrax/homepage/_featured_fields.html.erb @@ -1,10 +1,12 @@ -<%# OVERRIDE Hyrax 2.9.1 to remove the hardcorded image width %> +<%# OVERRIDE Hyrax v5.0.0rc2 to remove the hardcorded image width %> <div> <div class="featured-item-title"> <span class="sr-only"><%= t('hyrax.homepage.featured_works.document.title_label') %></span> <h3> <%= link_to [main_app, featured] do %> + <%# OVERRIDE begin %> <%= render_thumbnail_tag(featured, {alt: "#{featured.title.first.to_s} #{ t('hyrax.homepage.admin_sets.thumbnail')}" }, {suppress_link: true}) + featured.title.first %> + <%# OVERRIDE end %> <% end %> </h3> </div> diff --git a/app/views/hyrax/homepage/_home_content.html.erb b/app/views/hyrax/homepage/_home_content.html.erb index ec03f25d9a..a5e32f42b9 100644 --- a/app/views/hyrax/homepage/_home_content.html.erb +++ b/app/views/hyrax/homepage/_home_content.html.erb @@ -1,49 +1,64 @@ -<%# Override from hyrax 2.5.1 to add feature flags to show/hide: +<%# Override from Hyrax v5.0.0rc2 to add feature flags to show/hide: featured researcher featured works recently_uploaded also, to render featured collections %> -<div class="<%= @presenter.display_featured_works? || @presenter.display_recently_uploaded? ? 'home-tabs-left col-sm-6' : ''%>"> +<div class="<%= @presenter.display_featured_works? || @presenter.display_recently_uploaded? ? 'col-md-6' : '' %>"> <ul id="homeTabs" class="nav nav-tabs" role="list"> <%# add check for featured works %> <% if @presenter.display_featured_works? %> - <li class="active"><a href="#featured_container" data-toggle="tab" id="featureTab"><%= t('hyrax.homepage.featured_works.tab_label') %></a></li> + <li class="nav-item"> + <a class="nav-link active" href="#featured_container" data-toggle="tab" id="featureTab"> + <%= t('hyrax.homepage.featured_works.tab_label') %> + </a> + </li> <% end %> <%# add check for recently uploaded %> <% if @presenter.display_recently_uploaded? %> - <li class="<%= @presenter.display_featured_works? ? '' : 'active' %>"><a href="#recently_uploaded" data-toggle="tab" id="recentTab"><%= t('hyrax.homepage.recently_uploaded.tab_label') %></a></li> + <li class="nav-item <%= @presenter.display_featured_works? ? '' : 'active' %>"> + <a class="nav-link" href="#recently_uploaded" data-toggle="tab" id="recentTab"> + <%= t('hyrax.homepage.recently_uploaded.tab_label') %> + </a> + </li> <% end %> </ul> <div class="tab-content"> <%# add check for featured works %> <% if @presenter.display_featured_works? %> - <div class="tab-pane fade in active" id="featured_container" role="tabpanel" aria-labelledby="featureTab"> + <div class="tab-pane show active" id="featured_container" role="tabpanel" aria-labelledby="featureTab"> <%= render 'featured_works' %> </div> <% end %> <%# add check for recently_uploaded %> <% if @presenter.display_recently_uploaded? %> - <div class="<%= @presenter.display_featured_works? ? 'tab-pane fade' : 'tab-pane active in' %>" id="recently_uploaded" role="tabpanel" aria-labelledby="recentTab"> + <div class="tab-pane <%= @presenter.display_featured_works? ? '' : 'show active' %>" id="recently_uploaded" role="tabpanel" aria-labelledby="recentTab"> <%= render 'recently_uploaded', recent_documents: @recent_documents %> </div> - <% end %> + <% end %> </div> -</div><!-- /.col-xs-6 --> +</div><!-- /.col-md-6 --> -<div class="home-tabs-right col-sm-6"> +<div class="col-md-6"> <ul class="nav nav-tabs" role="list"> - <li class="active"><a aria-expanded="true" href="#tab-col2-first" data-toggle="tab"><%= t('hyrax.homepage.admin_sets.title') %></a></li> - <%# add check for featured researcher %> + <li class="nav-item"> + <a class="nav-link active" aria-expanded="true" href="#tab-col2-first" data-toggle="tab"> + <%= t('hyrax.homepage.admin_sets.title') %> + </a> + </li> <% if @presenter.display_featured_researcher? %> - <li class=""><a aria-expanded="false" href="#tab-col2-second" data-toggle="tab"><%= t('hyrax.homepage.featured_researcher.tab_label') %></a></li> + <li class="nav-item"> + <a class="nav-link" aria-expanded="false" href="#tab-col2-second" data-toggle="tab"> + <%= t('hyrax.homepage.featured_researcher.tab_label') %> + </a> + </li> <% end %> </ul> <div class="tab-content"> - <div class="tab-pane active" id="tab-col2-first"> + <div class="tab-pane show active" id="tab-col2-first"> <h2 class="sr-only"><%= t('hyrax.homepage.admin_sets.title') %></h2> <%= render 'featured_collection_section' %> </div> diff --git a/app/views/hyrax/homepage/_home_text.html.erb b/app/views/hyrax/homepage/_home_text.html.erb index 596f12d9a8..558fd86eac 100644 --- a/app/views/hyrax/homepage/_home_text.html.erb +++ b/app/views/hyrax/homepage/_home_text.html.erb @@ -1,7 +1,9 @@ -# frozen_string_literal: true - <% if display_content_block? @home_text %> - <div class="home_page_text"> - <%= displayable_content_block @home_text, class: 'col-sm-3 home_page_text' %> + <div class="row"> + <div class="col-sm-3"> + <div class="home_page_text"> + <%= displayable_content_block @home_text %> + </div> + </div> </div> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/homepage/_sortable_featured_collections.html.erb b/app/views/hyrax/homepage/_sortable_featured_collections.html.erb index 620f7b8e7d..95f99cf43c 100644 --- a/app/views/hyrax/homepage/_sortable_featured_collections.html.erb +++ b/app/views/hyrax/homepage/_sortable_featured_collections.html.erb @@ -3,17 +3,17 @@ <div class="dd-handle dd3-handle"> <%= t 'hyrax.homepage.featured_works.drag' %> </div> - <div class="dd3-content panel panel-default"> + <div class="dd3-content card"> <%= f.hidden_field :id %> <%= f.hidden_field :order, data: { property: "order" } %> - <div class="main row"> - <div class="col-sm-12"> + <div class="card-body row"> + <div class="col-md-12"> <% if can? :destroy, FeaturedCollection %> - <h3 class="pull-right"> + <h3 class="float-right"> <%= link_to main_app.featured_collection_path(presenter, format: :json), title: "Remove collection from feature section", data: {behavior: 'unfeature'} do %> - <i class='glyphicon glyphicon-remove'></i> + <i class='fas fa-times'></i> <% end %> <p class="sr-only"><%= t 'helpers.action.remove' %></p> </h3> diff --git a/app/views/hyrax/my/_collection_action_menu.html.erb b/app/views/hyrax/my/_collection_action_menu.html.erb index f5f92ca9ab..98cc23db77 100644 --- a/app/views/hyrax/my/_collection_action_menu.html.erb +++ b/app/views/hyrax/my/_collection_action_menu.html.erb @@ -1,15 +1,14 @@ -<%# OVERRIDE Hyrax v3.4.2 Adjust collection deletion permissions %> +<%# OVERRIDE Hyrax v5.0.0rc2 Adjust collection deletion permissions %> <% id = collection_presenter.id %> <% ul_id = 'collection-action-dropdown-ul-' + id %> <div class="btn-group"> <button class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" type="button" id="dropdownMenu_<%= id %>" aria-haspopup="true" aria-expanded="false" aria-controls="<%= ul_id %>"> <span class="sr-only"><%= t("hyrax.dashboard.my.sr.press_to") %> </span> - <%= t("hyrax.dashboard.my.action.select") %> <span class="caret" aria-hidden="true"></span> + <%= t("hyrax.dashboard.my.action.select") %> </button> - <ul role="menu" id="<%= ul_id %>" class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu_<%= id %>"> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= link_to hyrax.dashboard_collection_path(id), class: 'itemicon itemedit', title: t("hyrax.dashboard.my.action.view_collection") do %> @@ -17,7 +16,7 @@ <% end %> </li> <% if can? :edit, collection_presenter.solr_document %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= link_to hyrax.edit_dashboard_collection_path(id), class: 'itemicon itemedit', title: t("hyrax.dashboard.my.action.edit_collection") do %> @@ -25,7 +24,7 @@ <% end %> </li> <% else %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= link_to "#", class: 'itemicon itemedit edit-collection-deny-button', title: t("hyrax.dashboard.my.action.edit_collection") do %> @@ -33,7 +32,7 @@ <% end %> </li> <% end %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%# OVERRIDE: change hasaccess to check if a user can destroy the solr_doc, not just if they can edit it %> <%= link_to "#", class: 'itemicon itemtrash delete-collection-button', @@ -47,7 +46,7 @@ <% if Hyrax::CollectionType.any_nestable? %> <% # The user should have deposit access to the parent we are adding, and read access to the child (the collection we are linking here). %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= link_to "#", class: 'itemicon add-to-collection', title: t("hyrax.dashboard.my.action.add_to_collection"), diff --git a/app/views/hyrax/my/_work_action_menu.html.erb b/app/views/hyrax/my/_work_action_menu.html.erb index 70e94b17f4..61a02efc13 100644 --- a/app/views/hyrax/my/_work_action_menu.html.erb +++ b/app/views/hyrax/my/_work_action_menu.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax v3.4.2 Adjust #can? permission checks %> +<%# OVERRIDE Hyrax v5.0.0rc2 Adjust #can? permission checks %> <% ul_id = 'admin-set-action-dropdown-ul-' + document.id %> <div class="btn-group"> @@ -6,43 +6,43 @@ <button class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" type="button" id="dropdownMenu_<%= document.id %>" aria-haspopup="true" aria-expanded="false" aria-controls="<%= ul_id %>"> <span class="sr-only"><%= t("hyrax.dashboard.my.sr.press_to") %> </span> <%= t("hyrax.dashboard.my.action.select") %> - <span class="caret" aria-hidden="true"></span> </button> <ul role="menu" id="<%= ul_id %>" class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu_<%= document.id %>"> <% if can? :edit, document.id %> - <li role="menuitem" tabindex="-1"> - <%= link_to [main_app, :edit, document] do %> - <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> + <li class="dropdown-item" role="menuitem" tabindex="-1"> + <%= link_to [main_app, :edit, document], + id: 'action-edit-work' do %> + <i class="fa fa-pencil" aria-hidden="true"></i> <span> <%= t("hyrax.dashboard.my.action.edit_work") %> </span> <% end %> </li> <% end %> - <%# OVERRIDE: add separate #can? check for :delete instead of checking for :edit %> <% if can? :delete, document %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= link_to [main_app, document], method: :delete, + id: 'action-delete-work', data: { confirm: t("hyrax.dashboard.my.action.work_confirmation", application_name: application_name) } do %> - <i class="glyphicon glyphicon-trash" aria-hidden="true"></i> + <i class="fa fa-trash" aria-hidden="true"></i> <span> <%= t("hyrax.dashboard.my.action.delete_work") %> </span> <% end %> </li> <% end %> - <li role="menuitem" tabindex="-1"> + <li class="dropdown-item" role="menuitem" tabindex="-1"> <%= display_trophy_link(current_user, document.id) do |text| %> - <i class="glyphicon glyphicon-star" aria-hidden="true"></i> <%= text %> + <i class="fa fa-star" aria-hidden="true"></i> <%= text %> <% end %> </li> <% if can? :transfer, document.id %> - <li role="menuitem" tabindex="-1"> - <%= link_to(hyrax.new_work_transfer_path(document.id), class: 'itemicon itemtransfer', title: t("hyrax.dashboard.my.action.transfer")) do %> - <i class="glyphicon glyphicon-transfer" aria-hidden="true"></i> + <li class="dropdown-item" role="menuitem" tabindex="-1"> + <%= link_to(hyrax.new_work_transfer_path(document.id), id: 'action-transfer-work', class: 'itemicon itemtransfer', title: t("hyrax.dashboard.my.action.transfer")) do %> + <i class="fa fa-exchange" aria-hidden="true"></i> <span> <%= t("hyrax.dashboard.my.action.transfer") %> </span> <% end %> </li> diff --git a/app/views/hyrax/my/collections/_list_collections.html.erb b/app/views/hyrax/my/collections/_list_collections.html.erb index 7450a51e38..31efaec0fa 100644 --- a/app/views/hyrax/my/collections/_list_collections.html.erb +++ b/app/views/hyrax/my/collections/_list_collections.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax 3.5.0 to add appropriate alt tag %> +<%# OVERRIDE Hyrax v5.0.0rc2 to add appropriate alt tag %> <% # used by Your Collections tab %> <% id = collection_presenter.id %> @@ -39,7 +39,7 @@ <%# Expand arrow %> <a href="#" class="small show-more" title="Click for more details"> - <i id="expand_<%= id %>" class="glyphicon glyphicon-chevron-right" aria-hidden="true"></i> + <i id="expand_<%= id %>" class="fa fa-chevron-right" aria-hidden="true"></i> <span class="sr-only"> <%= "#{t("hyrax.dashboard.my.sr.detail_label")} #{collection_presenter.title_or_label}" %></span> </a> </div> diff --git a/app/views/hyrax/my/works/_list_works.html.erb b/app/views/hyrax/my/works/_list_works.html.erb index 7b3424bad2..5eca59eda1 100644 --- a/app/views/hyrax/my/works/_list_works.html.erb +++ b/app/views/hyrax/my/works/_list_works.html.erb @@ -1,4 +1,4 @@ -<%# OVERRIDE Hyrax 3.5.0 to add appropriate alt tag %> +<%# OVERRIDE Hyrax v5.0.0rc2 to add appropriate alt tag %> <tr id="document_<%= document.id %>"> @@ -9,14 +9,14 @@ <td> <div class='media'> - <%= link_to [main_app, document], class: 'media-left' do %> - <%= render_thumbnail_tag document, { class: 'hidden-xs file_listing_thumbnail', alt: block_for(name: 'default_work_image_text') || "#{document.title_or_label} #{t('hyrax.homepage.admin_sets.thumbnail')}" }, { suppress_link: true } %> + <%# OVERRIDE begin %> + <%= link_to [main_app, document], class: 'mr-2' do %> + <%= render_thumbnail_tag document, { class: 'd-none d-md-block file_listing_thumbnail', alt: block_for(name: 'default_work_image_text') || "#{document.title_or_label} #{t('hyrax.homepage.admin_sets.thumbnail')}" }, { suppress_link: true } %> <% end %> + <%# OVERRIDE end %> <div class='media-body'> - <div class='media-heading'> - - <%= link_to [main_app, document], id: "src_copy_link#{document.id}", class: 'document-title' do %> + <%= link_to [main_app, document], id: "src_copy_link#{document.id}", class: 'document-title' do %> <span class="sr-only"> <%= t("hyrax.dashboard.my.sr.show_label") %> </span> @@ -25,8 +25,6 @@ <br /> <%= render_collection_links(document) %> - - </div> </div> </div> </td> diff --git a/app/views/hyrax/uploads/_js_templates_branding.html.erb b/app/views/hyrax/uploads/_js_templates_branding.html.erb index 7acaa66117..be00d2b198 100644 --- a/app/views/hyrax/uploads/_js_templates_branding.html.erb +++ b/app/views/hyrax/uploads/_js_templates_branding.html.erb @@ -1,8 +1,8 @@ - <!-- OVERRIDE Hyrax v3.4.0 to add text for collection banner image --> +<!-- OVERRIDE Hyrax v5.0.0rc2 to add text for collection banner image --> <!-- The template to display files available for upload --> <script id="template-upload" type="text/x-tmpl"> {% for (var i=0, file; file=o.files[i]; i++) { %} - <tr class="template-upload fade"> + <tr class="template-upload fade show"> <td> <span class="preview"></span> </td> @@ -38,7 +38,7 @@ <!-- The template to display the banner once upload is complete --> <script id="template-download" type="text/x-tmpl"> {% for (var i=0, file; file=o.files[i]; i++) { %} - <span class="template-download fade"> + <span class="template-download fade show"> <div id="banner"> <div class="row branding-banner-row"> <div class="col-sm-3"> @@ -47,20 +47,18 @@ <input type="hidden" name="banner_files[]" value="{%=file.id%}"> </span> {% if (file.error) { %} - <span><span class="label label-danger"><%= t('.error') %></span> {%=file.error%}</span> + <span><span class="badge badge-danger"><%= t('.error') %></span> {%=file.error%}</span> {% } %} </div> - <div class="col-sm-4 branding-banner-input"> - <label for="banner_text"><%= t('.alt_text') %> - <input id="banner_text" type="text" name="banner_text[]" class="branding-banner-input" single> - </label> + <label for="banner_text"><%= t('.alt_text') %></label> + <input id="banner_text" type="text" name="banner_text[]" class="form-control branding-banner-input" single> </div> <div class="col-sm-2"> <button class="btn btn-link remove branding-banner-remove" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}" onclick=$("#banner").remove(); {% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}> - <span class="glyphicon glyphicon-remove"></span> + <span class="fa fa-times"></span> <span class="controls-remove-text"><%= t('.remove') %></span> <span class="sr-only"> <%= t('.previous') %> @@ -77,7 +75,7 @@ <!-- The template to display logo in the table once upload is complete --> <script id="logo-template-download" type="text/x-tmpl"> {% for (var i=0, file; file=o.files[i]; i++) { %} - <span class="template-download fade"> + <span class="template-download fade show"> <div class="row branding-logo-row"> <div class="col-sm-3"> <span class="preview"> @@ -96,7 +94,7 @@ </span> {% if (file.error) { %} - <span><span class="label label-danger">Error</span> {%=file.error%}</span> + <span><span class="badge badge-danger">Error</span> {%=file.error%}</span> {% } %} </div> @@ -112,7 +110,7 @@ <div class="col-sm-2 text-right"> <span class="input-group-btn field-controls"> <button class="btn btn-sm btn-danger delete branding-logo-remove" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}> - <span class="glyphicon glyphicon-remove"></span> + <span class="fa fa-times"></span> <span class="controls-remove-text">Remove</span> <span class="sr-only"> <%= t('.previous') %> @@ -125,4 +123,4 @@ </div> </span> {% } %} -</script> \ No newline at end of file +</script> diff --git a/app/views/hyrax/users/_vitals.html.erb b/app/views/hyrax/users/_vitals.html.erb deleted file mode 100644 index a5631a8a8a..0000000000 --- a/app/views/hyrax/users/_vitals.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%# override Hyrax 2.5 - fix Collections Created and Works Created links. Confirmed can be removed after Hyrax 3.0 %> -<div class="list-group-item"> - <span class="glyphicon glyphicon-time" aria-hidden="true"></span> <%= t("hyrax.dashboard.stats.joined_on") %> <%= user.created_at.to_date.strftime("%b %d, %Y") %> -</div> - -<div class="list-group-item"> - <span class="badge"><%= number_of_collections(user) %></span> - <span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> - <%= link_to_field('', '', t("hyrax.dashboard.stats.collections"), {generic_type: "Collection", depositor: user.to_s}) %> -</div> - -<div class="list-group-item"> - <span class="badge"><%= number_of_works(user) %></span> - <span class="glyphicon glyphicon-upload" aria-hidden="true"></span> - <%= link_to_field('', '', t("hyrax.dashboard.stats.works"), {generic_type: "Work", depositor: user.to_s}) %> - - <ul class="views-downloads-dashboard list-unstyled"> - <li><span class="badge badge-optional"><%= user.total_file_views %></span> <%= t("hyrax.dashboard.stats.file_views").pluralize(user.total_file_views) %></li> - <li><span class="badge badge-optional"><%= user.total_file_downloads %></span> <%= t("hyrax.dashboard.stats.file_downloads").pluralize(user.total_file_downloads) %></li> - </ul> -</div> diff --git a/app/views/identity_providers/_form.html.erb b/app/views/identity_providers/_form.html.erb index fd2d41430b..499cc0fbe5 100644 --- a/app/views/identity_providers/_form.html.erb +++ b/app/views/identity_providers/_form.html.erb @@ -1,6 +1,6 @@ -<div class="panel panel-default"> - <%= simple_form_for(@identity_provider) do |f| %> - <div class="panel-body"> +<%= simple_form_for(@identity_provider) do |f| %> + <div class="card"> + <div class="card-body"> <% if @identity_provider.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@identity_provider.errors.count, "error") %> prohibited this authentication provider from being saved:</h2> @@ -11,7 +11,7 @@ </ul> </div> <% end %> - <%= f.input :name %> + <%= f.input :name, label: t('hyku.identity_provider.label.name'), required: true %> <%= f.input :provider, collection: Devise.omniauth_providers.map {|o| [o, o.upcase]}, label_method: :second, @@ -38,22 +38,21 @@ <p>Metadata is available <%= link_to 'here', "/users/auth/saml/#{@identity_provider.id}/metadata", data: { turbolinks: false } %></p> <% end %> - <%= f.input :options, input_html: {value: @identity_provider.options&.to_json } %> + <%= f.input :options, label: t('hyku.identity_provider.label.options'), input_html: {value: @identity_provider.options&.to_json } %> <%# Upload Logo Image %> - <%= f.input :logo_image, as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.logo_image.hint') %> - <%= f.input :logo_image_text, as: :text %> + <%= 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? %> </div> - <div class="panel-footer text-right"> + <div class="card-footer text-right"> <% if IdentityProvider.count.nonzero? %> - <%= link_to 'Delete', identity_provider, method: :delete, data: { confirm: 'Are you sure?' }, class: - 'btn btn-danger' %> + <%= link_to 'Delete', identity_provider, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' %> <% end %> <%= f.submit class: 'btn btn-primary action-save' %> </div> - <% end %> -</div> + </div> +<% end %> diff --git a/app/views/identity_providers/index.html.erb b/app/views/identity_providers/index.html.erb index 594fe23d54..80a459f93c 100644 --- a/app/views/identity_providers/index.html.erb +++ b/app/views/identity_providers/index.html.erb @@ -1,18 +1,18 @@ <% content_for :page_header do %> - <h1><span class="fa fa-user"></span> <%= t(:'hyrax.admin.sidebar.identity_providers_and_permissions') %></h1> + <h1><span class="fa fa-user"></span> <%= t('hyku.identity_provider.header') %></h1> <% end %> <div class="row"> <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-body"> + <div class="card"> + <div class="card-body"> <table class="table table-striped datatable"> <thead> <tr> - <th>Name</th> - <th>Provider</th> - <th>Updated At</th> - <th>Logo</th> + <th><%= t('hyku.identity_provider.label.name') %></th> + <th><%= t('hyku.identity_provider.label.provider') %></th> + <th><%= t('hyku.identity_provider.label.updated_at') %></th> + <th><%= t('hyku.identity_provider.label.logo') %></th> <th></th> </tr> </thead> @@ -22,14 +22,17 @@ <td><%= u.name %></td> <td><%= u.provider %></td> <td><%= u.updated_at %></td> - <td><%= image_tag u.logo_image.url(:thumb), class: "img-responsive", alt: u.logo_image_text if u.logo_image? %></td> - <td class="col-md-2"><%= link_to t('.edit'), edit_identity_provider_path(u) %> | <%= link_to t('helpers.action.delete'), identity_provider_path(u), method: :delete, data: { confirm: t('.confirm_delete') } %></td> + <td><%= image_tag u.logo_image.url(:thumb), class: "img-fluid", alt: u.logo_image_text if u.logo_image? %></td> + <td class="col-md-2"> + <%= link_to t('hyku.identity_provider.label.edit'), edit_identity_provider_path(u) %> |  + <%= link_to t('helpers.action.delete'), identity_provider_path(u), method: :delete, data: { confirm: t('.confirm_delete') } %> + </td> </tr> <% end %> </tbody> </table> <%= link_to new_identity_provider_path, class: 'btn btn-primary' do %> - <span class="fa fa-edit"></span> <%= t('.create_new') %> + <span class="fa fa-edit"></span> <%= t('hyku.identity_provider.label.create_new') %> <% end %> </div> </div> diff --git a/app/views/labels/edit.html.erb b/app/views/labels/edit.html.erb index 270c44efba..604efc3379 100644 --- a/app/views/labels/edit.html.erb +++ b/app/views/labels/edit.html.erb @@ -4,40 +4,39 @@ <div class="row"> <div class="col-md-12"> - <div class="panel panel-default"> + <div class="card"> <%= form_for(@site, url: site_labels_path) do |f| %> - <div class="panel-heading"> - <h3 class="panel-title">General Repository Labels</h3> + <div class="card-header"> + <h3 class="card-title">General Repository Labels</h3> </div> - <div class="panel-body"> + <div class="card-body"> <% if @site.errors.any? %> - <div id="error_explanation"> - <h2><%= pluralize(@site.errors.count, "error") %> prohibited this site from being saved:</h2> - - <ul> - <% @site.errors.full_messages.each do |message| %> - <li><%= message %></li> - <% end %> - </ul> - </div> + <div id="error_explanation"> + <h2><%= pluralize(@site.errors.count, "error") %> prohibited this site from being saved:</h2> + <ul> + <% @site.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + </div> <% end %> <div class="field form-group"> - <%= f.label :application_name %><br> - <%= f.text_field :application_name, class: 'form-control'%> + <%= f.label :application_name %><br> + <%= f.text_field :application_name, class: 'form-control'%> </div> <div class="field form-group"> - <%= f.label :institution_name %><br> - <%= f.text_field :institution_name, class: 'form-control'%> + <%= f.label :institution_name %><br> + <%= f.text_field :institution_name, class: 'form-control'%> </div> <div class="field form-group"> - <%= f.label :institution_name_full %><br> - <%= f.text_field :institution_name_full, class: 'form-control'%> + <%= f.label :institution_name_full %><br> + <%= f.text_field :institution_name_full, class: 'form-control'%> </div> </div> - <div class="panel-footer"> - <%= f.submit class: 'btn btn-primary pull-right' %> + <div class="card-footer text-right"> + <%= f.submit class: 'btn btn-primary' %> </div> <% end %> </div> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d6c1c0ab3c..0f1c161e7e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ <!DOCTYPE html> <html> <head> - <title>Hyku + <%= Hyku::Application.html_head_title %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> diff --git a/app/views/layouts/hyrax.html.erb b/app/views/layouts/hyrax.html.erb index e2ff69136d..0387e147f5 100644 --- a/app/views/layouts/hyrax.html.erb +++ b/app/views/layouts/hyrax.html.erb @@ -12,15 +12,15 @@
    <%= link_to "Skip to Content", "#skip-to-content" %>
    - <%= render '/masthead' %> + <%= render '/masthead', placement_class: nil %> <%= content_for(:navbar) %> <%= content_for(:precontainer_content) %>
    <%= render '/flash_msg' %> - <%= render_breadcrumbs builder: Hyrax::BootstrapBreadcrumbsBuilder %> + <%= render_breadcrumbs builder: Hyrax.config.breadcrumb_builder %> <% if content_for?(:page_header) %>
    -
    +
    <%= yield(:page_header) %>
    @@ -31,6 +31,6 @@ <%= content_for?(:content) ? yield(:content) : yield %>
    <%= render 'shared/footer' %> - <%= render 'shared/ajax_modal' %> + <%= render 'shared/modal' %> diff --git a/app/views/proprietor/accounts/edit.html.erb b/app/views/proprietor/accounts/edit.html.erb index 56b82d6add..b835d66a13 100644 --- a/app/views/proprietor/accounts/edit.html.erb +++ b/app/views/proprietor/accounts/edit.html.erb @@ -4,9 +4,9 @@
    -