diff --git a/Gemfile b/Gemfile index 868c97ba3..8cda67972 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,9 @@ gem 'addressable', '2.8.1' # remove once https://github.com/postrank-labs/postra gem 'apartment' gem 'aws-sdk-sqs', group: %i[aws] gem 'blacklight', '~> 6.7' +gem 'blacklight_advanced_search' gem 'blacklight_oai_provider', '~> 6.1', '>= 6.1.1' +gem 'blacklight_range_limit', '6.5.0' gem 'bolognese', '>= 1.9.10' gem 'bootstrap-datepicker-rails' gem 'bulkrax', '~> 5.3' @@ -54,6 +56,7 @@ gem 'omniauth-multi-provider' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-saml', '~> 2.1' gem 'omniauth_openid_connect' +gem 'order_already' gem 'parser', '~> 2.5.3' gem 'pg' gem 'postrank-uri', '>= 1.0.24' diff --git a/app/actors/hyrax/environment.rb b/app/actors/hyrax/environment.rb new file mode 100644 index 000000000..814895f87 --- /dev/null +++ b/app/actors/hyrax/environment.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 2.9 to add in import flag +module Hyrax + module Actors + class Environment + # @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 + end + + attr_reader :curation_concern, :current_ability, :attributes, :importing + + # @return [User] the user from the current_ability + def user + current_ability.current_user + end + end + end +end diff --git a/app/assets/javascripts/admin_font_select.js b/app/assets/javascripts/admin_font_select.js index 7fa065108..e2c3e89cb 100644 --- a/app/assets/javascripts/admin_font_select.js +++ b/app/assets/javascripts/admin_font_select.js @@ -1,6 +1,34 @@ Blacklight.onLoad(function() { if($("#admin_appearance_body_font").length > 0){ - $("#admin_appearance_body_font").fontselect({lookahead: 20}); - $("#admin_appearance_headline_font").fontselect({lookahead: 20}); + $("#admin_appearance_body_font").fontselect({lookahead: 20}) + $("#admin_appearance_headline_font").fontselect({lookahead: 20}) } }); + +$('div.defaultable-fonts a.restore-default-font').click(function(e) { + e.preventDefault() + var defaultTarget = $(e.target).data('default-target') + var input = $("input[name='admin_appearance["+ defaultTarget +"]']") + var defaultValue = input.data('default-value').replace(';', '') + var inputDisplay = $("div[class$='"+ defaultTarget +"']").find('div.font-select span') + + input.val(defaultValue) + inputDisplay.css("font-family", defaultValue) + inputDisplay.text(defaultValue) +}) + +$('.panel-footer a.restore-all-default-fonts').click(function(e) { + e.preventDefault() + + var allFontInputs = $("input[name*='font']") + + allFontInputs.each(function() { + var thisTarget = $(this).attr('id').replace('admin_appearance_', '') + var defaultValue = $(this).data('default-value').replace(';', '') + var inputDisplay = $("div[class$='"+ thisTarget +"']").find('div.font-select span') + + $(this).val(defaultValue) + inputDisplay.css("font-family", defaultValue) + inputDisplay.text(defaultValue) + }) +}); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fe1ce9b26..c2e416d37 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -26,6 +26,8 @@ // Required by Blacklight //= require blacklight/blacklight //= require admin_font_select +//= 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) @@ -49,3 +51,12 @@ //= 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 000000000..4b2807651 --- /dev/null +++ b/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js @@ -0,0 +1,300 @@ +// 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); + } + }); + \ No newline at end of file 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 000000000..74aef9e9e --- /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 000000000..e29464229 --- /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/stylesheets/application.css b/app/assets/stylesheets/application.css index 8b66c7d5d..bb4734825 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -18,5 +18,8 @@ *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap *= require bootstrap-datepicker *= require single_signon + *= require blacklight_advanced_search + *= require blacklight_range_limit + *= require iiif_print/iiif_print *= require_self */ diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 72265252c..c50f887b0 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class CatalogController < ApplicationController + include BlacklightAdvancedSearch::Controller + include BlacklightRangeLimit::ControllerOverride include Hydra::Catalog include Hydra::Controller::ControllerBehavior include BlacklightOaiProvider::Controller @@ -8,20 +10,33 @@ 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| # 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 = { @@ -44,18 +59,32 @@ def self.modified_field 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 + # TODO ROB config.search_builder_class = AdvSearchBuilder + # Show gallery view config.view.gallery.partials = %i[index_header 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. @@ -81,11 +110,35 @@ 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 + # removed # config.add_facet_field solr_name("file_format", :facetable), limit: 5 + # removed # config.add_facet_field solr_name("contributor", :facetable), label: "Contributor", limit: 5 + # remvode config.add_facet_field solr_name("refereed", :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. config.add_facet_fields_to_solr_request! + # TODO ROB +# # Prior to this change, the applications specific translations were not loaded. Dogbiscuits were assuming the translations were already loaded. +# Rails.root.glob("config/locales/*.yml").each do |path| +# I18n.load_path << path.to_s +# end +# I18n.backend.reload! +# index_props = DogBiscuits.config.index_properties.collect do |prop| +# { prop => index_options(prop, DogBiscuits.config.property_mappings[prop]) } +# end +# add_index_field config, index_props + + # solr fields to be displayed in the show (single result) view + # The ordering of the field names is the order of the display + # show_props = DogBiscuits.config.all_properties + # add_show_field config, show_props + # solr fields to be displayed in the index (search results) view # The ordering of the field names is the order of the display config.add_index_field 'title_tesim', label: "Title", itemprop: 'name', if: false @@ -150,6 +203,8 @@ def self.modified_field # since we aren't specifying it otherwise. config.add_search_field('all_fields', label: 'All Fields', include_in_advanced_search: false) do |field| all_names = config.show_fields.values.map(&:field).join(" ") +# TODO ROB all_names = (config.show_fields.values.map { |v| v.field.to_s } + + # DogBiscuits.config.all_properties.map { |p| "#{p}_tesim" }).uniq.join(" ") title_name = 'title_tesim' field.solr_parameters = { qf: "#{all_names} file_format_tesim all_text_timv", @@ -178,6 +233,7 @@ def self.modified_field end config.add_search_field('creator') do |field| + # TODO ROB field.label = "Author" field.solr_parameters = { "spellcheck.dictionary": "creator" } solr_name = 'creator_tesim' field.solr_local_parameters = { @@ -220,6 +276,8 @@ 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" @@ -343,16 +401,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 = { diff --git a/app/controllers/hyrax/content_blocks_controller.rb b/app/controllers/hyrax/content_blocks_controller.rb deleted file mode 100644 index 60edc5286..000000000 --- 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 000000000..348ac4f20 --- /dev/null +++ b/app/controllers/hyrax/content_blocks_controller_decorator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax v3.4.0 to add home_text to permitted_params - Adding themes +module Hyrax + module ContentBlocksControllerDecorator + # 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, + :homepage_about_section_heading, + :homepage_about_section_content, + :researcher) + end + end +end + +Hyrax::ContentBlocksController.prepend Hyrax::ContentBlocksControllerDecorator diff --git a/app/controllers/hyrax/homepage_controller.rb b/app/controllers/hyrax/homepage_controller.rb deleted file mode 100644 index a38eda845..000000000 --- a/app/controllers/hyrax/homepage_controller.rb +++ /dev/null @@ -1,131 +0,0 @@ -# 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 - -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 - - around_action :inject_theme_views - - # The search builder for finding recent documents - # Override of Blacklight::RequestBuilders - def search_builder_class - Hyrax::HomepageSearchBuilder - end - - class_attribute :presenter_class - self.presenter_class = Hyrax::HomepagePresenter - layout 'homepage' - helper Hyrax::ContentBlockHelper - - # 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) - 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) - - respond_to do |format| - format.html { store_preferred_view } - format.rss { render layout: false } - format.atom { render layout: false } - format.json do - @presenter = Blacklight::JsonPresenter.new(@response, - @document_list, - facets_from_request, - blacklight_config) - end - additional_response_formats(format) - document_export_formats(format) - end - end - - 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' - end - - # 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 - - 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 - [] - 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 = [] - 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 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 - end - end -end diff --git a/app/controllers/saved_searches_controller.rb b/app/controllers/saved_searches_controller.rb new file mode 100644 index 000000000..f2618be72 --- /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 000000000..a97b2e3ff --- /dev/null +++ b/app/controllers/search_history_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class SearchHistoryController < ApplicationController + include Blacklight::SearchHistory + helper BlacklightAdvancedSearch::RenderConstraintsOverride + helper BlacklightRangeLimit::ViewHelperOverride + helper RangeLimitHelper +end diff --git a/app/factories/bulkrax/object_factory_decorator.rb b/app/factories/bulkrax/object_factory_decorator.rb new file mode 100644 index 000000000..928cd8384 --- /dev/null +++ b/app/factories/bulkrax/object_factory_decorator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Add ability to mark environment as from bulk import +module Bulkrax + module ObjectFactoryDecorator + # @param [Hash] attrs the attributes to put in the environment + # @return [Hyrax::Actors::Environment] + def environment(attrs) + Hyrax::Actors::Environment.new(object, Ability.new(@user), attrs, true) + end + end +end + +::Bulkrax::ObjectFactory.prepend(Bulkrax::ObjectFactoryDecorator) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d92ffddcb..70d716899 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base + # limit to 5 attempts + retry_on StandardError, wait: :exponentially_longer, attempts: 5 do |_job, _exception| + # Log error, do nothing, etc. + end end diff --git a/app/jobs/reindex_collections_job.rb b/app/jobs/reindex_collections_job.rb index e1ef281ba..422d1c8de 100644 --- a/app/jobs/reindex_collections_job.rb +++ b/app/jobs/reindex_collections_job.rb @@ -3,8 +3,7 @@ class ReindexCollectionsJob < ApplicationJob def perform Collection.find_each do |collection| - collection.try(:reindex_extent=, Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX) - collection.update_index + ReindexItemJob.perform_later(collection) end end end diff --git a/app/jobs/reindex_file_sets_job.rb b/app/jobs/reindex_file_sets_job.rb new file mode 100644 index 000000000..0e2e7931d --- /dev/null +++ b/app/jobs/reindex_file_sets_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ReindexFileSetsJob < ApplicationJob + def perform + FileSet.find_each do |file_set| + ReindexItemJob.perform_later(file_set) + end + end +end diff --git a/app/jobs/reindex_item_job.rb b/app/jobs/reindex_item_job.rb new file mode 100644 index 000000000..d44051be0 --- /dev/null +++ b/app/jobs/reindex_item_job.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ReindexItemJob < ApplicationJob + def perform(item) + item.update_index + end +end diff --git a/app/jobs/reindex_works_job.rb b/app/jobs/reindex_works_job.rb index 6aee04ba6..56adcf2d0 100644 --- a/app/jobs/reindex_works_job.rb +++ b/app/jobs/reindex_works_job.rb @@ -2,8 +2,10 @@ class ReindexWorksJob < ApplicationJob def perform - Hyrax.config.registered_curation_concern_types.each do |work_type| - work_type.constantize.find_each(&:update_index) + Site.instance.available_works.each do |work_type| + work_type.constantize.find_each do |work| + ReindexItemJob.perform_later(work) + end end end end diff --git a/app/middleware/account_elevator.rb b/app/middleware/account_elevator.rb index 1bdf38e85..6160b9a6e 100644 --- a/app/middleware/account_elevator.rb +++ b/app/middleware/account_elevator.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'apartment/elevators/generic' # Apartment middleware for switching tenants based on the # CNAME entry for an account. class AccountElevator < Apartment::Elevators::Generic @@ -7,7 +8,7 @@ class AccountElevator < Apartment::Elevators::Generic # @return [String] The tenant to switch to def parse_tenant_name(request) account = Account.from_request(request) - + account || Account.new.reset! # reset everything if no account is present account&.tenant end end diff --git a/app/models/collection.rb b/app/models/collection.rb index 39bd58492..2b2298fd8 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/generic_work.rb b/app/models/generic_work.rb index b8b740217..5ee8e9c01 100644 --- a/app/models/generic_work.rb +++ b/app/models/generic_work.rb @@ -10,4 +10,7 @@ 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/image.rb b/app/models/image.rb index e0218ec94..83adf2208 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -17,6 +17,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/search_builder.rb b/app/models/search_builder.rb index ad50b6e1e..4410a5bc8 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/views/advanced/_advanced_search_help.html.erb b/app/views/advanced/_advanced_search_help.html.erb new file mode 100644 index 000000000..df18fcbc5 --- /dev/null +++ b/app/views/advanced/_advanced_search_help.html.erb @@ -0,0 +1,26 @@ +"> + +
+ + <% if field_config[:segments] != false %> +<%= t(:'hyrax.content_blocks.instructions.homepage_about_section_heading_instructions') %>
+ <%= f.text_area :homepage_about_section_heading, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> +<%= t(:'hyrax.content_blocks.instructions.homepage_about_section_content_instructions') %>
+ <%= f.text_area :homepage_about_section_content, value: f.object.value, class: 'form-control tinymce', rows: 20, cols: 120 %> +