diff --git a/.env b/.env index d915fdd7a..540f0aacf 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ CHROME_HOSTNAME=chrome COMPOSE_DOCKER_CLI_BUILD=1 +DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true DB_ADAPTER=postgresql DB_HOST=db DB_HOST=db @@ -13,18 +14,19 @@ FCREPO_BASE_PATH=/hykudemo FCREPO_HOST=fcrepo FCREPO_PORT=8080 FCREPO_REST_PATH=rest +HYRAX_ACTIVE_JOB_QUEUE=good_job +HYRAX_FITS_PATH=/app/fits/fits.sh INITIAL_ADMIN_EMAIL=admin@example.com INITIAL_ADMIN_PASSWORD=testing123 -JAVA_OPTS=-Xmx4g -Xms1g IN_DOCKER=true +JAVA_OPTS= +JAVA_OPTS=-Xmx4g -Xms1g LD_LIBRARY_PATH=/opt/fits/tools/mediainfo/linux +NEGATIVE_CAPTCHA_SECRET=default-value-change-me PASSENGER_APP_ENV=development RAILS_LOG_TO_STDOUT=true REDIS_HOST=redis SECRET_KEY_BASE=asdf -HYRAX_ACTIVE_JOB_QUEUE=sidekiq -HYRAX_FITS_PATH=/app/fits/fits.sh -NEGATIVE_CAPTCHA_SECRET=default-value-change-me SOLR_ADMIN_PASSWORD=SolrRocks SOLR_ADMIN_USER=solr SOLR_COLLECTION_NAME=hydra-development @@ -32,6 +34,8 @@ SOLR_CONFIGSET_NAME=hyku SOLR_HOST=solr SOLR_PORT=8983 SOLR_URL=http://solr:SolrRocks@solr:8983/solr/ +TB_RSPEC_FORMATTER=progress +TB_RSPEC_OPTIONS="--format RspecJunitFormatter --out rspec.xml" # Comment out these 5 for single tenancy / Uncomment for multi HYKU_ADMIN_HOST=hyku.test diff --git a/.rubocop.yml b/.rubocop.yml index 90961c959..4a5fb78c8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -127,3 +127,7 @@ Metrics/BlockLength: - 'spec/**/*.rb' - 'lib/tasks/*.rake' - 'app/controllers/catalog_controller.rb' + +RSpec/FilePath: + Exclude: + - 'spec/config/application_spec.rb' diff --git a/Gemfile b/Gemfile index 905efd1ad..1ddb2fe60 100644 --- a/Gemfile +++ b/Gemfile @@ -13,11 +13,13 @@ gem 'addressable', '2.8.1' # remove once https://github.com/postrank-labs/postra gem 'apartment', github: 'scientist-softserv/apartment', branch: 'development' gem 'aws-sdk-sqs', group: %i[aws] gem 'blacklight', '~> 7.29' +gem 'blacklight_advanced_search' gem 'blacklight_oai_provider', '~> 7.0' +gem 'blacklight_range_limit' gem 'bolognese', '>= 1.9.10' gem 'bootstrap', '~> 4.6' gem 'bootstrap-datepicker-rails' -gem 'bulkrax', '~> 5.3' +gem 'bulkrax', '~> 5.4' gem 'byebug', group: %i[development test] gem 'capybara', group: %i[test] gem 'capybara-screenshot', '~> 1.0', group: %i[test] @@ -35,6 +37,7 @@ gem 'easy_translate', group: %i[development] gem 'factory_bot_rails', group: %i[test] gem 'fcrepo_wrapper', '~> 0.4', group: %i[development test] gem 'flutie' +gem 'good_job', '~> 2.99' gem 'googleauth', '= 1.8.1' # 1.9.0 got yanked from rubygems, hard pinning until we can upgrade gem 'hyrax', github: 'samvera/hyrax', branch: 'double_combo' gem 'hyrax-doi', github: 'samvera-labs/hyrax-doi', branch: 'rails_hyrax_upgrade' @@ -57,6 +60,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/Gemfile.lock b/Gemfile.lock index 8c95cd3bb..c397bf7af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,7 +39,7 @@ GIT GIT remote: https://github.com/samvera/hyrax.git - revision: 9ef64090a13abdeaabfaabdee851829093d02cc4 + revision: 0108fed6c83dd4a449b37cc1a6c19787ec54921d branch: double_combo specs: hyrax (5.0.0.rc2) @@ -231,7 +231,7 @@ GEM awesome_nested_set (3.6.0) activerecord (>= 4.0.0, < 7.2) aws-eventstream (1.3.0) - aws-partitions (1.864.0) + aws-partitions (1.865.0) aws-sdk-core (3.190.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -271,7 +271,7 @@ GEM smart_properties bibtex-ruby (6.0.0) latex-decode (~> 0.0) - bigdecimal (3.1.4) + bigdecimal (3.1.5) bindata (2.4.15) bindex (0.8.1) blacklight (7.35.0) @@ -291,6 +291,9 @@ GEM blacklight-gallery (4.4.0) blacklight (>= 7.17, < 9) rails (>= 6.1, < 8) + blacklight_advanced_search (7.0.0) + blacklight (~> 7.0) + parslet blacklight_iiif_search (2.0.0) blacklight (~> 7.0) iiif-presentation @@ -299,6 +302,10 @@ GEM blacklight (~> 7.0) oai (~> 1.2) rexml + blacklight_range_limit (8.4.0) + blacklight (>= 7.25.2, < 9) + deprecation + view_component (>= 2.54, < 4) bolognese (1.11.5) activesupport (>= 4.2.5) benchmark_methods (~> 0.7) @@ -510,6 +517,8 @@ GEM edtf (3.1.1) activesupport (>= 3.0, < 8.0) erubi (1.12.0) + et-orbi (1.2.7) + tzinfo ethon (0.16.0) ffi (>= 1.15.0) excon (0.71.1) @@ -547,12 +556,24 @@ GEM flutie (2.2.0) font-awesome-rails (4.7.0.8) railties (>= 3.2, < 8.0) + fugit (1.9.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) gender_detector (0.1.2) unicode_utils (>= 1.3.0) geo_coord (0.2.0) geocoder (1.8.2) globalid (1.2.1) activesupport (>= 6.1) + good_job (2.99.0) + activejob (>= 5.2.0) + activerecord (>= 5.2.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 5.2.0) + thor (>= 0.14.1) + webrick (>= 1.3) + zeitwerk (>= 2.0) google-apis-core (0.11.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -889,6 +910,8 @@ GEM openseadragon (0.6.0) rails (> 3.2.0) optimist (3.1.0) + order_already (0.3.1) + rails-html-sanitizer (~> 1.4) orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.0) @@ -923,6 +946,7 @@ GEM nokogiri (~> 1.6) rails (>= 5.0, < 7.2) rdf + raabro (1.4.0) racc (1.7.3) rack (2.2.8) rack-oauth2 (2.2.0) @@ -1333,11 +1357,13 @@ DEPENDENCIES apartment! aws-sdk-sqs blacklight (~> 7.29) + blacklight_advanced_search blacklight_oai_provider (~> 7.0) + blacklight_range_limit bolognese (>= 1.9.10) bootstrap (~> 4.6) bootstrap-datepicker-rails - bulkrax (~> 5.3) + bulkrax (~> 5.4) byebug capybara capybara-screenshot (~> 1.0) @@ -1355,6 +1381,7 @@ DEPENDENCIES factory_bot_rails fcrepo_wrapper (~> 0.4) flutie + good_job (~> 2.99) googleauth (= 1.8.1) hyku_knapsack! hyrax! @@ -1377,6 +1404,7 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.1) omniauth_openid_connect + order_already parser (~> 2.5.3) pg postrank-uri (>= 1.0.24) diff --git a/app/assets/javascripts/admin_color_select.js b/app/assets/javascripts/admin_color_select.js new file mode 100644 index 000000000..371fc0b42 --- /dev/null +++ b/app/assets/javascripts/admin_color_select.js @@ -0,0 +1,20 @@ +$(document).on('turbolinks:load', function() { + $('div.defaultable-colors a.restore-default-color').click(function(e) { + e.preventDefault() + + var defaultTarget = $(e.target).data('default-target') + var input = $("input[name='admin_appearance["+ defaultTarget +"]']") + + input.val(input.data('default-value')) + }) + + $('.panel-footer a.restore-all-default-colors').click(function(e) { + e.preventDefault() + + var allColorInputs = $("input[name*='color']") + + allColorInputs.each(function() { + $(this).val($(this).data('default-value')) + }) + }) +}); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 04b9aebbf..852de9889 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -28,6 +28,8 @@ // Required by Blacklight //= require blacklight/blacklight //= require blacklight_gallery +//= require admin_color_select +//= require blacklight_advanced_search // Moved the Hyku JS *above* the Hyrax JS to resolve #1187 (following // a pattern found in ScholarSphere) @@ -51,3 +53,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..1133b8dec --- /dev/null +++ b/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js @@ -0,0 +1,348 @@ +// for Blacklight.onLoad: + +/* A custom event "plotDrawn.blacklight.rangeLimit" will be sent when flot plot + is (re-)drawn on screen possibly with a new size. target of event will be the DOM element + containing the plot. Used to resize slider to match. */ + +Blacklight.onLoad(function () { + // ratio of width to height for desired display, multiply width by this ratio + // to get height. hard-coded in for now. + var display_ratio = 1 / (1.618 * 2); // half a golden rectangle, why not + var redrawnEvent = "plotDrawn.blacklight.rangeLimit"; + + // Facets already on the page? Turn em into a chart. + $(".range_limit .profile .distribution.chart_js ul").each(function () { + turnIntoPlot($(this).parent()); + }); + + // Add AJAX fetched range facets if needed, and add a chart to em + $(".range_limit .profile .distribution a.load_distribution").each( + function () { + var container = $(this).parent("div.distribution"); + + $(container).load($(this).attr("href"), function (response, status) { + if ($(container).hasClass("chart_js") && status == "success") { + turnIntoPlot(container); + } + }); + } + ); + + // Listen for twitter bootstrap collapsible open events, to render flot + // in previously hidden divs on open, if needed. + $("body").on("show.bs.collapse", function (event) { + // Was the target a .facet-content including a .chart-js? + var container = $(event.target).filter(".facet-content").find(".chart_js"); + + // only if it doesn't already have a canvas, it isn't already drawn + if (container && container.find("canvas").length == 0) { + // be willing to wait up to 1100ms for container to + // have width -- right away on show.bs is too soon, but + // shown.bs is later than we want, we want to start rendering + // while animation is still in progress. + turnIntoPlot(container, 1100); + } + }); + + // after a collapsible facet contents is fully shown, + // resize the flot chart to current conditions. This way, if you change + // browser window size, you can get chart resized to fit by closing and opening + // again, if needed. + + function redrawPlot(container) { + if (container && container.width() > 0) { + // resize the container's height, since width may have changed. + container.height(container.width() * display_ratio); + + // redraw the chart. + var plot = container.data("plot"); + if (plot) { + // how to redraw after possible resize? + // Cribbed from https://github.com/flot/flot/blob/master/jquery.flot.resize.js + plot.resize(); + plot.setupGrid(); + plot.draw(); + // plus trigger redraw of the selection, which otherwise ain't always right + // we'll trigger a fake event on one of the boxes + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin").trigger("change"); + + // send our custom event to trigger redraw of slider + $(container).trigger(redrawnEvent); + } + } + } + + $("body").on("shown.bs.collapse", function (event) { + var container = $(event.target).filter(".facet-content").find(".chart_js"); + redrawPlot(container); + }); + + // debouce borrowed from underscore + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + debounce = function (func, wait, immediate) { + var timeout; + return function () { + var context = this, + args = arguments; + var later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + }; + + $(window).on( + "resize", + debounce(function () { + $(".chart_js").each(function (i, container) { + redrawPlot($(container)); + }); + }, 350) + ); + + // second arg, if provided, is a number of ms we're willing to + // wait for the container to have width before giving up -- we'll + // set 50ms timers to check back until timeout is expired or the + // container is finally visible. The timeout is used when we catch + // bootstrap show event, but the animation hasn't barely begun yet -- but + // we don't want to wait until it's finished, we want to start rendering + // as soon as we can. + // + // We also will + function turnIntoPlot(container, wait_for_visible) { + // flot can only render in a a div with a defined width. + // for instance, a hidden div can't generally be rendered in (although if you set + // an explicit width on it, it might work) + // + // We'll count on later code that catch bootstrap collapse open to render + // on show, for currently hidden divs. + + // for some reason width sometimes return negative, not sure + // why but it's some kind of hidden. + if (container.width() > 0) { + var height = container.width() * display_ratio; + + // Need an explicit height to make flot happy. + container.height(height); + + areaChart($(container)); + + $(container).trigger(redrawnEvent); + } else if (wait_for_visible > 0) { + setTimeout(function () { + turnIntoPlot(container, wait_for_visible - 50); + }, 50); + } + } + + // Takes a div holding a ul of distribution segments produced by + // blacklight_range_limit/_range_facets and makes it into + // a flot area chart. + function areaChart(container) { + //flot loaded? And canvas element supported. + if (domDependenciesMet()) { + // Grab the data from the ul div + var series_data = new Array(); + var pointer_lookup = new Array(); + var x_ticks = new Array(); + var min = BlacklightRangeLimit.parseNum( + $(container).find("ul li:first-child span.from").text() + ); + var max = BlacklightRangeLimit.parseNum( + $(container).find("ul li:last-child span.to").text() + ); + + $(container) + .find("ul li") + .each(function () { + var from = BlacklightRangeLimit.parseNum( + $(this).find("span.from").text() + ); + var to = BlacklightRangeLimit.parseNum( + $(this).find("span.to").text() + ); + var count = BlacklightRangeLimit.parseNum( + $(this).find("span.count").text() + ); + var avg = count / (to - from + 1); + + //We use the avg as the y-coord, to make the area of each + //segment proportional to how many documents it holds. + series_data.push([from, avg]); + series_data.push([to + 1, avg]); + + x_ticks.push(from); + + pointer_lookup.push({ + from: from, + to: to, + count: count, + label: $(this).find(".facet_select").text(), + }); + }); + var max_plus_one = + BlacklightRangeLimit.parseNum( + $(container).find("ul li:last-child span.to").text() + ) + 1; + x_ticks.push(max_plus_one); + + var plot; + var config = + $(container).closest(".facet_limit").data("plot-config") || {}; + + try { + plot = $.plot( + $(container), + [series_data], + $.extend(true, config, { + yaxis: { ticks: [], min: 0, autoscaleMargin: 0.1 }, + //xaxis: { ticks: x_ticks }, + xaxis: { tickDecimals: 0 }, // force integer ticks + series: { lines: { fill: true, steps: true } }, + grid: { clickable: true, hoverable: true, autoHighlight: false }, + selection: { mode: "x" }, + }) + ); + } catch (err) { + alert(err); + } + + find_segment_for = function_for_find_segment(pointer_lookup); + var last_segment = null; + $(container).tooltip({ + placement: "bottom", + trigger: "manual", + delay: { show: 0, hide: 100 }, + }); + + $(container).bind("plothover", function (event, pos, item) { + segment = find_segment_for(pos.x); + + if (segment != last_segment) { + var title = + find_segment_for(pos.x).label + + " (" + + BlacklightRangeLimit.parseNum(segment.count) + + ")"; + $(container) + .attr("title", title) + .tooltip("_fixTitle") + .tooltip("show"); + + last_segment = segment; + } + }); + + $(container).bind("mouseout", function () { + last_segment = null; + $(container).tooltip("hide"); + }); + $(container).bind("plotclick", function (event, pos, item) { + if (plot.getSelection() == null) { + segment = find_segment_for(pos.x); + plot.setSelection(normalized_selection(segment.from, segment.to)); + } + }); + $(container).bind("plotselected plotselecting", function (event, ranges) { + if (ranges != null) { + var from = Math.floor(ranges.xaxis.from); + var to = Math.floor(ranges.xaxis.to); + + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin").val(from); + form.find("input.range_end").val(to); + + var slider_placeholder = $(container) + .closest(".limit_content") + .find("[data-slider-placeholder]"); + if (slider_placeholder) { + slider_placeholder.slider("setValue", [from, to + 1]); + } + } + }); + + var form = $(container) + .closest(".limit_content") + .find("form.range_limit"); + form.find("input.range_begin, input.range_end").change(function () { + plot.setSelection(form_selection(form, min, max), true); + }); + $(container) + .closest(".limit_content") + .find(".profile .range") + .on("slide", function (event, ui) { + var values = $(event.target).data("slider").getValue(); + form.find("input.range_begin").val(values[0]); + form.find("input.range_end").val(values[1]); + plot.setSelection( + normalized_selection(values[0], Math.max(values[0], values[1] - 1)), + true + ); + }); + + // initially entirely selected, to match slider + plot.setSelection({ xaxis: { from: min, to: max + 0.9999 } }); + } + } + + // Send endpoint to endpoint+0.99999 to have display + // more closely approximate limiting behavior esp + // at small resolutions. (Since we search on whole numbers, + // inclusive, but flot chart is decimal.) + function normalized_selection(min, max) { + max += 0.99999; + + return { xaxis: { from: min, to: max } }; + } + + function form_selection(form, min, max) { + var begin_val = BlacklightRangeLimit.parseNum( + $(form).find("input.range_begin").val() + ); + if (isNaN(begin_val) || begin_val < min) { + begin_val = min; + } + var end_val = BlacklightRangeLimit.parseNum( + $(form).find("input.range_end").val() + ); + if (isNaN(end_val) || end_val > max) { + end_val = max; + } + + return normalized_selection(begin_val, end_val); + } + + function function_for_find_segment(pointer_lookup_arr) { + return function (x_coord) { + for (var i = pointer_lookup_arr.length - 1; i >= 0; i--) { + var hash = pointer_lookup_arr[i]; + if (x_coord >= hash.from) return hash; + } + return pointer_lookup_arr[0]; + }; + } + + // Check if Flot is loaded, and if browser has support for + // canvas object, either natively or via IE excanvas. + function domDependenciesMet() { + var flotLoaded = typeof $.plot != "undefined"; + var canvasAvailable = + typeof document.createElement("canvas").getContext != "undefined" || + typeof window.CanvasRenderingContext2D != "undefined" || + typeof G_vmlCanvasManager != "undefined"; + + return flotLoaded && canvasAvailable; + } +}); diff --git a/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js b/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js new file mode 100644 index 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 9694fb3af..280aa914c 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -18,5 +18,8 @@ *= require dataTables.bootstrap4 *= require bootstrap-datepicker *= require single_signon + *= require blacklight_advanced_search + *= require blacklight_range_limit *= require_self + *= require hyku_knapsack/application */ diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 72265252c..a9105df5b 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,34 @@ 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 +202,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 +232,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,14 +275,15 @@ def self.modified_field } end + date_fields = ['date_created_tesim', 'sorted_date_isi', 'sorted_month_isi'] + config.add_search_field('date_created') do |field| field.solr_parameters = { "spellcheck.dictionary": "date_created" } - solr_name = 'created_tesim' field.solr_local_parameters = { - qf: solr_name, - pf: solr_name + qf: date_fields.join(' '), + pf: date_fields.join(' ') } end @@ -343,16 +399,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/admin/appearances_controller.rb b/app/controllers/hyrax/admin/appearances_controller.rb index b37308ebc..9130b8b11 100644 --- a/app/controllers/hyrax/admin/appearances_controller.rb +++ b/app/controllers/hyrax/admin/appearances_controller.rb @@ -16,8 +16,8 @@ def show add_breadcrumbs @form = form_class.new @fonts = [@form.headline_font, @form.body_font] - @home_theme_information = YAML.load_file('config/home_themes.yml') - @show_theme_information = YAML.load_file('config/show_themes.yml') + @home_theme_information = YAML.load_file(Hyku::Application.path_for('config/home_themes.yml')) + @show_theme_information = YAML.load_file(Hyku::Application.path_for('config/show_themes.yml')) @home_theme_names = load_home_theme_names @show_theme_names = load_show_theme_names @search_themes = load_search_themes 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..afaa2201f --- /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 index a38eda845..b9130eb9e 100644 --- a/app/controllers/hyrax/homepage_controller.rb +++ b/app/controllers/hyrax/homepage_controller.rb @@ -1,5 +1,17 @@ # frozen_string_literal: true +######################################################################################### +######################################################################################### +# +# +# HACK: We have copied over the Hyrax::HomepageController to address Hyku specific +# customizations. This controller needs significant refactoring and reconciliation +# with Hyrax prime. Note, we are inheriting differently than Hyrax does and +# there are other adjustments. +# +# +######################################################################################### +######################################################################################### # OVERRIDE: Hyrax 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 @@ -18,10 +30,11 @@ class HomepageController < CatalogController include Blacklight::SearchHelper include Blacklight::AccessControls::Catalog + # OVERRIDE: account for Hyku themes around_action :inject_theme_views # The search builder for finding recent documents - # Override of Blacklight::RequestBuilders + # Override of Blacklight::RequestBuilders and default CatalogController behavior def search_builder_class Hyrax::HomepageSearchBuilder end @@ -31,20 +44,23 @@ def search_builder_class layout 'homepage' helper Hyrax::ContentBlockHelper - # override hyrax v2.9.0 added @home_text - Adding Themes def index + # BEGIN copy Hyrax prime's Hyrax::HomepageController#index @presenter = presenter_class.new(current_ability, collections) @featured_researcher = ContentBlock.for(:researcher) @marketing_text = ContentBlock.for(:marketing) - @home_text = ContentBlock.for(:home_text) @featured_work_list = FeaturedWorkList.new - # OVERRIDE here to add featured collection list - @featured_collection_list = FeaturedCollectionList.new @announcement_text = ContentBlock.for(:announcement) recent + # END copy + + # BEGIN OVERRIDE + # What follows is Hyku specific overrides + @home_text = ContentBlock.for(:home_text) # hyrax v3.5.0 added @home_text - Adding Themes + @featured_collection_list = FeaturedCollectionList.new # OVERRIDE here to add featured collection list + ir_counts if home_page_theme == 'institutional_repository' - # override hyrax v2.9.0 added for facets on homepage - Adding Themes (@response, @document_list) = search_results(params) respond_to do |format| @@ -103,11 +119,11 @@ def recent @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 + # COPIED from Hyrax::HomepageController def sort_field "date_uploaded_dtsi desc" 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/forms/hyrax/forms/admin/appearance.rb b/app/forms/hyrax/forms/admin/appearance.rb index 7f20affae..56000bd98 100644 --- a/app/forms/hyrax/forms/admin/appearance.rb +++ b/app/forms/hyrax/forms/admin/appearance.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# OVERRIDE Hyrax 3.4.0 ot add custom theming +# OVERRIDE Hyrax 3.4.0 to add custom theming # rubocop:disable Metrics/ClassLength module Hyrax @@ -17,12 +17,21 @@ class Appearance delegate :default_collection_image, :default_collection_image?, to: :site delegate :default_work_image, :default_work_image?, to: :site - DEFAULT_FONTS = { + ## + # @!group Class Attributes + # + # @!attribute default_fonts + # @return [Hash] there should be at least the key "body_font" and + # "headline_font" + class_attribute :default_fonts, default: { 'body_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;', 'headline_font' => 'Helvetica Neue, Helvetica, Arial, sans-serif;' - }.freeze + } - DEFAULT_COLORS = { + ## + # @!attribute default_colors + # @return [Hash] + class_attribute :default_colors, default: { 'header_and_footer_background_color' => '#3c3c3c', 'header_and_footer_text_color' => '#dcdcdc', 'navbar_background_color' => '#000000', @@ -40,9 +49,8 @@ class Appearance # 'active_tabs_background_color' => '#337ab7', 'facet_panel_background_color' => '#f5f5f5', 'facet_panel_text_color' => '#333333' - }.freeze - - DEFAULT_VALUES = DEFAULT_FONTS.merge(DEFAULT_COLORS).freeze + } + # @!endgroup Class Attributes # @param [Hash] attributes the list of parameters from the form def initialize(attributes = {}) @@ -430,8 +438,12 @@ def convert_to_rgba(hex_color, alpha = 0.5) "rgba(#{rgb[0]}, #{rgb[1]}, #{rgb[2]}, #{alpha})" end + def default_values + @default_values ||= default_fonts.merge(default_colors) + end + def block_for(name, dynamic_default = nil) - ContentBlock.block_for(name: name, fallback_value: DEFAULT_VALUES[name] || dynamic_default) + ContentBlock.block_for(name: name, fallback_value: default_values[name] || dynamic_default) end # Persist a key/value tuple as a ContentBlock diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 681cd4d18..18b2d58b4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,6 +5,7 @@ module ApplicationHelper include Hyrax::OverrideHelperBehavior include GroupNavigationHelper include SharedSearchHelper + include HykuKnapsack::ApplicationHelper def hint_for(term:, record_class: nil) hint = locale_for(type: 'hints', term: term, record_class: record_class) 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/content_block.rb b/app/models/content_block.rb index c9e72b26e..5b383f192 100644 --- a/app/models/content_block.rb +++ b/app/models/content_block.rb @@ -16,7 +16,9 @@ class ContentBlock < ApplicationRecord help: :help_page, terms: :terms_page, agreement: :agreement_page, - home_text: :home_text + home_text: :home_text, + homepage_about_section_heading: :homepage_about_section_heading, + homepage_about_section_content: :homepage_about_section_content }.freeze # NOTE: method defined outside the metaclass wrapper below because @@ -90,6 +92,22 @@ def home_text=(value) home_text.update(value: value) end + def homepage_about_section_heading + find_or_create_by(name: 'homepage_about_section_heading') + end + + def homepage_about_section_heading=(value) + homepage_about_section_heading.update(value: value) + end + + def homepage_about_section_content + find_or_create_by(name: 'homepage_about_section_content') + end + + def homepage_about_section_content=(value) + homepage_about_section_content.update(value: value) + end + def about_page find_or_create_by(name: 'about_page') end diff --git a/app/models/generic_work.rb b/app/models/generic_work.rb index f69aae6ab..4c45fad1e 100644 --- a/app/models/generic_work.rb +++ b/app/models/generic_work.rb @@ -12,4 +12,6 @@ class GenericWork < ActiveFedora::Base validates :title, presence: { message: 'Your work must have a title.' } self.indexer = GenericWorkIndexer + + prepend OrderAlready.for(:creator) end diff --git a/app/models/image.rb b/app/models/image.rb index 51afe7599..e259048b3 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -19,6 +19,8 @@ class Image < ActiveFedora::Base include ::Hyrax::BasicMetadata self.indexer = ImageIndexer + prepend OrderAlready.for(:creator) + # Change this to restrict which works can be added as a child. # self.valid_child_concerns = [] validates :title, presence: { message: 'Your work must have a title.' } diff --git a/app/models/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/models/user.rb b/app/models/user.rb index cf903b102..3f27a488e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,9 +13,7 @@ class User < ApplicationRecord include Blacklight::User # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable - devise :database_authenticatable, :invitable, :registerable, - :recoverable, :rememberable, :trackable, :validatable, - :omniauthable, omniauth_providers: %i[saml openid_connect cas] + devise(*Hyku::Application.user_devise_parameters) after_create :add_default_group_membership! diff --git a/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb b/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb index 8348c245f..6260e456a 100644 --- a/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb +++ b/app/presenters/concerns/hyrax/iiif_av/displays_content_decorator.rb @@ -8,21 +8,21 @@ module IiifAv # request.base_url => hostname # also to remove #auth_service since it was not working for now module DisplaysContentDecorator - private + def solr_document + defined?(super) ? super : object + end - def solr_document - defined?(super) ? super : object - end + def current_ability + defined?(super) ? super : @ability + end - def current_ability - defined?(super) ? super : @ability - end + def request + Request.new(base_url: hostname) + end - Request = Struct.new(:base_url, keyword_init: true) + private - def request - Request.new(base_url: hostname) - end + Request = Struct.new(:base_url, keyword_init: true) def image_content return nil unless latest_file_id diff --git a/app/services/uploaded_collection_thumbnail_path_service.rb b/app/services/uploaded_collection_thumbnail_path_service.rb index 18a3f662e..3685f9bb9 100644 --- a/app/services/uploaded_collection_thumbnail_path_service.rb +++ b/app/services/uploaded_collection_thumbnail_path_service.rb @@ -7,14 +7,12 @@ def call(object) "/uploads/uploaded_collection_thumbnails/#{object.id}/#{object.id}_card.jpg" end - # rubocop:disable Metrics/LineLength, Rails/FilePath, Lint/StringConversionInInterpolation def uploaded_thumbnail?(collection) - File.exist?("#{Rails.root.to_s}/public/uploads/uploaded_collection_thumbnails/#{collection.id}/#{collection.id}_card.jpg") + File.exist?(File.join(upload_dir(collection), "#{collection.id}_card.jpg")) end def upload_dir(collection) - "#{Rails.root.to_s}/public/uploads/uploaded_collection_thumbnails/#{collection.id}" + Hyku::Application.path_for("public/uploads/uploaded_collection_thumbnails/#{collection.id}") end - # rubocop:enable Metrics/LineLength, Rails/FilePath, Lint/StringConversionInInterpolation end end diff --git a/app/views/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) %> +
    +
  • + + <%= range_display(field_name) %> + <%= link_to remove_range_param(field_name), :class=>"remove", :title => t('blacklight.range_limit.remove_limit') do %> + + [<%= t('blacklight.range_limit.remove_limit') %>] + <% end %> + + <%= number_with_delimiter(@response.total) %> +
  • +
+ + <% 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/admin/appearances/_default_colors_form.html.erb b/app/views/hyrax/admin/appearances/_default_colors_form.html.erb index 9a8251957..69c66108f 100644 --- a/app/views/hyrax/admin/appearances/_default_colors_form.html.erb +++ b/app/views/hyrax/admin/appearances/_default_colors_form.html.erb @@ -1,6 +1,6 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %>
- <% @form.class::DEFAULT_COLORS.each do |color_name, hex| %> + <% @form.default_colors.each do |color_name, hex| %> <%= render 'color_input', f: f, color_name: color_name, hex: hex %> <% end %>
@@ -8,4 +8,4 @@ <%= link_to 'Restore All Defaults', '#color', class: 'btn btn-default restore-all-default-colors' %> <%= f.submit class: 'btn btn-primary pull-right' %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb b/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb index 3df04aba3..04e6192b2 100644 --- a/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb +++ b/app/views/hyrax/admin/appearances/_default_fonts_form.html.erb @@ -1,6 +1,6 @@ <%= simple_form_for @form, url: admin_appearance_path do |f| %>
- <% df = @form.class::DEFAULT_FONTS %> + <% df = @form.default_fonts %> <% font = f.object.body_font %> <%= f.input :body_font, label: 'Select Body Font', required: false, input_html: { class: 'font-fields', data: { default_value: df['body_font'] } } %> @@ -14,4 +14,4 @@ <%= link_to 'Restore All Defaults', '#font', class: 'btn btn-default restore-all-default-fonts' %> <%= f.submit class: 'btn btn-primary pull-right' %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hyrax/content_blocks/_form.html.erb b/app/views/hyrax/content_blocks/_form.html.erb index 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/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb b/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb index 33306d375..7c78b27a4 100644 --- a/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb +++ b/app/views/hyrax/dashboard/collections/_current_thumbnail.html.erb @@ -1,5 +1,5 @@ <% thumbnail_path = SolrDocument.find(@collection.id).thumbnail_path %> - <% if thumbnail_path.include?("uploaded_collection_thumbnails") and File.exist? Rails.root.join("public#{::SolrDocument.find(@collection.id).thumbnail_path}") %> + <% if thumbnail_path.include?("uploaded_collection_thumbnails") and File.exist? Hyku::Application.path_for("public#{::SolrDocument.find(@collection.id).thumbnail_path}") %> <%= image_tag(thumbnail_path, class: "current-thumbnail") %>

    Current image: <%= @thumbnail_filename %>

    <% else %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d6c1c0ab3..0f1c161e7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ - Hyku + <%= Hyku::Application.html_head_title %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> diff --git a/app/views/records/show_fields/_license.html.erb b/app/views/records/show_fields/_license.html.erb new file mode 100644 index 000000000..15cef91e0 --- /dev/null +++ b/app/views/records/show_fields/_license.html.erb @@ -0,0 +1,4 @@ +<% service = Hyrax::LicenseService.new %> +<% record.license.each do |r| %> + <%= link_to_field('license', r, service.label(r)) %> <%= iconify_auto_link(r, false) %>
    +<% end %> diff --git a/bin/worker b/bin/worker index b7605486b..7698b346e 100755 --- a/bin/worker +++ b/bin/worker @@ -9,4 +9,10 @@ else puts 'DATABASE_URL not set, no pool change needed' end -exec "echo $DATABASE_URL && bundle exec sidekiq" +queue = ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') +case queue +when 'sidekiq' + exec "echo $DATABASE_URL && bundle exec sidekiq" +when 'good_job' + exec "echo $DATABASE_URL && bundle exec good_job start" +end diff --git a/config/application.rb b/config/application.rb index 88b9b87e3..3b4f85fcb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -35,12 +35,53 @@ def self.utf_8_encode(string) end class Application < Rails::Application + ## + # @!group Class Attributes + # + # @!attribute html_head_title + # The title to render for the application's HTML > HEAD > TITLE element. + # @return [String] + class_attribute :html_head_title, default: "Hyku", instance_accessor: false + + # @!attribute user_devise_parameters + # @return [Object] + # + # This is a value that you want to set in the before_initialize block. + class_attribute :user_devise_parameters, instance_accessor: false, default: [ + :database_authenticatable, + :invitable, + :registerable, + :recoverable, + :rememberable, + :trackable, + :validatable, + :omniauthable, { omniauth_providers: %i[saml openid_connect cas] }] + + # @!endgroup Class Attributes + # Add this line to load the lib folder first because we need + # IiifPrint::SplitPdfs::AdventistPagesToJpgsSplitter config.autoload_paths.unshift("#{Rails.root}/lib") # Add the middleware directory to the eager load paths config.eager_load_paths << "#{Rails.root}/app/middleware" + ## + # @api public + # + # @param relative_path [String] lookup the relative paths first in the Knapsack then in Hyku. + # + # @return [String] the path to the file, favoring those found in the knapsack but falling back + # to those in the Rails.root. + def self.path_for(relative_path) + if defined?(HykuKnapsack) + engine_path = HykuKnapsack::Engine.root.join(relative_path) + return engine_path.to_s if engine_path.exist? + end + + Rails.root.join(relative_path).to_s + end + # 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. @@ -65,8 +106,12 @@ class Application < Rails::Application config.to_prepare do - # Add any extra services before IiifPrint::PluggableDerivativeService to enable processing - Hyrax::DerivativeService.services = [IiifPrint::PluggableDerivativeService] + # 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 = [ + 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 @@ -76,7 +121,7 @@ class Application < Rails::Application # IiifPrint::DerivativeRodeoService, # Hyrax::FileSetDerivativesService] - DerivativeRodeo::Generators::HocrGenerator.additional_tessearct_options = "-l eng_best" + DerivativeRodeo::Generators::HocrGenerator.additional_tessearct_options = nil # Allows us to use decorator files Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")).sort.each do |c| @@ -122,7 +167,7 @@ class Application < Rails::Application # # Psych::DisallowedClass: Tried to load unspecified class: config.after_initialize do - config.active_record.yaml_column_permitted_classes = [ + yaml_column_permitted_classes = [ Symbol, Hash, Array, @@ -131,8 +176,10 @@ class Application < Rails::Application User, Time ] + config.active_record.yaml_column_permitted_classes = yaml_column_permitted_classes + # Seems at some point `ActiveRecord::Base.yaml_column_permitted_classes` loses all the values we set above + # so we need to set it again here. + ActiveRecord::Base.yaml_column_permitted_classes = yaml_column_permitted_classes end - - end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 07f298607..c6cf36d04 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -4,7 +4,12 @@ # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = !!Sidekiq.server? + if ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') == 'sidekiq' + config.cache_classes = !!Sidekiq.server? + else + config.cache_classes = false + end + # Do not eager load code on boot. config.eager_load = false diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index a2647571d..53934a77b 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -12,7 +12,7 @@ # Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace. # A typical example would be a Customer or Tenant model that stores each Tenant's information. - config.excluded_models = %w{ Account AccountCrossSearch DomainName Endpoint User UserStat SolrEndpoint FcrepoEndpoint RedisEndpoint } + config.excluded_models = %w{ Account AccountCrossSearch DomainName Endpoint User UserStat SolrEndpoint FcrepoEndpoint RedisEndpoint GoodJob::Execution GoodJob::Job GoodJob::Process } # In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment. # You can make this dynamic by providing a Proc object to be called on migrations. diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb new file mode 100644 index 000000000..0fe0259a7 --- /dev/null +++ b/config/initializers/good_job.rb @@ -0,0 +1,37 @@ + +# frozen_string_literal: true + +if ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') == 'good_job' + Rails.application.configure do + # Configure options individually... + config.good_job.preserve_job_records = true + config.good_job.retry_on_unhandled_error = false + config.good_job.on_thread_error = ->(exception) { Raven.capture_exception(exception) } + config.good_job.execution_mode = :external + # config.good_job.queues = '*' + config.good_job.shutdown_timeout = 60 # seconds + config.good_job.poll_interval = 5 + # config.good_job.enable_cron = true + # config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } } + end + + # Wrapping this in an after_initialize block to ensure that all constants are loaded + Rails.application.config.after_initialize do + # baseline of 0, higher is sooner + Bulkrax::ScheduleRelationshipsJob.priority = 50 + CreateDerivativesJob.priority = 40 + CharacterizeJob.priority = 30 + Hyrax::GrantEditToMembersJob.priority = 10 + ImportUrlJob.priority = 10 + IngestJob.priority = 10 + ApplicationJob.priority = 0 + AttachFilesToWorkJob.priority = -1 + Bulkrax::ImportWorkJob.priority = -5 + Bulkrax::ImportFileSetJob.priority = -15 + Bulkrax::CreateRelationshipsJob.priority = -20 + Bulkrax::ImporterJob.priority = -20 + IiifPrint::Jobs::CreateRelationshipsJob.priority = -20 + ContentDepositEventJob.priority = -50 + ContentUpdateEventJob.priority = -50 + end +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 6377db666..dc3a30bae 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,10 +1,13 @@ -config = YAML.load(ERB.new(IO.read(Rails.root + 'config' + 'redis.yml')).result)[Rails.env].with_indifferent_access -redis_config = config.merge(thread_safe: true) +if ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') == 'sidekiq' -Sidekiq.configure_server do |s| - s.redis = redis_config -end + config = YAML.load(ERB.new(IO.read(Rails.root + 'config' + 'redis.yml')).result)[Rails.env].with_indifferent_access + redis_config = config.merge(thread_safe: true) + + Sidekiq.configure_server do |s| + s.redis = redis_config + end -Sidekiq.configure_client do |s| - s.redis = redis_config + Sidekiq.configure_client do |s| + s.redis = redis_config + end end diff --git a/config/routes.rb b/config/routes.rb index a960e3203..b3132ebde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,10 +2,13 @@ # OVERRIDE Hyrax 2.9.0 to add featured collection routes -require 'sidekiq/web' +if ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') == 'sidekiq' + require 'sidekiq/web' +end 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 @@ -13,7 +16,13 @@ mount Riiif::Engine => 'images', as: :riiif if Hyrax.config.iiif_image_server? authenticate :user, ->(u) { u.is_superadmin || u.is_admin } do - mount Sidekiq::Web => '/jobs' + queue = ENV.fetch('HYRAX_ACTIVE_JOB_QUEUE', 'sidekiq') + case queue + when 'sidekiq' + mount Sidekiq::Web => '/jobs' + when 'good_job' + mount GoodJob::Engine => '/jobs' + end end if ActiveModel::Type::Boolean.new.cast(ENV.fetch('HYKU_MULTITENANT', false)) @@ -70,9 +79,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 +92,7 @@ concerns :oai_provider concerns :searchable + concerns :range_searchable end resources :solr_documents, only: [:show], path: '/catalog', controller: 'catalog' do diff --git a/config/uv/uv.html b/config/uv/uv.html index c8e8eced5..c44dbb45d 100644 --- a/config/uv/uv.html +++ b/config/uv/uv.html @@ -19,20 +19,20 @@ } - + - +
    - + diff --git a/db/migrate/20230406183810_setup_good_jobs_schema.rb b/db/migrate/20230406183810_setup_good_jobs_schema.rb new file mode 100644 index 000000000..5ffdffee0 --- /dev/null +++ b/db/migrate/20230406183810_setup_good_jobs_schema.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# This migration pre-sets the shared_extensions schema for in prep for good_jobs +class SetupGoodJobsSchema < ActiveRecord::Migration[5.2] + def change + # 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 diff --git a/db/migrate/20230406183814_create_good_jobs.rb b/db/migrate/20230406183814_create_good_jobs.rb new file mode 100644 index 000000000..d86ab61ac --- /dev/null +++ b/db/migrate/20230406183814_create_good_jobs.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +class CreateGoodJobs < ActiveRecord::Migration[5.2] + def change + enable_extension 'pgcrypto' + + create_table :good_jobs, id: :uuid do |t| + t.text :queue_name + t.integer :priority + t.jsonb :serialized_params + t.timestamp :scheduled_at + t.timestamp :performed_at + t.timestamp :finished_at + t.text :error + + t.timestamps + + t.uuid :active_job_id + t.text :concurrency_key + t.text :cron_key + t.uuid :retried_good_job_id + t.timestamp :cron_at + end + + create_table :good_job_processes, id: :uuid do |t| + t.timestamps + t.jsonb :state + end + + add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: "index_good_jobs_on_scheduled_at" + add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)", name: :index_good_jobs_on_queue_name_and_scheduled_at + add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at + add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished + add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at + add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true + add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id + add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at + end +end diff --git a/db/schema.rb b/db/schema.rb index a7661fc8c..f65809e9f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2,11 +2,11 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. @@ -315,6 +315,37 @@ t.index ["user_id"], name: "index_file_view_stats_on_user_id" end + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + end + + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + create_table "group_roles", force: :cascade do |t| t.bigint "role_id" t.bigint "group_id" diff --git a/docker-compose.yml b/docker-compose.yml index e618b5c47..f23f27273 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,7 +135,7 @@ services: web: <<: *app - # Uncomment command to access container with out starting Rails. Useful for debugging + # Uncomment command to access container with out starting bin/web. Useful for debugging # command: sleep infinity environment: - VIRTUAL_PORT=3000 @@ -167,6 +167,8 @@ services: worker: <<: *app image: ghcr.io/samvera/hyku/worker:${TAG:-latest} + # Uncomment command to access container with out starting bin/worker. Useful for debugging + # command: sleep infinity build: context: . target: hyku-worker @@ -176,7 +178,6 @@ services: - ghcr.io/samvera/hyku/base:latest - ghcr.io/samvera/hyku:latest - ghcr.io/samvera/hyku/worker:latest - command: bundle exec sidekiq depends_on: check_volumes: condition: service_completed_successfully diff --git a/lib/iiif_manifest/v3/manifest_builder/canvas_builder_decorator.rb b/lib/iiif_manifest/v3/manifest_builder/canvas_builder_decorator.rb new file mode 100644 index 000000000..990bd1409 --- /dev/null +++ b/lib/iiif_manifest/v3/manifest_builder/canvas_builder_decorator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# OVERRIDE IIIFManifest v1.3.1 to use the parent's title as the label instead of the filename + +module IIIFManifest + module V3 + module ManifestBuilderDecorator + module CanvasBuilderDecorator + def apply_record_properties + super + canvas.label = if record.to_s.present? + ManifestBuilder.language_map(record['parent_title_tesim']&.first || record.to_s) + end + end + end + end + end +end + +IIIFManifest::V3::ManifestBuilder.prepend(IIIFManifest::V3::ManifestBuilderDecorator) +IIIFManifest::V3::ManifestBuilder::CanvasBuilder.prepend(IIIFManifest::V3::ManifestBuilder::CanvasBuilderDecorator) 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 index 2edd1e108..84e3b2c50 100644 --- a/lib/wings/services/custom_queries/find_ids_by_model_decorator.rb +++ b/lib/wings/services/custom_queries/find_ids_by_model_decorator.rb @@ -29,10 +29,12 @@ def find_ids_by_model(model:, 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'] + solr_response = ActiveFedora::SolrService.post( + solr_query, + fl: 'id', + rows: @query_rows, + start: solr_response['start'] + @query_rows + )['response'] end end end diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb new file mode 100644 index 000000000..a3ab79038 --- /dev/null +++ b/spec/config/application_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Hyku::Application do + describe '.html_head_title' do + subject { described_class.html_head_title } + + it { is_expected.to be_a(String) } + end + + describe '.user_devise_parameters' do + subject { described_class.user_devise_parameters } + + it do + is_expected.to eq([:database_authenticatable, + :invitable, + :registerable, + :recoverable, + :rememberable, + :trackable, + :validatable, + :omniauthable, + { omniauth_providers: %i[saml openid_connect cas] }]) + end + end +end diff --git a/spec/controllers/search_history_controller_spec.rb b/spec/controllers/search_history_controller_spec.rb new file mode 100644 index 000000000..379e29e59 --- /dev/null +++ b/spec/controllers/search_history_controller_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe SearchHistoryController, type: :controller 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 diff --git a/spec/forms/hyrax/forms/admin/appearance_spec.rb b/spec/forms/hyrax/forms/admin/appearance_spec.rb new file mode 100644 index 000000000..466c50e3b --- /dev/null +++ b/spec/forms/hyrax/forms/admin/appearance_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Hyrax::Forms::Admin::Appearance do + describe '.default_fonts' do + subject { described_class.default_fonts } + + it { is_expected.to be_a(Hash) } + + it "has the 'body_font' and 'headline_font' keys" do + expect(subject.keys).to match_array(['body_font', 'headline_font']) + end + end + + describe '.default_colors' do + subject { described_class.default_colors } + + it { is_expected.to be_a(Hash) } + end +end diff --git a/spec/requests/catalog_controller_spec.rb b/spec/requests/catalog_controller_spec.rb index 818b80bd3..c36a9c43d 100644 --- a/spec/requests/catalog_controller_spec.rb +++ b/spec/requests/catalog_controller_spec.rb @@ -56,6 +56,18 @@ before do host! "http://#{cross_search_tenant_account.cname}/" + black_light_config.add_search_field('title') do |field| + field.solr_parameters = { + "spellcheck.dictionary": "title" + } + solr_name = 'title_tesim' + field.solr_local_parameters = { + qf: solr_name, + pf: solr_name + } + end + black_light_config.advanced_search ||= Blacklight::OpenStructWithHashAccess.new + black_light_config.advanced_search[:query_parser] ||= 'dismax' end context 'can fetch data from other tenants' do @@ -66,7 +78,7 @@ # get '/catalog', params: { q: '*' } # get search_catalog_url, params: { locale: 'en', q: 'test' } - get "http://#{cross_search_tenant_account.cname}/catalog?q=test" # , params: { q: 'test' } + get "http://#{cross_search_tenant_account.cname}/catalog?q=test", params: { q: 'title' } expect(response.status).to eq(200) end end diff --git a/spec/tasks/rake_spec.rb b/spec/tasks/rake_spec.rb index 3543bf304..c4734c2cf 100644 --- a/spec/tasks/rake_spec.rb +++ b/spec/tasks/rake_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rake' -load 'app/models/site.rb' +load Rails.root.join('app', 'models', 'site.rb') RSpec.describe "Rake tasks" do before(:all) do diff --git a/spec/views/hyrax/content_blocks/edit.html.erb_spec.rb b/spec/views/hyrax/content_blocks/edit.html.erb_spec.rb index 9039fa2bf..5dbd31afa 100644 --- a/spec/views/hyrax/content_blocks/edit.html.erb_spec.rb +++ b/spec/views/hyrax/content_blocks/edit.html.erb_spec.rb @@ -36,7 +36,7 @@ end it "renders the instruction blocks" do - expect(rendered).to have_xpath('//p[@class="content-block-instructions" ]', count: 4) + expect(rendered).to have_xpath('//p[@class="content-block-instructions" ]', count: 6) end # TODO: These next 4 tests are tightly coupled with the implimentation,