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 @@ +
+
+

Search tips

+
    +
  • Select "match all" to require all fields. +
  • + +
  • Select "match any" to find at least one field. +
  • + +
  • Combine keywords and attributes to find specific items. +
  • + +
  • Search by date with format: YYYYMMDD, YYYYMM or YYYY. Do not use "-", "/", or any other special characters.
  • + +
  • Use quotation marks to search as a phrase. + +
  • Use "+" before a term to make it required. (Otherwise results matching only some of your terms may be included).
  • + +
  • Use "-" before a word or phrase to exclude. + +
  • Use "OR", "AND", and "NOT" to create complex boolean logic. You can use parentheses in your complex expressions.
  • +
  • Truncation and wildcards are not supported - word-stemming is done automatically.
  • +
+
+
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 000000000..738e6e3d7 --- /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 %> + + +
+ <% if has_selected_range_limit?(field_name) %> + + + <% 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)) %> + + + <% unless params.has_key?(:search_field) %> + <%= hidden_field_tag("search_field", "dummy_range") %> + <% end %> + +
+ Between year: + <%= render_range_input(field_name, :begin, input_label_range_begin, maxlength) %> +
+
+ and year: + <%= render_range_input(field_name, :end, input_label_range_end, maxlength) %> +
+ <%= submit_tag t('blacklight.range_limit.submit_limit'), class: "#{BlacklightRangeLimit.classes[:submit]} btn btn-default btn-block" %> + <% end %> + <% end %> + + + <% unless selected_missing_for_range_limit?(field_name) %> + +
+ <% if stats_for_field?(field_name) %> + + <% end %> + + <% if (min = range_results_endpoint(field_name, :min)) && + (max = range_results_endpoint(field_name, :max)) %> +

"> + +

+ + <% if field_config[:segments] != false %> +
+ + <% 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 %> +
+ <% end %> + <% end %> + + <% if (stats = stats_for_field(field_name)) %> +
    +
  • + + <%= link_to BlacklightRangeLimit.labels[:missing], add_range_missing(field_name) %> + + + <%= number_with_delimiter(stats["missing"]) %> + +
  • +
+ <% end %> +
+ <% end %> +
diff --git a/app/views/hyrax/content_blocks/_form.html.erb b/app/views/hyrax/content_blocks/_form.html.erb index ccf22f70f..76d7e0d26 100644 --- a/app/views/hyrax/content_blocks/_form.html.erb +++ b/app/views/hyrax/content_blocks/_form.html.erb @@ -15,6 +15,12 @@
  • <%= t(:'hyrax.content_blocks.tabs.featured_researcher') %>
  • +
  • + <%= t(:'hyrax.content_blocks.tabs.homepage_about_section_heading') %> +
  • +
  • + <%= t(:'hyrax.content_blocks.tabs.homepage_about_section_content') %> +
  • @@ -90,6 +96,42 @@ <% end %>
    +
    +
    + <%= 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| %> +
    +
    + <%= f.label :homepage_about_section_heading %>
    + <%# the following line was changed from hyrax to give some context for what this context block does %> +

    <%= 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 %> +
    +
    + + <% end %> +
    +
    +
    +
    + <%= 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| %> +
    +
    + <%= f.label :homepage_about_section_content %>
    + <%# the following line was changed from hyrax to give some context for what this context block does %> +

    <%= 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 %> +
    +
    + + <% end %> +
    +
    <%= tinymce :content_block %> diff --git a/config/application.rb b/config/application.rb index 850a2b515..acf7c0523 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,35 @@ Bundler.require(*groups) module Hyku + # Providing a common method to ensure consistent UTF-8 encoding. Also removing the tricksy Byte + # Order Marker character which is an invisible 0 space character. + # + # @note In testing, we encountered errors with the file's character encoding + # (e.g. `Encoding::UndefinedConversionError`). The following will force the encoding to + # UTF-8 and replace any invalid or undefined characters from the original encoding with a + # "?". + # + # Given that we still have the original, and this is a derivative, the forced encoding + # should be acceptable. + # + # @param [String] + # @return [String] + # + # @see https://sentry.io/organizations/scientist-inc/issues/3773392603/?project=6745020&query=is%3Aunresolved&referrer=issue-stream + # @see https://github.com/samvera-labs/bulkrax/pull/689 + # @see https://github.com/samvera-labs/bulkrax/issues/688 + # @see https://github.com/scientist-softserv/adventist-dl/issues/179 + def self.utf_8_encode(string) + string + .encode(Encoding.find('UTF-8'), invalid: :replace, undef: :replace, replace: "?") + .delete("\xEF\xBB\xBF") + end + class Application < Rails::Application + # Add this line to load the lib folder first because we need + # IiifPrint::SplitPdfs::AdventistPagesToJpgsSplitter + config.autoload_paths.unshift("#{Rails.root}/lib") + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. @@ -33,22 +61,37 @@ class Application < Rails::Application end config.to_prepare do - # Allows us to use decorator files in the app directory + + # By default plain text files are not processed for text extraction. In adding + # Adventist::TextFileTextExtractionService to the beginning of the services array we are + # enabling text extraction from plain text files. + Hyrax::DerivativeService.services = [ + Adventist::TextFileTextExtractionService, + IiifPrint::PluggableDerivativeService] + + # When you are ready to use the derivative rodeo instead of the pluggable uncomment the + # following and comment out the preceding Hyrax::DerivativeService.service + # + # Hyrax::DerivativeService.services = [ + # Adventist::TextFileTextExtractionService, + # IiifPrint::DerivativeRodeoService, + # Hyrax::FileSetDerivativesService] + + DerivativeRodeo::Generators::HocrGenerator.additional_tessearct_options = "-l eng_best" + + # Allows us to use decorator files Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")).sort.each do |c| Rails.configuration.cache_classes ? require(c) : load(c) end - end - config.to_prepare do - # Allows us to use decorator files in the app directory Dir.glob(File.join(File.dirname(__FILE__), "../lib/**/*_decorator*.rb")).sort.each do |c| Rails.configuration.cache_classes ? require(c) : load(c) end - end - # OAI additions - Dir.glob(File.join(File.dirname(__FILE__), "../lib/oai/**/*.rb")).sort.each do |c| - Rails.configuration.cache_classes ? require(c) : load(c) + # OAI additions + Dir.glob(File.join(File.dirname(__FILE__), "../lib/oai/**/*.rb")).sort.each do |c| + Rails.configuration.cache_classes ? require(c) : load(c) + end end # resolve reloading issue in dev mode @@ -67,6 +110,8 @@ class Application < Rails::Application Object.include(AccountSwitch) end + # copies tinymce assets directly into public/assets + config.tinymce.install = :copy ## # Psych Allow YAML Classes # diff --git a/config/database.yml b/config/database.yml index 865dc1c5b..bc235ae14 100644 --- a/config/database.yml +++ b/config/database.yml @@ -2,6 +2,7 @@ login: &login adapter: <%= ENV['DB_ADAPTER'] || 'postgresql' %> + schema_search_path: "public,shared_extensions" host: <%= ENV['DB_HOST'] %> username: <%= ENV['DB_USER'] %> password: <%= ENV['DB_PASSWORD'] %> diff --git a/config/fedora.yml b/config/fedora.yml index 399e0bf4e..102e80b50 100644 --- a/config/fedora.yml +++ b/config/fedora.yml @@ -18,3 +18,4 @@ production: password: fedoraAdmin url: http://<%= ENV['FCREPO_HOST'] || 'localhost' %>:<%= ENV['FCREPO_PORT'] || 8080 %>/<%= ENV['FCREPO_REST_PATH'] || 'rest' %> base_path: <%= ENV['FCREPO_BASE_PATH'] || '/prod' %> + request: { timeout: 600, open_timeout: 60} diff --git a/config/initializers/active_fedora_override.rb b/config/initializers/active_fedora_override.rb new file mode 100644 index 000000000..58ec7fe04 --- /dev/null +++ b/config/initializers/active_fedora_override.rb @@ -0,0 +1,11 @@ +# Based on https://github.com/samvera/hyrax/issues/4581#issuecomment-843085122 + +# Monkey-patch to short circuit ActiveModel::Dirty which attempts to load the whole master files ordered list when calling nodes_will_change! +# This leads to a stack level too deep exception when attempting to delete a master file from a media object on the manage files step. +# See https://github.com/samvera/active_fedora/pull/1312/commits/7c8bbbefdacefd655a2ca653f5950c991e1dc999#diff-28356c4daa0d55cbaf97e4269869f510R100-R103 +ActiveFedora::Aggregation::ListSource.class_eval do + def attribute_will_change!(attr) + return super unless attr == 'nodes' + attributes_changed_by_setter[:nodes] = true + end +end diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index e523b4cdf..a2647571d 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -39,6 +39,8 @@ # Any schemas added here will be available along with your selected Tenant. # # config.persistent_schemas = %w{ hstore } + config.persistent_schemas = ['shared_extensions'] + # <== PostgreSQL only options # diff --git a/config/routes.rb b/config/routes.rb index 73a5d0f50..f0a2a3753 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,7 @@ Rails.application.routes.draw do # rubocop:disable Metrics/BlockLength resources :identity_providers + concern :range_searchable, BlacklightRangeLimit::Routes::RangeSearchable.new concern :iiif_search, BlacklightIiifSearch::Routes.new concern :oai_provider, BlacklightOaiProvider::Routes.new @@ -70,9 +71,10 @@ mount Qa::Engine => '/authorities' mount Blacklight::Engine => '/' + mount BlacklightAdvancedSearch::Engine => '/' mount Hyrax::Engine, at: '/' mount Bulkrax::Engine, at: '/' if ENV.fetch('HYKU_BULKRAX_ENABLED', 'true') == 'true' - + mount HykuKnapsack::Engine, at: '/' concern :searchable, Blacklight::Routes::Searchable.new concern :exportable, Blacklight::Routes::Exportable.new @@ -82,6 +84,7 @@ concerns :oai_provider concerns :searchable + concerns :range_searchable end resources :solr_documents, only: [:show], path: '/catalog', controller: 'catalog' do diff --git a/lib/active_fedora/solr_service_decorator.rb b/lib/active_fedora/solr_service_decorator.rb new file mode 100644 index 000000000..b50a5749e --- /dev/null +++ b/lib/active_fedora/solr_service_decorator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# OVERRIDE: class ActiveFedora::SolrService from Fedora 12.1.1 +module ActiveFedora + module SolrServiceDecorator + # Get the count of records that match the query + # @param [String] query a solr query + # @param [Hash] args arguments to pass through to `args' param of SolrService.query + # (note that :rows will be overwritten to 0) + # @return [Integer] number of records matching + # + # OVERRIDE: use `post` rather than `get` to handle larger query sizes + def count(query, args = {}) + args = args.merge(rows: 0) + SolrService.post(query, args)['response']['numFound'].to_i + end + end +end + +ActiveFedora::SolrService.singleton_class.send(:prepend, ActiveFedora::SolrServiceDecorator) diff --git a/lib/hydra/derivatives/processors/image_decorator.rb b/lib/hydra/derivatives/processors/image_decorator.rb new file mode 100644 index 000000000..f9f4145af --- /dev/null +++ b/lib/hydra/derivatives/processors/image_decorator.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Fix PDF tripple issue + +module Hydra + module Derivatives + module Processors + module ImageDecorator + protected + + # When resizing images, it is necessary to flatten any layers, otherwise the background + # may be completely black. This happens especially with PDFs. See #110 + def create_resized_image + create_image do |xfrm| + if size + xfrm.combine_options do |i| + i.flatten + i.resize(size) + end + end + end + end + end + end + end +end + +::Hydra::Derivatives::Processors::Image.prepend(Hydra::Derivatives::Processors::ImageDecorator) diff --git a/lib/iiif_manifest/manifest_builder/canvas_builder_decorator.rb b/lib/iiif_manifest/manifest_builder/canvas_builder_decorator.rb new file mode 100644 index 000000000..777d9b03b --- /dev/null +++ b/lib/iiif_manifest/manifest_builder/canvas_builder_decorator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# OVERRIDE IIIFManifest v0.5.0 to use the parent's title as the label instead of the filename + +module IIIFManifest + module ManifestBuilderDecorator + module CanvasBuilderDecorator + def apply_record_properties + canvas['@id'] = path + canvas.label = record['parent_title_tesim']&.first || record.to_s + end + end + end +end + +IIIFManifest::ManifestBuilder.prepend(IIIFManifest::ManifestBuilderDecorator) +IIIFManifest::ManifestBuilder::CanvasBuilder.prepend(IIIFManifest::ManifestBuilder::CanvasBuilderDecorator) diff --git a/lib/tasks/db_enhancements.rake b/lib/tasks/db_enhancements.rake new file mode 100644 index 000000000..551db56c3 --- /dev/null +++ b/lib/tasks/db_enhancements.rake @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +namespace :db do + desc 'Also create shared_extensions Schema' + task extensions: :environment do + # Create Schema + ActiveRecord::Base.connection.execute 'CREATE SCHEMA IF NOT EXISTS shared_extensions;' + # Enable Hstore + ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS HSTORE SCHEMA shared_extensions;' + # Enable UUID-OSSP + ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;' + ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA shared_extensions;' + # Grant usage to public + ActiveRecord::Base.connection.execute 'GRANT usage ON SCHEMA shared_extensions to public;' + end +end + +Rake::Task["db:create"].enhance do + Rake::Task["db:extensions"].invoke +end + +Rake::Task["db:test:purge"].enhance do + Rake::Task["db:extensions"].invoke +end diff --git a/lib/tasks/index.rake b/lib/tasks/index.rake new file mode 100644 index 000000000..c02885af2 --- /dev/null +++ b/lib/tasks/index.rake @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'ruby-progressbar' + +desc "reindex just the works in the background" +task index_works: :environment do + Account.find_each do |account| + puts "=============== #{account.name}============" + next if account.name == "search" + switch!(account) + in_each_account do + ReindexWorksJob.perform_later + end + end +end + +desc "reindex just the collections in the background" +task index_collections: :environment do + Account.find_each do |account| + puts "=============== #{account.name}============" + next if account.name == "search" + switch!(account) + in_each_account do + ReindexCollectionsJob.perform_later + end + end +end + +desc "reindex just the admin_sets in the background" +task index_admin_sets: :environment do + Account.find_each do |account| + puts "=============== #{account.name}============" + next if account.name == "search" + switch!(account) + in_each_account do + ReindexAdminSetsJob.perform_later + end + end +end + +desc "reindex just the file_sets in the background" +task index_file_sets: :environment do + Account.find_each do |account| + puts "=============== #{account.name}============" + next if account.name == "search" + switch!(account) + in_each_account do + ReindexFileSetsJob.perform_later + end + end +end + +def in_each_account + Account.find_each do |account| + puts "=============== #{account.name}============" + next if account.name == "search" + switch!(account) + yield + end +end diff --git a/lib/wings/services/custom_queries/find_ids_by_model_decorator.rb b/lib/wings/services/custom_queries/find_ids_by_model_decorator.rb new file mode 100644 index 000000000..7cbaaef29 --- /dev/null +++ b/lib/wings/services/custom_queries/find_ids_by_model_decorator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# OVERRIDE Hyrax 3.5 to use post instead of get for Solr requests + +module Wings + module CustomQueries + ## + # @see https://github.com/samvera/valkyrie/wiki/Queries#custom-queries + # @see Hyrax::CustomQueries::FindIdsByModel + module FindIdsByModelDecorator + ## + # @note uses solr to do the lookup + # + # @param model [Class] + # @param ids [Enumerable<#to_s>, Symbol] + # + # @return [Enumerable] + def find_ids_by_model(model:, ids: :all) + return enum_for(:find_ids_by_model, model: model, ids: ids) unless block_given? + model_name = ModelRegistry.lookup(model).model_name + + solr_query = "_query_:\"{!raw f=has_model_ssim}#{model_name}\"" + solr_response = ActiveFedora::SolrService.post(solr_query, fl: 'id', rows: @query_rows)['response'] + + loop do + response_docs = solr_response['docs'] + response_docs.select! { |doc| ids.include?(doc['id']) } unless ids == :all + + response_docs.each { |doc| yield doc['id'] } + + break if (solr_response['start'] + solr_response['docs'].count) >= solr_response['numFound'] + solr_response = ActiveFedora::SolrService.post(solr_query, fl: 'id', rows: @query_rows, start: solr_response['start'] + @query_rows)['response'] + end + end + end + end +end + +Wings::CustomQueries::FindIdsByModel.prepend Wings::CustomQueries::FindIdsByModelDecorator diff --git a/spec/controllers/search_history_controller_spec.rb b/spec/controllers/search_history_controller_spec.rb new file mode 100644 index 000000000..d9a349279 --- /dev/null +++ b/spec/controllers/search_history_controller_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe SearchHistoryController do + routes { Blacklight::Engine.routes } + + describe 'index' do + let(:one) { Search.create } + let(:two) { Search.create } + let(:three) { Search.create } + + it 'only fetches searches with ids in the session' do + session[:history] = [one.id, three.id] + get :index + searches = assigns(:searches) + expect(searches).to include(one) + expect(searches).not_to include(two) + end + + it 'tolerates bad ids in session' do + session[:history] = [one.id, three.id, 'NOT_IN_DB'] + get :index + searches = assigns(:searches) + expect(searches).to include(one) + expect(searches).to include(three) + end + + it 'does not fetch any searches if there is no history' do + session[:history] = [] + get :index + searches = assigns(:searches) + expect(searches).to be_empty + end + end +end