├── .all-contributorsrc ├── .codeclimate.yml ├── .eslintrc ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .solr_wrapper ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── arclight │ │ │ └── logo.png │ │ └── blacklight │ │ │ ├── bookmark.svg │ │ │ ├── collection.svg │ │ │ ├── compact.svg │ │ │ ├── container.svg │ │ │ ├── ead.svg │ │ │ ├── file.svg │ │ │ ├── folder.svg │ │ │ ├── list.svg │ │ │ ├── minus.svg │ │ │ ├── online.svg │ │ │ ├── pdf.svg │ │ │ ├── plus.svg │ │ │ └── repository.svg │ ├── javascripts │ │ └── arclight │ │ │ ├── arclight.js │ │ │ ├── oembed_controller.js │ │ │ └── truncate_controller.js │ └── stylesheets │ │ └── arclight │ │ ├── application.scss │ │ ├── build.scss │ │ ├── modules │ │ ├── collection_search.scss │ │ ├── hierarchy_and_online_contents.scss │ │ ├── highlights.scss │ │ ├── icons.scss │ │ ├── layout.scss │ │ ├── mastheads.scss │ │ ├── repositories.scss │ │ ├── repository_card.scss │ │ ├── search_form.scss │ │ ├── search_results.scss │ │ ├── show_collection.scss │ │ └── truncator.scss │ │ ├── responsive.scss │ │ └── variables.scss ├── components │ ├── arclight │ │ ├── access_component.html.erb │ │ ├── access_component.rb │ │ ├── bookmark_component.html.erb │ │ ├── bookmark_component.rb │ │ ├── breadcrumb_component.rb │ │ ├── breadcrumbs_hierarchy_component.html.erb │ │ ├── breadcrumbs_hierarchy_component.rb │ │ ├── collection_context_component.html.erb │ │ ├── collection_context_component.rb │ │ ├── collection_info_component.html.erb │ │ ├── collection_info_component.rb │ │ ├── collection_sidebar_component.html.erb │ │ ├── collection_sidebar_component.rb │ │ ├── constraints_component.html.erb │ │ ├── constraints_component.rb │ │ ├── document_collection_context_component.html.erb │ │ ├── document_collection_context_component.rb │ │ ├── document_collection_hierarchy_component.html.erb │ │ ├── document_collection_hierarchy_component.rb │ │ ├── document_component.html.erb │ │ ├── document_component.rb │ │ ├── document_components_hierarchy_component.html.erb │ │ ├── document_components_hierarchy_component.rb │ │ ├── document_download_component.html.erb │ │ ├── document_download_component.rb │ │ ├── embed_component.html.erb │ │ ├── embed_component.rb │ │ ├── expand_hierarchy_button_component.html.erb │ │ ├── expand_hierarchy_button_component.rb │ │ ├── group_component.html.erb │ │ ├── group_component.rb │ │ ├── header_component.html.erb │ │ ├── header_component.rb │ │ ├── hierarchy_toggle_component.html.erb │ │ ├── hierarchy_toggle_component.rb │ │ ├── index_metadata_field_component.html.erb │ │ ├── index_metadata_field_component.rb │ │ ├── masthead_component.html.erb │ │ ├── masthead_component.rb │ │ ├── metadata_section_component.html.erb │ │ ├── metadata_section_component.rb │ │ ├── oembed_viewer_component.html.erb │ │ ├── oembed_viewer_component.rb │ │ ├── online_content_filter_component.html.erb │ │ ├── online_content_filter_component.rb │ │ ├── online_status_indicator_component.rb │ │ ├── repository_breadcrumb_component.html.erb │ │ ├── repository_breadcrumb_component.rb │ │ ├── repository_location_component.html.erb │ │ ├── repository_location_component.rb │ │ ├── search_bar_component.html.erb │ │ ├── search_bar_component.rb │ │ ├── search_result_breadcrumbs_component.html.erb │ │ ├── search_result_breadcrumbs_component.rb │ │ ├── search_result_component.html.erb │ │ ├── search_result_component.rb │ │ ├── search_result_title_component.html.erb │ │ ├── search_result_title_component.rb │ │ ├── sidebar_component.html.erb │ │ ├── sidebar_component.rb │ │ └── upper_metadata_layout_component.rb │ └── blacklight │ │ └── icons │ │ ├── bookmark_component.rb │ │ ├── collection_component.rb │ │ ├── compact_component.rb │ │ ├── container_component.rb │ │ ├── ead_component.rb │ │ ├── file_component.rb │ │ ├── folder_component.rb │ │ ├── minus_component.rb │ │ ├── online_component.rb │ │ ├── pdf_component.rb │ │ ├── plus_component.rb │ │ └── repository_component.rb ├── controllers │ └── arclight │ │ └── repositories_controller.rb ├── helpers │ ├── arclight │ │ ├── ead_format_helpers.rb │ │ └── field_config_helpers.rb │ └── arclight_helper.rb ├── models │ ├── arclight │ │ ├── document_downloads.rb │ │ ├── parent.rb │ │ ├── parents.rb │ │ └── requests │ │ │ ├── aeon_external_request.rb │ │ │ ├── aeon_web_ead.rb │ │ │ └── google_form.rb │ └── concerns │ │ └── arclight │ │ ├── catalog.rb │ │ ├── search_behavior.rb │ │ └── solr_document.rb ├── presenters │ └── arclight │ │ ├── index_presenter.rb │ │ └── show_presenter.rb └── views │ ├── arclight │ ├── .keep │ ├── _requests.html.erb │ ├── repositories │ │ ├── _repository.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ └── requests │ │ ├── _aeon_external_request_endpoint.html.erb │ │ ├── _aeon_web_ead.html.erb │ │ └── _google_form.html.erb │ ├── catalog │ ├── _document_list.html.erb │ ├── _group.html.erb │ ├── _group_toggle.html.erb │ ├── _search_results_header.html.erb │ ├── hierarchy.html.erb │ └── index.html.erb │ └── shared │ ├── _breadcrumbs.html.erb │ └── _main_menu_links.html.erb ├── arclight.gemspec ├── bin ├── console ├── rails └── setup ├── config ├── breadcrumbs.rb ├── i18n-tasks.yml ├── importmap.rb ├── locales │ └── arclight.en.yml ├── repositories.yml └── routes.rb ├── docker-compose.yml ├── lib ├── arclight.rb ├── arclight │ ├── digital_object.rb │ ├── engine.rb │ ├── exceptions.rb │ ├── hash_absolute_xpath.rb │ ├── level_label.rb │ ├── missing_id_strategy.rb │ ├── normalized_date.rb │ ├── normalized_id.rb │ ├── normalized_title.rb │ ├── repository.rb │ ├── routes.rb │ ├── routes │ │ └── hierarchy.rb │ ├── traject │ │ ├── ead2_component_config.rb │ │ ├── ead2_config.rb │ │ └── nokogiri_namespaceless_reader.rb │ ├── version.rb │ └── year_range.rb ├── generators │ └── arclight │ │ ├── install_generator.rb │ │ ├── templates │ │ ├── arclight.scss │ │ ├── catalog_controller.rb │ │ └── config │ │ │ ├── downloads.yml │ │ │ ├── locales │ │ │ └── arclight.en.yml │ │ │ └── repositories.yml │ │ └── update_generator.rb └── tasks │ └── index.rake ├── package.json ├── solr └── conf │ ├── _rest_managed.json │ ├── admin-extra.html │ ├── elevate.xml │ ├── mapping-ISOLatin1Accent.txt │ ├── protwords.txt │ ├── schema.xml │ ├── solrconfig.xml │ ├── spellings.txt │ ├── stopwords.txt │ ├── stopwords_en.txt │ ├── stopwords_punctuation.txt │ ├── synonyms.txt │ └── xslt │ ├── example.xsl │ ├── example_atom.xsl │ ├── example_rss.xsl │ └── luke.xsl ├── spec ├── arclight_spec.rb ├── components │ └── arclight │ │ ├── breadcrumb_component_spec.rb │ │ ├── collection_sidebar_component_spec.rb │ │ ├── document_download_component_spec.rb │ │ ├── expand_hierarchy_button_component_spec.rb │ │ ├── hierarchy_toggle_component_spec.rb │ │ ├── repository_location_component_spec.rb │ │ └── search_bar_component_spec.rb ├── controllers │ ├── arclight │ │ └── repositories_controller_spec.rb │ └── catalog_controller_spec.rb ├── features │ ├── aeon_web_ead_request_spec.rb │ ├── arclight_spec.rb │ ├── autocomplete_spec.rb │ ├── bookmarks_spec.rb │ ├── collection_context_spec.rb │ ├── collection_filtering_spec.rb │ ├── collection_page_spec.rb │ ├── compact_search_results_spec.rb │ ├── component_page_spec.rb │ ├── document_tools_spec.rb │ ├── fielded_search_results_spec.rb │ ├── google_form_request_spec.rb │ ├── grouped_results_spec.rb │ ├── highlighted_search_results_spec.rb │ ├── item_breadcrumbs_spec.rb │ ├── many_component_ead_spec.rb │ ├── masthead_links_spec.rb │ ├── online_content_spec.rb │ ├── repositories_page_spec.rb │ ├── search_breadcrumb_spec.rb │ ├── search_query_spec.rb │ ├── search_results_spec.rb │ └── traject │ │ └── ead2_indexing_spec.rb ├── fixtures │ ├── config │ │ ├── downloads.yml │ │ └── repositories.yml │ ├── ead │ │ ├── nlm │ │ │ ├── alphaomegaalpha.xml │ │ │ └── ncaids544-id-test.xml │ │ ├── sample │ │ │ ├── deeply-nested-components.xml │ │ │ ├── large-components-list.xml │ │ │ └── no-ids-recordgrp-level.xml │ │ └── sul-spec │ │ │ ├── a0011.xml │ │ │ ├── m0198_from_ASpace.xml │ │ │ └── pc0170.xml │ └── xsd │ │ └── ead.xsd ├── helpers │ ├── arclight │ │ └── ead_format_helpers_spec.rb │ └── arclight_helper_spec.rb ├── i18n_spec.rb ├── lib │ └── arclight │ │ ├── digital_object_spec.rb │ │ ├── hash_absolute_xpath_spec.rb │ │ ├── level_label_spec.rb │ │ ├── missing_id_strategy_spec.rb │ │ ├── normalized_date_spec.rb │ │ ├── normalized_id_spec.rb │ │ ├── normalized_title_spec.rb │ │ ├── repository_spec.rb │ │ └── year_range_spec.rb ├── models │ ├── arclight │ │ ├── document_downloads_spec.rb │ │ ├── parents_spec.rb │ │ └── requests │ │ │ ├── aeon_external_request_spec.rb │ │ │ ├── aeon_web_ead_spec.rb │ │ │ └── google_form_spec.rb │ └── concerns │ │ └── arclight │ │ ├── search_behavior_spec.rb │ │ └── solr_document_spec.rb ├── presenters │ └── arclight │ │ ├── index_presenter_spec.rb │ │ └── show_presenter_spec.rb ├── routing │ ├── arclight │ │ └── repositories_spec.rb │ └── collections_spec.rb ├── spec_helper.rb ├── support │ └── controller_level_helpers.rb ├── test_app_templates │ ├── Gemfile.extra │ └── lib │ │ └── generators │ │ └── test_app_generator.rb └── views │ ├── _requests.html.erb_spec.rb │ └── repositories │ ├── index.html.erb_spec.rb │ └── show.html.erb_spec.rb ├── tasks └── arclight.rake └── template.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | exclude_patterns: 3 | - "app/assets/javascripts/arclight/context_navigation.js" 4 | - "vendor/assets/javascripts/**/*.js" 5 | - "spec/**/*.rb" 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2022, 7 | "sourceType": "module" 8 | }, 9 | "extends": "airbnb-base/legacy", 10 | "globals": { 11 | "Blacklight": true, 12 | "bootstrap": true, 13 | "Stimulus": true, 14 | }, 15 | "rules": { 16 | "no-param-reassign": [2, { "props": false }], 17 | "func-names": ["error", "never"], 18 | "indent": ["warn"], 19 | "quotes": ["error", "single", { "avoidEscape": true }], 20 | "semi": ["warn", "never"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: test (ruby ${{ matrix.ruby }} / rails ${{ matrix.rails_version }} ${{ matrix.additional_name }}) 13 | strategy: 14 | matrix: 15 | rails_version: [8.0.1] 16 | ruby: ["3.4"] 17 | additional_engine_cart_rails_options: [""] 18 | additional_name: [""] 19 | include: 20 | - ruby: "3.3" 21 | rails_version: 7.2.2.1 22 | - ruby: "3.2" 23 | rails_version: 7.1.5.1 24 | - ruby: "3.4" 25 | rails_version: "8.0.1" 26 | blacklight_version: 9.0.0.beta1 27 | additional_name: "/ Blacklight 9" 28 | env: 29 | RAILS_VERSION: ${{ matrix.rails_version }} 30 | ENGINE_CART_RAILS_OPTIONS: "--skip-git --skip-listen --skip-spring --skip-keeps --skip-kamal --skip-solid --skip-coffee --skip-test --css bootstrap -a propshaft -j importmap ${{ matrix.additional_engine_cart_rails_options }}" 31 | BLACKLIGHT_VERSION: ${{ matrix.blacklight_version }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby ${{ matrix.ruby }} 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | bundler: latest 38 | ruby-version: ${{ matrix.ruby }} 39 | - name: Install dependencies with Rails ${{ matrix.rails_version }} 40 | run: bundle install 41 | - name: Run tests 42 | run: bundle exec rake ci 43 | lint: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout code 47 | uses: actions/checkout@v4 48 | - name: Install Ruby and gems 49 | uses: ruby/setup-ruby@v1 50 | with: 51 | bundler: latest 52 | ruby-version: 3.2 53 | - name: Install dependencies with Bundler 54 | run: bundle install 55 | - name: Install dependencies with yarn 56 | run: yarn install 57 | - name: Lint Ruby files 58 | run: bundle exec rake rubocop eslint 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.ruby-version 3 | /.yardoc 4 | /Gemfile.lock 5 | /yarn.lock 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | /app/assets/builds/ 13 | 14 | # rspec failure tracking 15 | .rspec_status 16 | 17 | .internal_test_app 18 | 19 | /node_modules/ 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.solr_wrapper: -------------------------------------------------------------------------------- 1 | # Place any default configuration for solr_wrapper here 2 | # port: 8983 3 | collection: 4 | dir: solr/conf 5 | name: blacklight-core 6 | version: 9.7.0 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | ArcLight is a collaborative open source project where contributions are welcome. This contributing guide is borrowed in part from the [GeoBlacklight Contributing Guide](https://github.com/geoblacklight/geoblacklight/blob/main/CONTRIBUTING.md). 4 | 5 | ### Reporting issues 6 | 7 | Did you find a bug in ArcLight or interested in a new feature? Please make sure to add an issue for it in the [issue tracker](https://github.com/projectblacklight/arclight/issues) as follows: 8 | 9 | - Make sure you have a [GitHub account](https://github.com/signup/free) 10 | - Submit a new [Github issue](https://github.com/projectblacklight/arclight/issues) by: 11 | - Clearly describing the issue 12 | - Provide a descriptive summary 13 | - Explain the expected behavior 14 | - Explain the actual behavior 15 | - Provide steps to reproduce the actual behavior 16 | 17 | ### Contributing code or documentation 18 | 19 | ArcLight also welcomes code and documentation contributions. We follow the [pull request](https://help.github.com/articles/using-pull-requests/) model for contributing on GitHub. 20 | 21 | #### Pull request overview 22 | 23 | 1. Fork it ( http://github.com/projectblacklight/arclight/fork ) 24 | 2. Create your feature branch (`git checkout -b my-new-feature`) 25 | 3. Commit your changes (`git commit -am 'Add some feature'`) 26 | 4. Push to the branch (`git push origin my-new-feature`) 27 | 5. Create new Pull Request 28 | 29 | #### Merging Changes 30 | 31 | Here are some general rules to follow for merging pull requests. 32 | 33 | - It is considered "poor form" to merge your own request. 34 | - Please take the time to review the changes and get a sense of what is being changed. Things to consider: 35 | - Does the commit message explain what is going on? 36 | - Does the code changes have tests? _Not all changes need new tests, some changes are refactorings_ 37 | - Do all new methods, modules, and classes have comments? Do changed methods, modules, and classes have comments? 38 | - Does the commit contain more than it should? Are two separate concerns being addressed in one commit? 39 | - Did the Travis tests complete successfully? 40 | - If you are uncertain, bring other contributors into the conversation by creating a comment that includes their `@username`. 41 | - If you like the pull request, but want others to chime in, create a +1 comment and tag a user. 42 | 43 | If you wish to ask questions or participate further, see our general [README](https://github.com/projectblacklight/arclight/blob/main/README.md). 44 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in arclight.gemspec 6 | gemspec 7 | 8 | # BEGIN ENGINE_CART BLOCK 9 | # engine_cart: 2.5.0 10 | # engine_cart stanza: 2.5.0 11 | # the below comes from engine_cart, a gem used to test this Rails engine gem in the context of a Rails app. 12 | file = File.expand_path('Gemfile', ENV['ENGINE_CART_DESTINATION'] || ENV['RAILS_ROOT'] || File.expand_path('.internal_test_app', File.dirname(__FILE__))) 13 | if File.exist?(file) 14 | begin 15 | eval_gemfile file 16 | rescue Bundler::GemfileError => e 17 | Bundler.ui.warn '[EngineCart] Skipping Rails application dependencies:' 18 | Bundler.ui.warn e.message 19 | end 20 | else 21 | Bundler.ui.warn "[EngineCart] Unable to find test application dependencies in #{file}, using placeholder dependencies" 22 | 23 | if ENV['RAILS_VERSION'] 24 | if ENV['RAILS_VERSION'] == 'edge' 25 | gem 'rails', github: 'rails/rails' 26 | ENV['ENGINE_CART_RAILS_OPTIONS'] = '--edge --skip-turbolinks' 27 | else 28 | gem 'rails', ENV['RAILS_VERSION'] 29 | end 30 | 31 | case ENV['RAILS_VERSION'] 32 | when /^6.0/ 33 | gem 'sass-rails', '>= 6' 34 | gem 'webpacker', '~> 4.0' 35 | when /^5.[12]/ 36 | gem 'sass-rails', '~> 5.0' 37 | gem 'sprockets', '~> 3.7' 38 | gem 'thor', '~> 0.20' 39 | end 40 | end 41 | end 42 | # END ENGINE_CART BLOCK 43 | 44 | # Used by engine_cart if you need to change a specific version of a dependency 45 | eval_gemfile File.expand_path('spec/test_app_templates/Gemfile.extra', File.dirname(__FILE__)) 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 The Board of Trustees of the Leland Stanford Junior University 2 | Copyright (c) 2017, Regents of the University of Michigan. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require 'rubocop/rake_task' 9 | RuboCop::RakeTask.new(:rubocop) 10 | 11 | Dir.glob('./tasks/*.rake').each { |f| load f } 12 | Dir.glob('./lib/tasks/*.rake').each { |f| load f } 13 | 14 | require 'engine_cart/rake_task' 15 | 16 | task default: %i[rubocop eslint ci] 17 | -------------------------------------------------------------------------------- /app/assets/images/arclight/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/arclight/2fcfe481afcec80a5fe78b6ce45a415c70b123e9/app/assets/images/arclight/logo.png -------------------------------------------------------------------------------- /app/assets/images/blacklight/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/collection.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/compact.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/container.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/ead.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/file.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/list.svg: -------------------------------------------------------------------------------- 1 | file 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/online.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/repository.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/arclight/arclight.js: -------------------------------------------------------------------------------- 1 | import Truncate from 'arclight/truncate_controller' 2 | Stimulus.register('arclight-truncate', Truncate) 3 | import Oembed from 'arclight/oembed_controller' 4 | Stimulus.register('arclight-oembed', Oembed) 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/arclight/oembed_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus' 2 | 3 | export default class OembedController extends Controller { 4 | static values = { 5 | url: String 6 | } 7 | 8 | connect() { 9 | const loadedAttr = this.element.getAttribute('loaded') 10 | 11 | if (loadedAttr && loadedAttr === 'loaded') { 12 | return 13 | } 14 | 15 | fetch(this.urlValue) 16 | .then((response) => { 17 | if (response.ok) return response.text() 18 | throw new Error(`HTTP error, status = ${response.status}`) 19 | }) 20 | .then((body) => { 21 | const oEmbedEndPoint = this.findOEmbedEndPoint(body) 22 | if (!oEmbedEndPoint || oEmbedEndPoint.length === 0) { 23 | console.warn(`No oEmbed endpoint found in at ${this.urlValue}`) 24 | return 25 | } 26 | this.loadEndPoint(oEmbedEndPoint) 27 | }).catch(error => { 28 | console.error(error) 29 | }) 30 | } 31 | 32 | // We are choosing not to make this class static so that downstream classes 33 | // can override it and access values to populate extraParams. 34 | // E.g. https://github.com/sul-dlss/vt-arclight/blob/main/app/javascript/controllers/sul_embed_controller.js 35 | findOEmbedEndPoint(body, extraParams = {}) { // eslint-disable-line class-methods-use-this 36 | // Parse out link elements so image assets are not loaded 37 | const template = document.createElement('template') 38 | template.innerHTML = body.match(//g).join('') 39 | 40 | // Look for a link element containing the oEmbed endpoint; bail out if none 41 | const endpoint = template.content.querySelector('link[rel="alternate"][type="application/json+oembed"]') 42 | ?.getAttribute('href') 43 | if (!endpoint) return '' 44 | 45 | // Serialize any extra params and append them to the endpoint 46 | const qs = new URLSearchParams(extraParams).toString() 47 | return `${endpoint}&${qs}` 48 | } 49 | 50 | loadEndPoint(oEmbedEndPoint) { 51 | fetch(oEmbedEndPoint) 52 | .then((response) => response.json()) 53 | .then((json) => { 54 | this.element.innerHTML = json.html 55 | this.element.setAttribute('loaded', 'loaded') 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/assets/javascripts/arclight/truncate_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus' 2 | 3 | export default class extends Controller { 4 | static targets = ['content'] 5 | 6 | connect() { 7 | // target elements 8 | const contentInner = Array.from(this.contentTarget.children) 9 | 10 | // calculate total scrollable inner height vs. observed outer height 11 | const outerHeight = this.contentTarget.clientHeight 12 | const innerHeight = contentInner.map(e => e.scrollHeight).reduce((a, b) => a + b, 0) 13 | 14 | // truncation occurred if total inner height exceeds outer (observed) height. 15 | // if no longer truncated, reset the expanded state (e.g. on window resize). 16 | if (innerHeight > outerHeight) { 17 | this.element.classList.add('truncated') 18 | } else { 19 | this.element.classList.remove('truncated') 20 | this.contentTarget.classList.remove('expanded') 21 | } 22 | } 23 | 24 | trigger() { 25 | this.element.classList.toggle('expanded') 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/application.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'responsive'; 3 | @import 'modules/collection_search'; 4 | @import 'modules/highlights'; 5 | @import 'modules/hierarchy_and_online_contents'; 6 | @import 'modules/icons'; 7 | @import 'modules/layout'; 8 | @import 'modules/mastheads'; 9 | @import 'modules/repositories.scss'; 10 | @import 'modules/repository_card.scss'; 11 | @import 'modules/search_results'; 12 | @import 'modules/show_collection'; 13 | @import 'modules/truncator'; 14 | @import 'modules/search_form'; 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/build.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap/scss/functions"; 2 | @import "bootstrap/scss/mixins/breakpoints"; 3 | @import "bootstrap/scss/variables"; 4 | @import "application"; 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/collection_search.scss: -------------------------------------------------------------------------------- 1 | .al-collection-count { 2 | padding-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/highlights.scss: -------------------------------------------------------------------------------- 1 | .al-document-highlight { 2 | padding-top: 10px; 3 | padding-left: 10px; 4 | font-size: $font-size-sm; 5 | font-style: italic; 6 | margin-bottom: $spacer; 7 | em { 8 | background-color: $background-highlight; 9 | font-weight: bold; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/icons.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --al-online-icon-color: #{$online-icon-color}; 3 | } 4 | .toggle-bookmark, 5 | .breadcrumb-item, 6 | .document, 7 | .al-online-content-icon { 8 | .blacklight-icons svg { 9 | height: 1rem; 10 | width: 1rem; 11 | } 12 | } 13 | 14 | .al-online-content-icon .blacklight-icons svg { 15 | fill: var(--al-online-icon-color); 16 | } 17 | 18 | .btn > .bi:first-child { 19 | margin-inline-end: 0.25rem !important; 20 | } 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/mastheads.scss: -------------------------------------------------------------------------------- 1 | .navbar-search { 2 | border-bottom: $default-border-styling; 3 | border-top: $default-border-styling; 4 | margin-bottom: ($spacer * 0.5); 5 | z-index: 10; 6 | 7 | .search-query-form { 8 | max-width: 100% !important; // work around blacklight styles 9 | display: flex; 10 | gap: 0.5rem; 11 | 12 | // wrap collection dropdown onto its own line on smaller screens 13 | @include media-breakpoint-down(md) { 14 | flex-wrap: wrap; 15 | } 16 | 17 | .input-group { 18 | width: unset; 19 | flex: auto; 20 | } 21 | 22 | .within-collection-dropdown { 23 | flex: none; 24 | } 25 | } 26 | } 27 | 28 | .al-masthead { 29 | --al-mastead-active-link-color: var(--bs-gray-700); 30 | --al-masthead-title-size: #{$h2-font-size}; 31 | --bs-nav-link-font-weight: 700; 32 | 33 | .h1 { 34 | font-size: var(--al-masthead-title-size); 35 | } 36 | 37 | .navbar-nav { 38 | .active a { 39 | color: var(--al-mastead-active-link-color); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/repositories.scss: -------------------------------------------------------------------------------- 1 | .blacklight-repositories-show { 2 | .al-repository-show-header { 3 | margin-bottom: ($spacer * 0.5); 4 | margin-top: ($spacer * 2); 5 | } 6 | 7 | .al-repository-collections{ 8 | font-size: $font-size-sm; 9 | 10 | &::after { 11 | content: '\bb'; 12 | } 13 | } 14 | 15 | .al-document-title-bar { 16 | border-top: $default-border-styling; 17 | margin-bottom: $spacer; 18 | padding-top: ($spacer * 0.5); 19 | 20 | .al-document-abstract-or-scope, 21 | .al-collection-id { 22 | font-size: $font-size-sm; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/repository_card.scss: -------------------------------------------------------------------------------- 1 | .al-repository { 2 | border: $default-border-styling; 3 | } 4 | 5 | @include media-breakpoint-up(sm) { 6 | .al-repository-description { 7 | border-left: $default-border-styling; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/search_form.scss: -------------------------------------------------------------------------------- 1 | // Original style comes from https://github.com/twbs/bootstrap/blob/v5.3.2/scss/_dropdown.scss#L184 2 | // via https://github.com/projectblacklight/blacklight/blob/v8.1.0/app/assets/stylesheets/blacklight/_search_form.scss#L25 3 | .input-group > .search-autocomplete-wrapper { 4 | ul { 5 | li { 6 | white-space: normal; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/show_collection.scss: -------------------------------------------------------------------------------- 1 | // Collection context 2 | .al-show-sub-heading { 3 | font-size: $h6-font-size; 4 | text-transform: uppercase; 5 | } 6 | 7 | .al-collection-actions-menu { 8 | float: right; 9 | } 10 | 11 | // Access 12 | #access-and-use { 13 | dl dd { 14 | line-height: 1.4; 15 | margin-bottom: $spacer; 16 | } 17 | 18 | img { 19 | margin-bottom: $headings-margin-bottom; 20 | margin-right: $spacer; 21 | max-height: 150px; 22 | max-width: 150px; 23 | } 24 | 25 | .al-in-person-repository-name, 26 | .al-in-person-repository-location { 27 | font-size: $font-size-sm; 28 | line-height: 1.25; 29 | } 30 | 31 | .al-repository-contact-city_state_zip_country { 32 | margin-bottom: ($spacer * 0.5); 33 | } 34 | } 35 | 36 | .al-digital-object.breadcrumb-item { 37 | margin-bottom: ($spacer * 0.5); 38 | } 39 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/modules/truncator.scss: -------------------------------------------------------------------------------- 1 | .truncator { 2 | // content is clamped to 3 lines unless expanded 3 | .content { 4 | overflow: hidden; 5 | display: -webkit-box; 6 | text-overflow: ellipsis; 7 | -webkit-box-orient: vertical; 8 | -webkit-line-clamp: 3; 9 | } 10 | 11 | &.expanded .content { 12 | display: initial; 13 | } 14 | 15 | // safari truncation fix 16 | .content > * { 17 | margin: 0; 18 | } 19 | 20 | // expand/collapse toggle is only shown if content is truncated 21 | button { 22 | display: none; 23 | } 24 | 25 | &.truncated button { 26 | display: inline-block; 27 | } 28 | 29 | // "view more"/"view less" text swaps when content is expanded 30 | .view-more { 31 | color: shade-color($link-color, 20%); 32 | display: inline; 33 | } 34 | 35 | .view-less { 36 | color: shade-color($link-color, 20%); 37 | display: none; 38 | } 39 | 40 | &.expanded { 41 | .view-more { 42 | display: none; 43 | } 44 | 45 | .view-less { 46 | display: inline; 47 | } 48 | } 49 | 50 | // icon rotates when content is expanded 51 | .icon::before { 52 | content: "▶"; 53 | } 54 | 55 | &.expanded .icon::before { 56 | content: "▼"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/responsive.scss: -------------------------------------------------------------------------------- 1 | .my-w-25 { 2 | width: 25% !important; 3 | } 4 | 5 | .my-w-75 { 6 | width: 75% !important; 7 | } 8 | 9 | .w-md-100 { 10 | @media (max-width: 767px) { 11 | width: 100% !important; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/arclight/variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $online-icon-color: $green !default; 3 | $background-highlight: #ffffaa !default; 4 | $default-border-color: $border-color !default; 5 | $light-icon-color: #f5f5f5 !default; 6 | $dark-icon-color: $secondary !default; 7 | 8 | // Spacing 9 | $result-item-body-padding: 12px !default; 10 | 11 | // Mixins 12 | $default-border-styling: 1px solid $default-border-color !default; 13 | 14 | @mixin extent-badge { 15 | background-color: $gray-200; 16 | border: $default-border-styling; 17 | border-radius: $border-radius-lg; 18 | color: $dark; 19 | font-size: $font-size-sm; 20 | line-height: 1.2; 21 | margin: 0; 22 | padding: ($spacer * .25) ($spacer * .5); 23 | text-align: left; 24 | white-space: normal; 25 | } 26 | -------------------------------------------------------------------------------- /app/components/arclight/access_component.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% access_content = capture do %> 3 | <%= render Arclight::MetadataSectionComponent.with_collection(section_names, 4 | metadata_attr: { layout: Arclight::UpperMetadataLayoutComponent }, 5 | presenter: presenter) %> 6 | <% end %> 7 | 8 | <% if access_content.present? %> 9 |
10 |

<%= t 'arclight.views.show.sections.access_field' %>

11 | 12 | <%= access_content%> 13 |
14 | <% end %> -------------------------------------------------------------------------------- /app/components/arclight/access_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render access information for a document 5 | class AccessComponent < ViewComponent::Base 6 | def initialize(presenter:) 7 | super 8 | @show_config = presenter.configuration.show 9 | @presenter = presenter 10 | end 11 | 12 | attr_reader :presenter, :show_config 13 | 14 | delegate :collection?, to: :presenter 15 | 16 | # @return Array a list of metadata section names 17 | def section_names 18 | return Array(collection_access_items) if collection? 19 | 20 | Array(component_access_items) 21 | end 22 | 23 | delegate :collection_access_items, :component_access_items, to: :show_config 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/components/arclight/bookmark_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_or_guest_user %> 2 | <%= form_tag(bookmark_path, 3 | authenticity_token: false, 4 | method: bookmarked? ? :delete : :put, 5 | class: "bookmark-toggle ms-2", 6 | data: { 7 | 'doc-id' => document.id, 8 | present: "#{blacklight_icon(:bookmark).html_safe}#{t('blacklight.search.bookmarks.present')}", 9 | absent: "#{blacklight_icon(:bookmark).html_safe}#{t('blacklight.search.bookmarks.absent')}", 10 | inprogress: t('blacklight.search.bookmarks.inprogress') 11 | }) do %> 12 |
13 | 17 |
18 | 19 | <%= submit_tag(t(bookmarked? ? 'remove.button' : 'add.button', scope: 'blacklight.bookmarks'), 20 | id: "bookmark_toggle_#{document.id.to_s.parameterize}", 21 | class: "bookmark-#{bookmarked? ? 'remove' : 'add'} btn btn-outline-secondary") %> 22 | <% end %> 23 | <% else %> 24 |   25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/components/arclight/bookmark_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # A component to render a bookmark button for a document 5 | class BookmarkComponent < Blacklight::Document::BookmarkComponent 6 | delegate :current_or_guest_user, :blacklight_icon, to: :helpers 7 | attr_accessor :document 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/components/arclight/breadcrumb_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Display the document hierarchy as "breadcrumbs" 5 | class BreadcrumbComponent < ViewComponent::Base 6 | # @param [Integer] count if provided the number of bookmarks is limited to this number 7 | def initialize(document:, count: nil, offset: 0) 8 | @document = document 9 | @count = count 10 | @offset = offset 11 | super 12 | end 13 | 14 | def call 15 | breadcrumb_links = components.drop(@offset) 16 | 17 | if @count && breadcrumb_links.length > @count 18 | breadcrumb_links = breadcrumb_links.first(@count) 19 | breadcrumb_links << tag.li('…'.html_safe, class: 'breadcrumb-item') 20 | end 21 | tag.ol class: 'breadcrumb' do 22 | safe_join(breadcrumb_links) 23 | end 24 | end 25 | 26 | def components 27 | return to_enum(:components) unless block_given? 28 | 29 | yield build_repository_link 30 | 31 | @document.parents.each do |parent| 32 | yield tag.li(class: 'breadcrumb-item') { link_to(parent.label, solr_document_path(parent.id)) } 33 | end 34 | end 35 | 36 | def build_repository_link 37 | render Arclight::RepositoryBreadcrumbComponent.new(document: @document) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/components/arclight/breadcrumbs_hierarchy_component.html.erb: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /app/components/arclight/breadcrumbs_hierarchy_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render the hierarchy for a document 5 | class BreadcrumbsHierarchyComponent < ViewComponent::Base 6 | delegate :document, to: :@presenter 7 | delegate :blacklight_icon, to: :helpers 8 | 9 | def initialize(presenter:) 10 | super 11 | 12 | @presenter = presenter 13 | collections, @parents_under_collection = document.parents.partition(&:collection?) 14 | @collection = collections.first 15 | end 16 | 17 | attr_reader :collection, :parents_under_collection 18 | 19 | def repository 20 | return tag.span(t('arclight.show_breadcrumb_label')) if document.repository_config.blank? 21 | 22 | link_to(document.repository_config.name, helpers.arclight_engine.repository_path(document.repository_config.slug)) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/components/arclight/collection_context_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Collection

5 |

<%= title %>

6 | 7 |
8 | <%= document_download %> 9 | <%= collection_info %> 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /app/components/arclight/collection_context_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render various actions for a collection (e.g. requesting, download links, etc) 5 | class CollectionContextComponent < ViewComponent::Base 6 | def initialize(presenter:, download_component:) 7 | super 8 | 9 | @collection = presenter.document.collection 10 | @download_component = download_component 11 | end 12 | 13 | attr_reader :collection 14 | 15 | def title 16 | collection.normalized_title 17 | end 18 | 19 | def document_download 20 | render @download_component.new(downloads: collection.downloads) || Arclight::DocumentDownloadComponent.new(downloads: collection.downloads) 21 | end 22 | 23 | def collection_info 24 | render Arclight::CollectionInfoComponent.new(collection: collection) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/components/arclight/collection_info_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 28 |
29 | -------------------------------------------------------------------------------- /app/components/arclight/collection_info_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render information about the collection 5 | class CollectionInfoComponent < ViewComponent::Base 6 | def initialize(collection:) 7 | super 8 | 9 | @collection = collection 10 | end 11 | 12 | attr_reader :collection 13 | 14 | delegate :total_component_count, :online_item_count, :last_indexed, :collection_unitid, to: :collection 15 | delegate :blacklight_icon, to: :helpers 16 | 17 | def info_icon 18 | icon = <<~SVG 19 | 20 | 21 | 22 | SVG 23 | icon.html_safe # rubocop:disable Rails/OutputSafety 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/components/arclight/collection_sidebar_component.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/arclight/collection_sidebar_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Draw the links to the collection info in the sidebar 5 | class CollectionSidebarComponent < ViewComponent::Base 6 | def initialize(document:, partials:, collection_presenter:) 7 | super 8 | 9 | @document = document 10 | @partials = Array(partials) 11 | @collection_presenter = collection_presenter 12 | end 13 | 14 | attr_reader :document, :partials, :collection_presenter 15 | 16 | def has_section?(section) 17 | # Access field data comes from repositories.yml not from solr, so handle it in a different way. 18 | return true if section == :access_field 19 | 20 | collection_presenter.with_field_group(section).fields_to_render.any? 21 | end 22 | 23 | def document_section_path(section) 24 | [document_path, section_anchor(section)].join 25 | end 26 | 27 | def section_label(section) 28 | t("arclight.views.show.sections.#{section}") 29 | end 30 | 31 | def document_path 32 | @document_path ||= solr_document_path(document.root) 33 | end 34 | 35 | def section_anchor(section) 36 | "##{t("arclight.views.show.sections.#{section}").parameterize}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/components/arclight/constraints_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if repository %> 2 |
3 | <%= render repository %> 4 |
5 | <% end %> 6 | 7 | <%= render Blacklight::ConstraintsComponent.new(**@kwargs) %> 8 | -------------------------------------------------------------------------------- /app/components/arclight/constraints_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Extend the upstream constraints with breadcrumbs and 5 | # repository context information 6 | class ConstraintsComponent < Blacklight::ConstraintsComponent 7 | def initialize(**kwargs) 8 | super 9 | 10 | @kwargs = kwargs 11 | end 12 | 13 | def repository 14 | @repository ||= helpers.repository_faceted_on 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/components/arclight/document_collection_context_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.tr( 2 | id: @document.id, 3 | data: { 4 | 'document-id': @document.id.to_s.parameterize, 5 | 'document-counter': @counter, 6 | }, 7 | itemscope: true, 8 | itemtype: @document.itemtype, 9 | class: classes.join(' ')) do %> 10 | 11 |
12 | <%= helpers.link_to_document document, counter: @counter %> 13 | <% if document.children? %> 14 | <%= document.number_of_children %><%= t(:'arclight.views.index.number_of_components', count: document.number_of_children) %> 15 | <% end %> 16 |
17 | 18 | 19 | <%= tag.td class: 'text-muted' do %> 20 | <%= document.containers.join(', ') %> 21 | <% end %> 22 | 23 | <%= tag.td class: 'text-end' do %> 24 | <%= helpers.render_index_doc_actions document, wrapping_class: 'd-inline-flex justify-content-end' %> 25 | <% end %> 26 | 27 | 28 | <%= render Arclight::IndexMetadataFieldComponent.with_collection(@presenter&.field_presenters.select { |field| field.field_config.collection_context }, classes: ['col pl-0 my-0']) %> 29 | <% end %> 30 | -------------------------------------------------------------------------------- /app/components/arclight/document_collection_context_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Display a single document in the collection 5 | class DocumentCollectionContextComponent < Arclight::SearchResultComponent 6 | # @param [SolrDocument] document 7 | def initialize(document: nil, blacklight_config: nil, **kwargs) 8 | super(document: document, **kwargs) 9 | @blacklight_config = blacklight_config 10 | end 11 | 12 | attr_reader :blacklight_config 13 | 14 | def classes 15 | (super - ['row'] + ['al-collection-context']).flatten 16 | end 17 | 18 | private 19 | 20 | def online_status 21 | render online_status_component.new(document: @document) 22 | end 23 | 24 | def online_status_component 25 | blacklight_config.show.online_status_component || Arclight::OnlineStatusIndicatorComponent 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/components/arclight/document_collection_hierarchy_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag :li, 2 | id: "#{@document.id}-hierarchy-item", 3 | data: { 4 | 'document-id': @document.id.to_s.parameterize, 5 | 'document-counter': @counter, 6 | }, 7 | itemscope: true, 8 | itemtype: @document.itemtype, 9 | class: (classes.flatten + ['al-collection-context', ('al-hierarchy-highlight' if current_target?)].compact).join(' ') do %> 10 |
11 | <%= render(Arclight::HierarchyToggleComponent.new(document: document, expanded: show_expanded?)) %> 12 |
13 | <% if current_target? %> 14 | <%= document.normalized_title %> 15 | <% else %> 16 | <%= helpers.link_to_document document, counter: @counter %> 17 | <% end %> 18 | <% if document.children? %> 19 | <%= document.number_of_children %><%= t(:'arclight.views.index.number_of_components', count: document.number_of_children) %> 20 | <% end %> 21 | <%= online_status %> 22 |
23 | <%= content_tag('div', class: 'al-document-container text-muted') do %> 24 | <%= document.containers.join(', ') %> 25 | <% end if document.containers.present? %> 26 |
27 | 28 | <%= render Arclight::IndexMetadataFieldComponent.with_collection(@presenter&.field_presenters.select { |field| field.field_config.collection_context }, classes: ['col pl-0 my-0']) %> 29 | <% if document.number_of_children > 0 %> 30 | <%= content_tag(:div, id: "collapsible-hierarchy-#{document.id}", 31 | class: "collapse al-collection-context-collapsible al-hierarchy-level-#{document.component_level} #{'show' if show_expanded?}" 32 | ) do %> 33 | <%= render Arclight::DocumentComponentsHierarchyComponent.new(document: @document, target_index: target_index) %> 34 | <% end %> 35 | <% end %> 36 | <% end %> 37 | -------------------------------------------------------------------------------- /app/components/arclight/document_collection_hierarchy_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Display a single document in the collection hierarchy 5 | class DocumentCollectionHierarchyComponent < Arclight::SearchResultComponent 6 | # @param [Blacklight::DocumentPresenter] document 7 | # @param [String] nest_path determines which element to highlight 8 | def initialize(document: nil, nest_path: nil, blacklight_config: nil, **kwargs) 9 | super(document: document, **kwargs) 10 | @nest_path = nest_path 11 | @blacklight_config = blacklight_config 12 | end 13 | 14 | attr_reader :blacklight_config 15 | 16 | def classes 17 | super - ['row'] 18 | end 19 | 20 | # we want to eager-load this document's children if we're in the 21 | # target document's component hierarchy 22 | def show_expanded? 23 | within_original_tree? 24 | end 25 | 26 | # Solr nest paths are constructed using the path name and an index, e.g. 27 | # `/components#5/components#3`. 28 | # 29 | # @return [String] the targeted index for this level of the hierarchy 30 | def target_index 31 | return -1 unless within_original_tree? 32 | 33 | remaining_path = nest_path.sub("#{@document.nest_path}/", '') 34 | current_component, _rest = remaining_path.split('/', 2) 35 | _name, index = current_component.split('#', 2) 36 | 37 | index&.to_i 38 | end 39 | 40 | private 41 | 42 | # We're in the targeted document's original tree if the target nest path 43 | # includes this document's path. 44 | def within_original_tree? 45 | nest_path&.start_with? "#{@document.nest_path}/" 46 | end 47 | 48 | def current_target? 49 | nest_path == @document.nest_path 50 | end 51 | 52 | def online_status 53 | render online_status_component.new(document: @document) 54 | end 55 | 56 | def online_status_component 57 | blacklight_config.show.online_status_component || Arclight::OnlineStatusIndicatorComponent 58 | end 59 | 60 | attr_reader :nest_path 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/components/arclight/document_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 |
8 | 9 |
10 | <%= content_tag :h1 do %> 11 | <%= document.normalized_title %> 12 | <% end %> 13 | <%= render 'arclight/requests', document: document %> 14 | <%= render Arclight::BookmarkComponent.new document: document, action: bookmark_config %> 15 | <%= toggle_sidebar %> 16 | <%= online_filter %> 17 |
18 | 19 | <%= render Arclight::MetadataSectionComponent.with_collection(component_metadata_partials, 20 | metadata_attr: { layout: Arclight::UpperMetadataLayoutComponent }, 21 | presenter: presenter) unless document.collection? %> 22 | 23 | <%= embed %> 24 | 25 | <% if document.collection? %> 26 |
27 |

<%= t 'arclight.views.show.context' %>

28 | <%= render Arclight::MetadataSectionComponent.with_collection(metadata_partials, 29 | metadata_attr: { layout: Arclight::UpperMetadataLayoutComponent }, 30 | presenter: presenter, heading: true) %> 31 |
32 | <% elsif document.children? %> 33 |
34 |

<%= t 'arclight.views.show.contents' %>

35 | <%= helpers.turbo_frame_tag "al-hierarchy-#{document.id}-document", loading: 'lazy', src: helpers.hierarchy_solr_document_path(id: document.id, paginate: true, key: '-document', per_page: 50) %> 36 |
37 | <% end %> 38 | 39 | <%= access %> 40 | -------------------------------------------------------------------------------- /app/components/arclight/document_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a single document 5 | class DocumentComponent < Blacklight::DocumentComponent 6 | attr_reader :document 7 | 8 | def online_content? 9 | document.online_content? && (document.collection? || document.children?) 10 | end 11 | 12 | def blacklight_config 13 | presenter.configuration 14 | end 15 | 16 | # @return [Blacklight::Configuration::ToolConfig] the configuration for the bookmark 17 | def bookmark_config 18 | blacklight_config.index.document_actions.arclight_bookmark_control 19 | end 20 | 21 | def breadcrumb_component 22 | blacklight_config.show.breadcrumb_component || Arclight::BreadcrumbsHierarchyComponent 23 | end 24 | 25 | def metadata_partials 26 | blacklight_config.show.metadata_partials || [] 27 | end 28 | 29 | def component_metadata_partials 30 | blacklight_config.show.component_metadata_partials || [] 31 | end 32 | 33 | def online_filter 34 | render Arclight::OnlineContentFilterComponent.new(document: document) 35 | end 36 | 37 | def access 38 | render (blacklight_config.show.access_component || Arclight::AccessComponent).new(presenter: presenter) 39 | end 40 | 41 | def toggle_sidebar 42 | button_tag(t('arclight.views.show.toggle_sidebar'), 43 | type: :button, 44 | class: 'btn btn-sm btn-secondary d-lg-none sidebar-toggle', 45 | data: { bs_toggle: 'offcanvas', bs_target: '#sidebar' }, 46 | aria: { controls: 'sidebar' }) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/components/arclight/document_components_hierarchy_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if paginate? && @target_index >= 0 && @target_index >= @maximum_left_gap %> 2 | <%# render the hierarchy as: outer (left) window ... window ... because our current document hierarchy is so far down the list it'd be buried %> 3 | <%= helpers.turbo_frame_tag "al-hierarchy-#{@document.id}-left", loading: 'lazy', src: hierarchy_path(limit: @left_outer_window, key: '-left') %> 4 | <%= tag.turbo_frame id: "al-hierarchy-#{@document.id}-gap" do %> 5 | <%= render expand_hierarchy_component.new(path: hierarchy_path(offset: @left_outer_window, limit: @target_index - @left_outer_window - (@window / 2), key: '-gap')) %> 6 | <% end %> 7 | <%= helpers.turbo_frame_tag "al-hierarchy-#{@document.id}-window", src: hierarchy_path(offset: @target_index - (@window / 2), limit: @window, key: '-window') %> 8 | <%= tag.turbo_frame id: "al-hierarchy-#{@document.id}-right" do %> 9 | <%= render expand_hierarchy_component.new(path: hierarchy_path(offset: @target_index + (@window / 2), key: '-right')) %> 10 | <% end %> 11 | <% elsif paginate? %> 12 | <%# render the first N documents, and let the user expand the remaining if desired %> 13 | <%= helpers.turbo_frame_tag "al-hierarchy-#{@document.id}-sidebar", loading: ('lazy' unless @target_index >= 0), src: hierarchy_path(limit: @maximum_left_gap, key: '-sidebar') %> 14 | <%= tag.turbo_frame id: "al-hierarchy-#{@document.id}-right" do %> 15 | <%= render expand_hierarchy_component.new(path: hierarchy_path(offset: @maximum_left_gap, key: '-right')) %> 16 | <% end %> 17 | <% else %> 18 | <%# there aren't enough to bother paginating, so load them all at once %> 19 | <%= helpers.turbo_frame_tag "al-hierarchy-#{@document.id}-sidebar", loading: ('lazy' unless @target_index >= 0), src: hierarchy_path(key: '-sidebar') %> 20 | <% end %> 21 | -------------------------------------------------------------------------------- /app/components/arclight/document_components_hierarchy_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Display a document's constituent components with appropriate lazy-loading 5 | # to keep the page load time reasonable. 6 | class DocumentComponentsHierarchyComponent < ViewComponent::Base 7 | # rubocop:disable Metrics/ParameterLists 8 | def initialize(document: nil, target_index: -1, minimum_pagination_size: 20, left_outer_window: 3, maximum_left_gap: 10, window: 10) 9 | super 10 | 11 | @document = document 12 | @target_index = target_index&.to_i || -1 13 | @minimum_pagination_size = minimum_pagination_size 14 | @left_outer_window = left_outer_window 15 | @maximum_left_gap = maximum_left_gap 16 | @window = window 17 | end 18 | # rubocop:enable Metrics/ParameterLists 19 | 20 | def paginate? 21 | @document.number_of_children > @minimum_pagination_size 22 | end 23 | 24 | def hierarchy_path(**kwargs) 25 | helpers.hierarchy_solr_document_path(id: @document.id, hierarchy: true, nest_path: params[:nest_path], **kwargs) 26 | end 27 | 28 | def expand_hierarchy_component 29 | blacklight_config.show.expand_hierarchy_component || Arclight::ExpandHierarchyButtonComponent 30 | end 31 | 32 | delegate :blacklight_config, to: :helpers 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/components/arclight/document_download_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% case files.size %> 3 | <% when 2 %> 4 | 17 | <% when 1 %> 18 | <% files.each do |file| %> 19 | <%= link_to(file.href, class: "btn btn-secondary btn-sm", **@link_options) do %> 20 | <%= download_icon %> <%= label(file) %> 21 | <% end %> 22 | <% end %> 23 | <% end %> 24 |
25 | -------------------------------------------------------------------------------- /app/components/arclight/embed_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('arclight.views.show.embedded_content') %>

3 | <%= render Arclight::OembedViewerComponent.with_collection(embeddable_resources, document: @document) %> 4 | 5 | <% linked_resources.each do |resource| %> 6 |
7 | <%= link_to(resource.label, resource.href) %> 8 |
9 | <% end %> 10 |
11 | -------------------------------------------------------------------------------- /app/components/arclight/embed_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render digital object links for a document 5 | class EmbedComponent < ViewComponent::Base 6 | def initialize(document:, presenter:, **kwargs) 7 | super 8 | 9 | @document = document 10 | @presenter = presenter 11 | end 12 | 13 | def render? 14 | resources.any? 15 | end 16 | 17 | def embeddable_resources 18 | resources.first(1).select { |object| embeddable?(object) } 19 | end 20 | 21 | def linked_resources 22 | resources - embeddable_resources 23 | end 24 | 25 | def resources 26 | @resources ||= @document.digital_objects || [] 27 | end 28 | 29 | def depth 30 | @document.parents.length || 0 31 | end 32 | 33 | def embeddable?(object) 34 | exclude_patterns.none? do |pattern| 35 | object.href =~ pattern 36 | end 37 | end 38 | 39 | def exclude_patterns 40 | Arclight::Engine.config.oembed_resource_exclude_patterns 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/components/arclight/expand_hierarchy_button_component.html.erb: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/components/arclight/expand_hierarchy_button_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Component for rendering an expand button inside the hierarchy view 5 | class ExpandHierarchyButtonComponent < Blacklight::Component 6 | def initialize(path:, classes: 'btn btn-secondary btn-sm') 7 | super 8 | @path = path 9 | @classes = classes 10 | end 11 | 12 | def expand_link 13 | link_to t('arclight.views.show.expand'), @path, class: @classes 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/components/arclight/group_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <% if document.repository_config.present? %> 5 | 8 | <% end %> 9 |

<%= helpers.link_to_document document %>

10 | <% document.extent.each do |extent| %> 11 | <%= tag.span extent, class: 'al-document-extent badge' unless compact? %> 12 | <% end %> 13 |
14 | <%= render Arclight::IndexMetadataFieldComponent.with_collection(presenter.field_presenters.select { |field| !compact? || field.field_config.compact }) %> 15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 | <% if @group.total > 3 %> 23 | <%= t('arclight.views.index.top_group_results', count: 3) %> 24 | <%= link_to( 25 | t('arclight.views.index.all_group_results', count: @group.total), 26 | search_within_collection_url) 27 | %> 28 | <% else %> 29 | <%= t('arclight.views.index.group_results_count', count: @group.total) %> 30 | <% end %> 31 |
32 | 33 | <%= helpers.render_document_index @group.docs %> 34 |
35 | -------------------------------------------------------------------------------- /app/components/arclight/group_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a collection document for a 5 | # grouped search result view 6 | class GroupComponent < Blacklight::Document::GroupComponent 7 | def compact? 8 | helpers.document_index_view_type.to_s == 'compact' 9 | end 10 | 11 | def document 12 | @document ||= @group.docs.first.collection 13 | end 14 | 15 | def presenter 16 | @presenter ||= Arclight::ShowPresenter.new(document, helpers).with_field_group('group_header_field') 17 | end 18 | 19 | def search_within_collection_url 20 | search_catalog_path(helpers.search_without_group.deep_merge(f: { collection: [document.collection_name] })) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/components/arclight/header_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= top_bar %> 3 | <%= masthead %> 4 | <%= search_bar %> 5 |
6 | -------------------------------------------------------------------------------- /app/components/arclight/header_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Provides a header with a masthead 5 | class HeaderComponent < Blacklight::HeaderComponent 6 | def masthead 7 | render Arclight::MastheadComponent.new 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/arclight/hierarchy_toggle_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to( 2 | "#collapsible-hierarchy-#{document.id}", 3 | class: "al-toggle-view-children#{' collapsed' unless expanded }", 4 | aria: { 5 | label: t('arclight.hierarchy.view_all'), 6 | expanded: expanded 7 | }, 8 | data: { 9 | bs_toggle: 'collapse', 10 | toggle: 'collapse' 11 | } 12 | ) do %> 13 | 17 | <% end %> -------------------------------------------------------------------------------- /app/components/arclight/hierarchy_toggle_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Component for rendering the plus/minus icons at each level of expandable hierarchy components 5 | class HierarchyToggleComponent < ViewComponent::Base 6 | attr_reader :document, :expanded 7 | 8 | def initialize(document:, expanded:) 9 | @document = document 10 | @expanded = expanded 11 | super 12 | end 13 | 14 | delegate :blacklight_icon, to: :helpers 15 | 16 | def render? 17 | @document.children? 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/components/arclight/index_metadata_field_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= tag.dt @field.label, class: 'visually-hidden' %> 3 | <% if truncate? %> 4 | <%= tag.dd(class: @classes + ['truncator'], data: { controller: 'arclight-truncate' }) do %> 5 | <%= tag.div @field.render, class: 'content', data: { arclight_truncate_target: 'content' } %> 6 | <%= button_tag(type: :button, class: 'btn btn-sm btn-link px-0', 7 | data: { action: 'click->arclight-truncate#trigger' }) do %> 8 | <%= tag.span t('arclight.truncation.view_more'), class: 'view-more' %> 9 | <%= tag.span t('arclight.truncation.view_less'), class: 'view-less' %> 10 | <%= tag.span(class: 'icon') %> 11 | <% end %> 12 | <% end %> 13 | <% else %> 14 | <%= tag.dd @field.render, class: @classes %> 15 | <% end %> 16 |
17 | -------------------------------------------------------------------------------- /app/components/arclight/index_metadata_field_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a simple metadata field (e.g. without labels) in a .row div 5 | class IndexMetadataFieldComponent < Blacklight::MetadataFieldComponent 6 | def initialize(field:, classes: ['col'], **kwargs) 7 | super(field: field, **kwargs) 8 | 9 | @classes = classes + ["al-document-#{@field.key.dasherize}"] 10 | end 11 | 12 | def render? 13 | helpers.document_index_view_type != :compact || @field.field_config.compact 14 | end 15 | 16 | def truncate? 17 | !!@field.field_config.truncate 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/components/arclight/masthead_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
<%= heading %>
6 |
7 | 8 |
9 | 14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /app/components/arclight/masthead_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render the masthead 5 | class MastheadComponent < Blacklight::Component 6 | def heading 7 | t('arclight.masthead_heading_html') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/arclight/metadata_section_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.div id: t("arclight.views.show.sections.#{@section}").parameterize do %> 2 | <% if @heading %> 3 |

4 | <%= t("arclight.views.show.sections.#{@section}", level: @presenter.document.level) %> 5 |

6 | <% end %> 7 | 8 | <%= tag.dl class: @classes do %> 9 | <% @presenter.field_presenters.each do |field_presenter| %> 10 | <%= render (field_presenter.component || Blacklight::MetadataFieldComponent).new(field: field_presenter, show: true, **@metadata_attr) %> 11 | <% end %> 12 | <% end %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/components/arclight/metadata_section_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a simple metadata field (e.g. without labels) in a .row div 5 | class MetadataSectionComponent < ViewComponent::Base 6 | with_collection_parameter :section 7 | 8 | def initialize(section:, presenter:, metadata_attr: {}, classes: %w[row dl-invert], heading: false) 9 | super 10 | 11 | @classes = classes 12 | @section = section 13 | @presenter = presenter.with_field_group(section) 14 | @heading = heading 15 | @metadata_attr = metadata_attr 16 | end 17 | 18 | def render? 19 | @presenter.fields_to_render.any? 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/components/arclight/oembed_viewer_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.div(class: 'al-oembed-viewer', data: { controller: 'arclight-oembed', arclight_oembed_url_value: @resource.href }) do %> 2 |
3 | <%= link_to(@resource.label, @resource.href) %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/components/arclight/oembed_viewer_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render an oembed viewer for a document 5 | class OembedViewerComponent < ViewComponent::Base 6 | with_collection_parameter :resource 7 | 8 | def initialize(resource:, document:, depth: 0) 9 | super 10 | 11 | @resource = resource 12 | @document = document 13 | @depth = depth 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/components/arclight/online_content_filter_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

6 | Filter 7 | <%= t('arclight.views.show.online_content.title') %> 8 |

9 |
10 |
11 |
<%= t('arclight.views.show.online_content.description') %>
12 | <%= link_to t('arclight.views.show.online_content.link_text'), helpers.search_action_path(f: { collection: [collection_name], access: ['online'] }) %> 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/components/arclight/online_content_filter_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render the box that displays a link to filter only for online content 5 | class OnlineContentFilterComponent < Blacklight::Component 6 | def initialize(document:) 7 | @document = document 8 | super 9 | end 10 | 11 | def render? 12 | @document.collection? && @document.online_content? 13 | end 14 | 15 | delegate :collection_name, to: :@document 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/components/arclight/online_status_indicator_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render an online status indicator for a document 5 | class OnlineStatusIndicatorComponent < Blacklight::Component 6 | def initialize(document:, **) 7 | @document = document 8 | super 9 | end 10 | 11 | def render? 12 | @document.online_content? 13 | end 14 | 15 | def call 16 | tag.span helpers.blacklight_icon(:online), class: 'al-online-content-icon' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/components/arclight/repository_breadcrumb_component.html.erb: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/components/arclight/repository_breadcrumb_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Draws the repository breadcrumb item for a search result 5 | class RepositoryBreadcrumbComponent < ViewComponent::Base 6 | def initialize(document:) 7 | super 8 | @document = document 9 | end 10 | 11 | delegate :blacklight_icon, to: :helpers 12 | 13 | def repository_path 14 | @document.repository_config&.slug 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/components/arclight/repository_location_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render(@layout.new(field: @field)) do |component| %> 2 | <% component.with_label do %> 3 | <%= label %> 4 | <% end %> 5 | <% component.with_value do %> 6 |
7 | <% if repository.thumbnail_url %> 8 | <%= image_tag repository.thumbnail_url, alt: '', class: 'img-fluid float-left' %> 9 | <% end %> 10 |
11 | <%= link_to(repository.name, helpers.arclight_engine.repository_path(repository.slug)) %> 12 |
13 |
14 |
15 |
16 | <%= repository.location %> 17 |
18 |
19 | <% end %> 20 | <% end %> 21 | -------------------------------------------------------------------------------- /app/components/arclight/repository_location_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render the repository location card as a metadata field 5 | class RepositoryLocationComponent < Blacklight::MetadataFieldComponent 6 | def repository 7 | @field.values.first 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/arclight/search_bar_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= render(Blacklight::SearchBarComponent.new( 2 | **@kwargs, 3 | params: @params.merge(f: (@params[:f] || {}).except(:collection)), 4 | q: @q, 5 | search_field: @search_field)) do |c| %> 6 | 7 | <% c.with_before_input_group do %> 8 |
9 | 12 | <%= select_tag ('f[collection][]' if collection_name.present?), within_collection_options, id: 'within_collection', class: 'form-select search-field rounded-end' %> 13 |
14 | <% end %> 15 | 16 | <% c.with_prepend do %> 17 | <%= prepend %> 18 | <% end %> 19 | 20 | <% c.with_append do %> 21 | <%= append %> 22 | <% end if append? %> 23 | 24 | <% c.with_search_button do %> 25 | <%= search_button %> 26 | <% end if respond_to?(:search_button?) && search_button? # only BL8 has this option %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/components/arclight/search_bar_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Override Blacklight's SearchBarComponent to add a dropdown for choosing 5 | # the context of the search (within "this collection" or "all collections"). 6 | # If a collection has not been chosen, it displays a dropdown with only "all collections" 7 | # as the only selectable option. 8 | class SearchBarComponent < Blacklight::SearchBarComponent 9 | def initialize(**kwargs) 10 | super 11 | 12 | @kwargs = kwargs 13 | end 14 | 15 | def within_collection_options 16 | value = collection_name || 'none-selected' 17 | options_for_select( 18 | [ 19 | [t('arclight.within_collection_dropdown.all_collections'), ''], 20 | [t('arclight.within_collection_dropdown.this_collection'), value] 21 | ], 22 | selected: collection_name, 23 | disabled: 'none-selected' 24 | ) 25 | end 26 | 27 | def collection_name 28 | @collection_name ||= Array(@params.dig(:f, :collection)).reject(&:empty?).first || 29 | helpers.current_context_document&.collection_name 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_breadcrumbs_component.html.erb: -------------------------------------------------------------------------------- 1 |
Collection Context
2 |
3 | 6 |
7 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_breadcrumbs_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render the breadcrumbs for a search result document 5 | class SearchResultBreadcrumbsComponent < Blacklight::MetadataFieldComponent 6 | delegate :document, to: :@field 7 | 8 | def initialize(field:, **kwargs) 9 | @field = field 10 | super 11 | end 12 | 13 | def breadcrumbs 14 | offset = grouped? ? 2 : 0 15 | 16 | Arclight::BreadcrumbComponent.new(document: document, count: breadcrumb_count, offset: offset) 17 | end 18 | 19 | def rendered_breadcrumbs 20 | @rendered_breadcrumbs ||= capture { render breadcrumbs } 21 | end 22 | 23 | def render? 24 | rendered_breadcrumbs.present? 25 | end 26 | 27 | def breadcrumb_count 28 | @field.field_config.compact&.dig(:count) if compact? 29 | end 30 | 31 | delegate :grouped?, to: :helpers 32 | 33 | def compact? 34 | helpers.document_index_view_type == :compact 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_component.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag @component, 2 | id: @id, 3 | data: { 4 | 'document-id': @document.id.to_s.parameterize, 5 | 'document-counter': @counter, 6 | }, 7 | itemscope: true, 8 | itemtype: @document.itemtype, 9 | class: classes.flatten.join(' ') do %> 10 |
11 | <%= icon %> 12 |
13 |
14 | <%= render Arclight::SearchResultTitleComponent.new(document: document, compact: compact?) %> 15 | 16 |
17 | <%= metadata %> 18 |
19 |
20 | <% end %> 21 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a document for a search result; this works with 5 | # both the compact and list views for grouped or ungrouped 6 | # results. 7 | class SearchResultComponent < Blacklight::DocumentComponent 8 | attr_reader :document 9 | 10 | def compact? 11 | presenter.view_config.key.to_s == 'compact' 12 | end 13 | 14 | def icon 15 | helpers.blacklight_icon helpers.document_or_parent_icon(@document) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_title_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= helpers.link_to_document @document, counter: @counter %> 4 | <% @document.extent.each do |extent| %> 5 | <%= tag.span extent, class: 'al-document-extent badge' unless compact? %> 6 | <% end %> 7 | <%= tag.span class: 'al-document-container text-muted' do %> 8 | <%= @document.containers.join(', ') %> 9 | <% end if @document.containers.present? %> 10 |

11 | 12 | <% actions.each do |action| %> 13 | <%= action %> 14 | <% end %> 15 |
16 | -------------------------------------------------------------------------------- /app/components/arclight/search_result_title_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Render a document title for a search result 5 | class SearchResultTitleComponent < Blacklight::DocumentTitleComponent 6 | def initialize(compact:, **args) 7 | @compact = compact 8 | super(**args) 9 | end 10 | 11 | def compact? 12 | @compact 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/components/arclight/sidebar_component.html.erb: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/components/arclight/sidebar_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # A sidebar with collection context widget and tools 5 | class SidebarComponent < Blacklight::Document::SidebarComponent 6 | delegate :blacklight_config, :document_presenter, :should_render_field?, 7 | :turbo_frame_tag, to: :helpers 8 | 9 | def collection_context 10 | render Arclight::CollectionContextComponent.new(presenter: document_presenter(document), download_component: Arclight::DocumentDownloadComponent) 11 | end 12 | 13 | def collection_sidebar 14 | render Arclight::CollectionSidebarComponent.new(document: document, 15 | collection_presenter: document_presenter(document.collection), 16 | partials: blacklight_config.show.metadata_partials) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/components/arclight/upper_metadata_layout_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Override upstream to add an offset bootstrap column class 5 | class UpperMetadataLayoutComponent < Blacklight::MetadataFieldLayoutComponent 6 | def initialize(field:, label_class: 'col-md-3 offset-md-1', value_class: 'col-md-8') 7 | super 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/bookmark_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The bookmark icon 6 | class BookmarkComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/collection_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The collection icon 6 | class CollectionComponent < Blacklight::Icons::IconComponent 7 | # Used unmodified from https://fontawesome.com 8 | # CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 9 | self.svg = <<~SVG 10 | 11 | SVG 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/compact_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The compact icon 6 | class CompactComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/container_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The container icon 6 | class ContainerComponent < Blacklight::Icons::IconComponent 7 | # Used unmodified from https://fontawesome.com 8 | # CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 9 | self.svg = <<~SVG 10 | 11 | SVG 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/ead_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The ead icon 6 | class EadComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/file_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The file icon 6 | class FileComponent < Blacklight::Icons::IconComponent 7 | # Used unmodified from https://fontawesome.com 8 | # CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 9 | self.svg = <<~SVG 10 | 11 | SVG 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/folder_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The folder icon 6 | class FolderComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/minus_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The minus icon 6 | class MinusComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/online_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The online icon 6 | class OnlineComponent < Blacklight::Icons::IconComponent 7 | # Used unmodified from https://fontawesome.com 8 | # CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 9 | self.svg = <<~SVG 10 | 11 | 12 | SVG 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/pdf_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The pdf icon 6 | class PdfComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/plus_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The plus icon 6 | class PlusComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/components/blacklight/icons/repository_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Icons 5 | # The repository icon 6 | class RepositoryComponent < Blacklight::Icons::IconComponent 7 | self.svg = <<~SVG 8 | 9 | SVG 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/arclight/repositories_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Controller for our /repositories index page 5 | class RepositoriesController < ApplicationController 6 | def index 7 | @repositories = Arclight::Repository.all 8 | load_collection_counts 9 | end 10 | 11 | def show 12 | @repository = Arclight::Repository.find_by!(slug: params[:id]) 13 | search_service = Blacklight.repository_class.new(blacklight_config) 14 | @response = search_service.search( 15 | q: "level_ssim:Collection repository_ssim:\"#{@repository.name}\"", 16 | rows: 100 17 | ) 18 | @collections = @response.documents 19 | end 20 | 21 | private 22 | 23 | def load_collection_counts 24 | counts = fetch_collection_counts 25 | @repositories.each do |repository| 26 | repository.collection_count = counts[repository.name] || 0 27 | end 28 | end 29 | 30 | def fetch_collection_counts 31 | search_service = Blacklight.repository_class.new(blacklight_config) 32 | results = search_service.search( 33 | q: 'level_ssim:Collection', 34 | 'facet.field': 'repository_ssim', 35 | rows: 0 36 | ) 37 | Hash[*results.facet_fields['repository_ssim']] 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/helpers/arclight/field_config_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # A module to add configuration helpers for certain fields used by Arclight 6 | module FieldConfigHelpers 7 | include Arclight::EadFormatHelpers 8 | 9 | def link_to_name_facet(args) 10 | options = args[:config]&.separator_options || {} 11 | values = args[:value] || [] 12 | 13 | values.map do |value| 14 | link_to( 15 | value, 16 | search_action_path(f: { names: [value] }) 17 | ) 18 | end.to_sentence(options).html_safe 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/arclight/parent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # Logic containing information about Solr_Ead "Parent" 6 | # https://github.com/awead/solr_ead/blob/8cf7ffaa66e0e4c9c0b12f5646d6c2e20984cd99/lib/solr_ead/behaviors.rb#L54-L57 7 | class Parent 8 | attr_reader :id, :label, :eadid, :level 9 | 10 | def initialize(id:, label:, eadid:, level:) 11 | @id = id 12 | @label = label 13 | @eadid = eadid 14 | @level = level 15 | end 16 | 17 | def collection? 18 | level == 'collection' 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/arclight/parents.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # Object for parsing and formalizing Solr_Ead "Parents" 6 | # https://github.com/awead/solr_ead/blob/8cf7ffaa66e0e4c9c0b12f5646d6c2e20984cd99/lib/solr_ead/behaviors.rb#L54-L57 7 | class Parents 8 | attr_reader :ids, :labels, :levels 9 | 10 | def initialize(ids:, labels:, eadid:, levels:) 11 | @ids = ids 12 | @labels = labels 13 | @eadid = eadid 14 | @levels = levels 15 | end 16 | 17 | def eadid 18 | Arclight::NormalizedId.new(@eadid).to_s 19 | end 20 | 21 | ## 22 | # @return [Array[Arclight::Parent]] 23 | def as_parents 24 | ids.map.with_index { |id, idx| Arclight::Parent.new(id: id, label: labels[idx], eadid: eadid, level: levels[idx]) } 25 | end 26 | 27 | ## 28 | # @param [SolrDocument] document 29 | def self.from_solr_document(document) 30 | ids = document.parent_ids 31 | labels = document.parent_labels 32 | eadid = document.eadid 33 | levels = document.parent_levels 34 | new(ids: ids, labels: labels, eadid: eadid, levels: levels) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/arclight/requests/aeon_external_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Requests 5 | ## 6 | # This class should be used to turn configuration into a URL and 7 | # POST form specifically aimed at Aeon's external request 8 | # endpoint (https://support.atlas-sys.com/hc/en-us/articles/360011820054-External-Request-Endpoint) 9 | class AeonExternalRequest 10 | def initialize(document, presenter) 11 | @document = document 12 | @presenter = presenter 13 | end 14 | 15 | def config 16 | @config ||= @document.repository_config.request_config_for_type('aeon_external_request_endpoint') 17 | end 18 | 19 | def url 20 | "#{config['request_url']}?#{url_params}" 21 | end 22 | 23 | def form_mapping 24 | static_mappings.merge(dynamic_mappings) 25 | end 26 | 27 | def static_mappings 28 | config['request_mappings']['static'] 29 | end 30 | 31 | def dynamic_mappings 32 | config['request_mappings']['accessor'].transform_values do |v| 33 | @document.public_send(v.to_sym) 34 | end 35 | end 36 | 37 | def url_params 38 | config['request_mappings']['url_params'].to_query 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/arclight/requests/aeon_web_ead.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Requests 5 | ## 6 | # This object relies on the ability to respond to attributes passed in as 7 | # query parameters from the form mapping configuration 8 | class AeonWebEad 9 | attr_reader :document, :ead_url 10 | 11 | ## 12 | # @param [SolrDocument] document 13 | # @param [String] ead_url 14 | def initialize(document, ead_url) 15 | @document = document 16 | @ead_url = ead_url 17 | end 18 | 19 | ## 20 | # Url target for Aeon request params 21 | def request_url 22 | request_config['request_url'] 23 | end 24 | 25 | ## 26 | # Constructed request URL 27 | def url 28 | "#{request_url}?#{form_mapping.to_query}" 29 | end 30 | 31 | ## 32 | # Converts mappings as a query url param into a Hash used for sending 33 | # messages 34 | # If a defined method is provided as a value, that method will be invoked 35 | # "collection_name=entry.123" => { "collection_name" => "entry.123" } 36 | # @return [Hash] 37 | def form_mapping 38 | form_hash = Rack::Utils.parse_nested_query( 39 | request_config['request_mappings'] 40 | ) 41 | form_hash.each do |key, value| 42 | respond_to?(value) && form_hash[key] = send(value) 43 | end 44 | form_hash 45 | end 46 | 47 | def request_config 48 | document.repository_config.request_config_for_type('aeon_web_ead') 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/models/arclight/requests/google_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Requests 5 | ## 6 | # This object relies on the ability to respond to attributes passed in as 7 | # query parameters from the form mapping configuratino 8 | class GoogleForm 9 | attr_reader :document, :presenter, :document_url 10 | 11 | delegate :collection_name, :collection_creator, :eadid, :containers, to: :document 12 | 13 | ## 14 | # @param [SolrDocument] document 15 | # @param [Arclight::ShowPresenter] presenter 16 | # @param [String] document_url 17 | def initialize(document, presenter, document_url) 18 | @document = document 19 | @presenter = presenter 20 | @document_url = document_url 21 | end 22 | 23 | ## 24 | # Url of form to fill 25 | def url 26 | request_config['request_url'] 27 | end 28 | 29 | ## 30 | # Converts mappings as a query url param into a Hash used for sending 31 | # messages and providing pre-filled form fields 32 | # "collection_name=entry.123" => { "collection_name" => "entry.123" } 33 | # @return [Hash] 34 | def form_mapping 35 | Rack::Utils.parse_nested_query( 36 | request_config['request_mappings'] 37 | ) 38 | end 39 | 40 | def title 41 | presenter.heading 42 | end 43 | 44 | def request_config 45 | document.repository_config&.request_config_for_type('google_form') 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/models/concerns/arclight/catalog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # Arclight specific methods for the Catalog 6 | module Catalog 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | before_action only: :index do 11 | if (params.dig(:f, :collection) || []).any?(&:blank?) 12 | params[:f][:collection].compact_blank! 13 | params[:f].delete(:collection) if params[:f][:collection].blank? 14 | end 15 | end 16 | 17 | before_action only: :hierarchy do 18 | blacklight_config.search_state_fields += %i[id limit offset] 19 | end 20 | 21 | Blacklight::Configuration.define_field_access :summary_field, Blacklight::Configuration::ShowField 22 | Blacklight::Configuration.define_field_access :background_field, Blacklight::Configuration::ShowField 23 | Blacklight::Configuration.define_field_access :related_field, Blacklight::Configuration::ShowField 24 | Blacklight::Configuration.define_field_access :indexed_terms_field, Blacklight::Configuration::ShowField 25 | Blacklight::Configuration.define_field_access :in_person_field, Blacklight::Configuration::ShowField 26 | Blacklight::Configuration.define_field_access :cite_field, Blacklight::Configuration::ShowField 27 | Blacklight::Configuration.define_field_access :contact_field, Blacklight::Configuration::ShowField 28 | Blacklight::Configuration.define_field_access :component_field, Blacklight::Configuration::ShowField 29 | Blacklight::Configuration.define_field_access :component_indexed_terms_field, Blacklight::Configuration::ShowField 30 | Blacklight::Configuration.define_field_access :terms_field, Blacklight::Configuration::ShowField 31 | Blacklight::Configuration.define_field_access :component_terms_field, Blacklight::Configuration::ShowField 32 | Blacklight::Configuration.define_field_access :group_header_field, Blacklight::Configuration::IndexField 33 | end 34 | 35 | def hierarchy 36 | @response = search_service.search_results 37 | end 38 | 39 | # Overrides blacklight search state so we can exclude some parameters from being passed into the SearchState 40 | def search_state 41 | @search_state ||= search_state_class.new(params.except('hierarchy', 'nest_path'), blacklight_config, self) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/models/concerns/arclight/search_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # Customized Search Behavior for Arclight 6 | module SearchBehavior 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | self.default_processor_chain += %i[ 11 | add_highlighting 12 | add_grouping 13 | add_hierarchy_behavior 14 | ] 15 | end 16 | 17 | # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity 18 | def add_hierarchy_behavior(solr_parameters) 19 | return unless search_state.controller&.action_name == 'hierarchy' 20 | 21 | solr_parameters[:fq] ||= [] 22 | solr_parameters[:fq] << "_nest_parent_:#{blacklight_params[:id]}" 23 | solr_parameters[:rows] = blacklight_params[:per_page]&.to_i || blacklight_params[:limit]&.to_i || 999_999_999 24 | solr_parameters[:start] = blacklight_params[:offset] if blacklight_params[:offset] 25 | solr_parameters[:sort] = 'sort_isi asc' 26 | solr_parameters[:facet] = false 27 | end 28 | # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity 29 | 30 | ## 31 | # Add highlighting 32 | def add_highlighting(solr_params) 33 | solr_params['hl'] = true 34 | solr_params['hl.fl'] = CatalogController.blacklight_config.highlight_field 35 | solr_params['hl.snippets'] = 3 36 | solr_params 37 | end 38 | 39 | ## 40 | # Adds grouping parameters for Solr if enabled 41 | def add_grouping(solr_params) 42 | solr_params.merge!(Arclight::Engine.config.catalog_controller_group_query_params) if blacklight_params[:group] == 'true' 43 | 44 | solr_params 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/presenters/arclight/index_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Custom presentation methods for index partials 5 | class IndexPresenter < Blacklight::IndexPresenter 6 | def label(*) 7 | document.normalized_title 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/presenters/arclight/show_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Custom presentation methods for show partial 5 | class ShowPresenter < Blacklight::ShowPresenter 6 | attr_accessor :field_group 7 | 8 | delegate :collection?, to: :document 9 | 10 | def heading 11 | document.normalized_title 12 | end 13 | 14 | def with_field_group(group) 15 | if block_given? 16 | old_group = field_group 17 | 18 | begin 19 | self.field_group = group 20 | yield(self) 21 | ensure 22 | self.field_group = old_group if block_given? 23 | end 24 | else 25 | dup.tap { |x| x.field_group = group } 26 | end 27 | end 28 | 29 | private 30 | 31 | # @return [Hash] all the fields for this index view 32 | def fields 33 | if field_group 34 | configuration["#{field_group}s"] || [] 35 | else 36 | super 37 | end 38 | end 39 | 40 | def field_config(field) 41 | return super unless field_group 42 | 43 | fields.fetch(field) do 44 | if defined?(Blacklight::Configuration::NullDisplayField) 45 | # For Blacklight 8: 46 | Blacklight::Configuration::NullDisplayField.new(field) 47 | else 48 | # Blacklight 7 49 | Blacklight::Configuration::NullField.new(field) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/views/arclight/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/arclight/2fcfe481afcec80a5fe78b6ce45a415c70b123e9/app/views/arclight/.keep -------------------------------------------------------------------------------- /app/views/arclight/_requests.html.erb: -------------------------------------------------------------------------------- 1 | <% if document.requestable? %> 2 |
3 | <% document.repository_config.available_request_types.each do |request_type| %> 4 | <%= render partial: "arclight/requests/#{request_type}", locals: { document: document } %> 5 | <% end %> 6 |
7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/arclight/repositories/_repository.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= image_tag repository.thumbnail_url, alt: repository.name, class: 'img-fluid d-block mx-auto' %> 6 |
7 | 8 |
9 | <%= content_tag(params[:id] == repository.slug ? :h1 : :h2, class: 'h5 mb-3 repo-name') do %> 10 | <%= link_to_unless(params[:id] == repository.slug, repository.name, arclight_engine.repository_path(repository.slug)) %> 11 | <% end %> 12 |
13 |
14 |
15 |
16 | <%= repository.location %> 17 |
18 | 19 |
20 | <%= repository.contact %> 21 |
22 |
23 |
24 | 25 |
26 | <%= repository.description %> 27 |
28 | 29 | <% if on_repositories_index? %> 30 |
31 |
32 | <%= t(:'arclight.views.repositories.number_of_collections', count: repository.collection_count) %> 33 |
34 | <% if repository.collection_count&.positive? %> 35 | <%= link_to(t(:'arclight.views.repositories.view_more'), arclight_engine.repository_path(repository.slug), class: 'btn btn-secondary btn-sm') %> 36 | <% end %> 37 |
38 | <% end %> 39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /app/views/arclight/repositories/index.html.erb: -------------------------------------------------------------------------------- 1 | <% breadcrumb :repositories %> 2 | 3 |

<%= t('arclight.repositories') %>

4 | 5 |
6 | <% @page_title = t('arclight.views.repositories.title') %> 7 | <%= render 'shared/breadcrumbs' %> 8 | <%= render @repositories %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/arclight/repositories/show.html.erb: -------------------------------------------------------------------------------- 1 | <% @page_title = t(:'arclight.views.repositories.show', name: @repository.name) %> 2 | <% breadcrumb :repository, @repository %> 3 | <%= render 'shared/breadcrumbs' %> 4 | <%= render @repository %> 5 |
6 |
7 |

8 | <%= t('arclight.views.show.our_collections') %> 9 |

10 |
11 |
12 | 13 | <%= link_to(t(:'arclight.views.repositories.view_all_collections'), repository_collections_path(@repository)) %> 14 | 15 |
16 |
17 | 18 |
19 | <% @collections.each do |document| %> 20 | <% doc_presenter = Arclight::IndexPresenter.new(document, self, CatalogController.blacklight_config) %> 21 |
22 |
23 |
24 |
25 |

<%= link_to doc_presenter.label, solr_document_path(document.id) %>

26 | <%= render Arclight::IndexMetadataFieldComponent.with_collection(doc_presenter.field_presenters.select { |p| p.field_config.repository_context }) %> 27 |
28 | <% if document.unitid %> 29 |
30 | <%= t(:'arclight.views.show.collection_id', id: document.unitid) %> 31 |
32 | <% end %> 33 |
34 |
35 |
36 | <% end %> 37 |
38 | -------------------------------------------------------------------------------- /app/views/arclight/requests/_aeon_external_request_endpoint.html.erb: -------------------------------------------------------------------------------- 1 | <% aeon_external_request ||= Arclight::Requests::AeonExternalRequest.new(document, document_presenter(document)) %> 2 | 3 | <%= form_tag aeon_external_request.url, method: :post, class: 'd-inline-block al-request-form' do |f| %> 4 | <%= render(Blacklight::HiddenSearchStateComponent.new(params: aeon_external_request.form_mapping)) %> 5 | 8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/arclight/requests/_aeon_web_ead.html.erb: -------------------------------------------------------------------------------- 1 | <% if parsed_files = document.ead_file %> 2 | <% aeon_web_ead ||= Arclight::Requests::AeonWebEad.new(document, parsed_files.href) %> 3 | <% aeon_web_ead_url = aeon_web_ead&.url %> 4 | 5 | <%= link_to 'Request', aeon_web_ead_url, class: 'al-request-button btn btn-primary btn-sm me-1' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/arclight/requests/_google_form.html.erb: -------------------------------------------------------------------------------- 1 | <% google_form ||= Arclight::Requests::GoogleForm.new(document, document_presenter(document), solr_document_path(document)) %> 2 | 3 | <%= form_tag google_form.url, method: :get, class: 'd-inline-block al-request-form' do |f| %> 4 | <% google_form.form_mapping.each do |key, value| %> 5 | <%= hidden_field_tag value, google_form.public_send(key) %> 6 | <% end %> 7 | 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/catalog/_document_list.html.erb: -------------------------------------------------------------------------------- 1 | <% # Override to conditionally add id="documents" on div. When documents are grouped it causes duplicate id issues %> 2 | <% # https://github.com/projectblacklight/blacklight/blob/v8.1.0/app/views/catalog/_document_list.html.erb %> 3 | <% # container for all documents in index list view -%> 4 | <% view_config = local_assigns[:view_config] || blacklight_config&.view_config(document_index_view_type) %> 5 |
class="al-document-listings documents-<%= view_config&.key || document_index_view_type %>"> 6 | <% document_presenters = documents.map { |doc| document_presenter(doc) } -%> 7 | <%= render view_config.document_component.with_collection(document_presenters, partials: view_config.partials, counter_offset: @response&.start || 0) %> 8 |
-------------------------------------------------------------------------------- /app/views/catalog/_group.html.erb: -------------------------------------------------------------------------------- 1 | <% # container for all groups in index view -%> 2 |
3 | <%= render (blacklight_config.view_config(document_index_view_type).group_component || Arclight::GroupComponent).with_collection(@response.groups) %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/catalog/_group_toggle.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to t('arclight.views.index.all_results'), 3 | search_catalog_path(search_without_group), 4 | class: "btn btn-outline-secondary #{'active' unless grouped?}" 5 | %> 6 | <%= link_to t('arclight.views.index.group_by_collection'), 7 | search_catalog_path(search_with_group), 8 | class: "btn btn-outline-secondary #{'active' if grouped?}" 9 | %> 10 |
11 | -------------------------------------------------------------------------------- /app/views/catalog/_search_results_header.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'shared/breadcrumbs' %> 2 | 3 |

<%= search_results_header_text %>

4 | -------------------------------------------------------------------------------- /app/views/catalog/hierarchy.html.erb: -------------------------------------------------------------------------------- 1 |

<%= params[:id] %>

2 | <%= turbo_frame_tag "al-hierarchy-#{params[:id]}#{params[:key]}" do %> 3 | <%= render partial: "paginate_compact", object: @response if show_pagination? && params[:paginate] %> 4 | <% presenters = @response.documents.map{ | document | document_presenter(document) } %> 5 | <% if params[:hierarchy].present? %> 6 | 11 | <% else %> 12 | 13 | <%= render Arclight::DocumentCollectionContextComponent.with_collection(presenters, 14 | blacklight_config: blacklight_config) %> 15 |
16 | <% end %> 17 | 18 | <%= render 'results_pagination' if params[:paginate] %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /app/views/catalog/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:sidebar) do %> 2 | <% conf = blacklight_config.view_config(document_index_view_type) %> 3 | <%= render conf.sidebar_component.new(blacklight_config: blacklight_config, 4 | response: @response, 5 | view_config: conf) %> 6 | <% end %> 7 | 8 | <% breadcrumb :search_results, search_state %> 9 | 10 | <%= render 'search_results' %> 11 | -------------------------------------------------------------------------------- /app/views/shared/_breadcrumbs.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/views/shared/_main_menu_links.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /arclight.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'arclight/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'arclight' 9 | spec.version = Arclight::VERSION 10 | spec.authors = ['Darren Hardy', 'Jessie Keck', 'Gordon Leacock', 'Jack Reed'] 11 | spec.email = ['drh@stanford.edu', 'jessie.keck@gmail.com', 'gordonl@umich.edu', 'phillipjreed@gmail.com'] 12 | 13 | spec.summary = 'A Blacklight-based environment to support discovery and delivery for archives and special collections' 14 | spec.description = '' 15 | spec.homepage = 'https://library.stanford.edu/projects/arclight' 16 | spec.metadata = { 'source_code_uri' => 'https://github.com/projectblacklight/arclight' } 17 | spec.license = 'Apache-2.0' 18 | 19 | spec.required_ruby_version = '>= 3.0.0' 20 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 21 | f.match(%r{^(test|spec|features)/}) 22 | end 23 | spec.bindir = 'exe' 24 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 25 | spec.require_paths = ['lib'] 26 | 27 | spec.add_dependency 'blacklight', '>= 8.0.0', '< 10' 28 | spec.add_dependency 'gretel' 29 | spec.add_dependency 'rails', '>= 7.1', '< 9' 30 | spec.add_dependency 'traject', '~> 3.0' 31 | spec.add_dependency 'traject_plus', '~> 2.0' 32 | 33 | spec.add_development_dependency 'bundler' 34 | spec.add_development_dependency 'capybara' 35 | spec.add_development_dependency 'engine_cart' 36 | spec.add_development_dependency 'i18n-tasks' 37 | spec.add_development_dependency 'rake', '>= 12.0' 38 | spec.add_development_dependency 'rspec-rails' 39 | spec.add_development_dependency 'rubocop', '~> 1.8' 40 | spec.add_development_dependency 'rubocop-rails', '~> 2.8' 41 | spec.add_development_dependency 'rubocop-rake' 42 | spec.add_development_dependency 'rubocop-rspec', '~> 2.3' 43 | spec.add_development_dependency 'selenium-webdriver' 44 | spec.add_development_dependency 'simplecov' 45 | spec.add_development_dependency 'solr_wrapper' 46 | end 47 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "arclight" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 6 | ENGINE_PATH = File.expand_path('../../lib/arclight/engine', __FILE__) 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 11 | 12 | require 'rails/all' 13 | require 'rails/engine/commands' 14 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /config/breadcrumbs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Root crumb 4 | crumb :root do 5 | link t('arclight.routes.home'), root_path 6 | end 7 | 8 | crumb :repositories do 9 | link t('arclight.routes.repositories'), arclight_engine.repositories_path 10 | end 11 | 12 | crumb :repository do |repository| 13 | link repository.name, arclight_engine.repository_path(repository.slug) 14 | 15 | parent :repositories 16 | end 17 | 18 | crumb :search_results do |search_state| 19 | if search_state.filter('level').values == ['Collection'] 20 | link t('arclight.routes.collections') 21 | else 22 | link t('arclight.routes.search_results') 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | pin_all_from File.expand_path('../app/assets/javascript', __dir__) 4 | -------------------------------------------------------------------------------- /config/repositories.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/arclight/2fcfe481afcec80a5fe78b6ce45a415c70b123e9/config/repositories.yml -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Arclight::Engine.routes.draw do 4 | get 'collections' => 'catalog#index', defaults: { f: { level: ['Collection'] } }, as: :collections 5 | resources :repositories, only: %i[index show], controller: 'arclight/repositories' 6 | end 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | solr: 5 | image: solr:9.7.0 6 | volumes: 7 | - $PWD/solr/conf:/opt/solr/conf 8 | ports: 9 | - 8983:8983 10 | entrypoint: 11 | - docker-entrypoint.sh 12 | - solr-precreate 13 | - blacklight-core 14 | - /opt/solr/conf 15 | - "-Xms256m" 16 | - "-Xmx512m" 17 | -------------------------------------------------------------------------------- /lib/arclight.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'arclight/version' 4 | require 'arclight/engine' 5 | require 'arclight/repository' 6 | require 'arclight/year_range' 7 | 8 | # :nodoc: 9 | module Arclight 10 | autoload :Routes, 'arclight/routes' 11 | 12 | def self.deprecation 13 | @deprecation ||= ActiveSupport::Deprecation.new('3.0', 'Arclight') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/arclight/digital_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # Plain ruby class to model serializing/deserializing digital object data 6 | class DigitalObject 7 | attr_reader :label, :href 8 | 9 | def initialize(label:, href:) 10 | @label = label.presence || href 11 | @href = href 12 | end 13 | 14 | def to_json(*) 15 | { label: label, href: href }.to_json 16 | end 17 | 18 | def self.from_json(json) 19 | object_data = JSON.parse(json) 20 | new(label: object_data['label'], href: object_data['href']) 21 | end 22 | 23 | def ==(other) 24 | href == other.href && label == other.label 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/arclight/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'blacklight' 4 | require 'traject' 5 | require 'active_model' 6 | require 'arclight/exceptions' 7 | require 'arclight/normalized_date' 8 | require 'arclight/normalized_id' 9 | require 'arclight/normalized_title' 10 | require 'arclight/digital_object' 11 | require 'gretel' 12 | 13 | module Arclight 14 | ## 15 | # This is the defining class for the Arclight Rails Engine 16 | class Engine < ::Rails::Engine 17 | config.oembed_resource_exclude_patterns = [/\.pdf$/, /\.ppt$/] 18 | 19 | Arclight::Engine.config.catalog_controller_group_query_params = { 20 | group: true, 21 | 'group.field': '_root_', 22 | 'group.ngroups': true, 23 | 'group.limit': 3, 24 | fl: '*,collection:[subquery]', 25 | 'collection.q': '{!terms f=id v=$row._root_}', 26 | 'collection.defType': 'lucene', 27 | 'collection.fl': '*', 28 | 'collection.rows': 1 29 | } 30 | 31 | initializer 'arclight.helpers' do 32 | config.after_initialize do 33 | ActiveSupport.on_load(:action_view) { include ArclightHelper } 34 | end 35 | end 36 | 37 | initializer 'arclight.assets', before: 'assets' do |app| 38 | app.config.assets.precompile << 'arclight/arclight.js' 39 | app.config.assets.precompile << 'arclight/oembed_controller.js' 40 | app.config.assets.precompile << 'arclight/truncate_controller.js' 41 | end 42 | 43 | initializer 'arclight.importmap', before: 'importmap' do |app| 44 | app.config.importmap.paths << Engine.root.join('config/importmap.rb') if app.config.respond_to?(:importmap) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/arclight/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Exceptions 5 | # Id's must be present on all documents and components 6 | class IDNotFound < StandardError 7 | def message 8 | 'id must be present for all documents and components' 9 | end 10 | end 11 | 12 | # Unittitle or unitdate must be present on all documents and components 13 | class TitleNotFound < StandardError 14 | def message 15 | ' or must be present for all documents and components' 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/arclight/hash_absolute_xpath.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest' 4 | 5 | module Arclight 6 | ## 7 | # Take a Nokogiri node and get its absolute path (inserting our own indexes for component levels) 8 | # and hash that outout. This is intended as a potential strategy for handling missing IDs in EADs. 9 | class HashAbsoluteXpath 10 | class << self 11 | attr_writer :hash_algorithm 12 | 13 | def hash_algorithm 14 | return Digest::SHA1 unless defined? @hash_algorithm 15 | 16 | @hash_algorithm 17 | end 18 | end 19 | 20 | COMPONENT_NODE_NAME_REGEX = /^c\d{,2}$/ 21 | attr_reader :node 22 | 23 | def initialize(node) 24 | @node = node 25 | end 26 | 27 | def to_hexdigest 28 | self.class.hash_algorithm.hexdigest(absolute_xpath).prepend('al_') 29 | end 30 | 31 | def absolute_xpath 32 | ancestor_tree = node.ancestors.map do |ancestor| 33 | ancestor_name_and_index(ancestor) 34 | end 35 | 36 | "#{[ancestor_tree.reverse, node.name].flatten.join('/')}#{current_index}" 37 | end 38 | 39 | private 40 | 41 | def current_index 42 | siblings.index(node) 43 | end 44 | 45 | def component_siblings_for_node(xml_node) 46 | xml_node.parent.children.select { |n| n.name =~ COMPONENT_NODE_NAME_REGEX } 47 | end 48 | 49 | def siblings 50 | @siblings ||= component_siblings_for_node(node) 51 | end 52 | 53 | def ancestor_name_and_index(ancestor) 54 | if ancestor.name =~ COMPONENT_NODE_NAME_REGEX 55 | index = component_siblings_for_node(ancestor).index(ancestor) 56 | "#{ancestor.name}#{index}" 57 | else 58 | ancestor.name 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/arclight/level_label.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # A utility class to return a human-readable label for an EAD @level code. 6 | # Can use the value from @otherlevel if provided. 7 | # Examples from @level: recordgrp = "Record Group" 8 | # collection = "Collection" 9 | # subseries = "Subseries" 10 | # otherlevel = (text provided in @otherlevel) 11 | class LevelLabel 12 | # @param [String] `level` from the collection or component @level 13 | # @param [String] `other_level` from the collection or component @otherlevel 14 | def initialize(level, other_level = nil) 15 | @level = level 16 | @other_level = other_level if other_level.present? 17 | end 18 | 19 | # @return [String] the human-readable label 20 | def to_s 21 | human_readable_level 22 | end 23 | 24 | private 25 | 26 | attr_reader :level, :other_level 27 | 28 | CUSTOM_LEVEL_LABELS = { 29 | recordgrp: 'Record Group', 30 | subgrp: 'Subgroup' 31 | }.freeze 32 | 33 | def human_readable_level 34 | if level == 'otherlevel' 35 | alternative_level 36 | elsif level.present? 37 | CUSTOM_LEVEL_LABELS.fetch(level.to_sym, level.capitalize).to_s 38 | end 39 | end 40 | 41 | def alternative_level 42 | alternative_level = other_level if other_level 43 | alternative_level.present? ? alternative_level.capitalize : 'Other' 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/arclight/missing_id_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'arclight/hash_absolute_xpath' 4 | 5 | module Arclight 6 | ## 7 | # A class to configure a selected MissingIdStrategy. 8 | # Defaults to Arclight::HashAbsoluteXpath 9 | # This can be updated in an initializer to be any other class 10 | class MissingIdStrategy 11 | class << self 12 | attr_writer :selected 13 | 14 | def selected 15 | return Arclight::HashAbsoluteXpath unless defined? @selected 16 | 17 | @selected 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/arclight/normalized_date.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | ## 5 | # A utility class to normalize dates, typically by joining inclusive and bulk dates 6 | # e.g., "1990-2000, bulk 1990-1999" 7 | # @see http://www2.archivists.org/standards/DACS/part_I/chapter_2/4_date 8 | class NormalizedDate 9 | # @param [String | Array] `inclusive` from the `unitdate` 10 | # @param [Array] `bulk` from the `unitdate` 11 | # @param [Array] `other` from the `unitdate` when type is not specified 12 | def initialize(inclusive, bulk = [], other = []) 13 | @inclusive = (inclusive || []).map do |inclusive_text| 14 | if inclusive_text.is_a? Array # of YYYY-YYYY for ranges 15 | # NOTE: This code is not routable AFAICT in actual indexing. 16 | # We pass arrays of strings (or xml nodes) here, and never a multidimensional array 17 | year_range(inclusive_text) 18 | elsif inclusive_text.present? 19 | inclusive_text.strip 20 | end 21 | end&.join(', ') 22 | 23 | @bulk = Array.wrap(bulk).compact.map(&:strip).join(', ') 24 | @other = Array.wrap(other).compact.map(&:strip).join(', ') 25 | end 26 | 27 | # @return [String] the normalized title/date 28 | def to_s 29 | normalize 30 | end 31 | 32 | private 33 | 34 | attr_reader :inclusive, :bulk, :other 35 | 36 | def year_range(date_array) 37 | YearRange.new(date_array.include?('/') ? date_array : date_array.map { |v| v.tr('-', '/') }).to_s 38 | end 39 | 40 | # @see http://www2.archivists.org/standards/DACS/part_I/chapter_2/4_date for rules 41 | def normalize 42 | result = [] 43 | result << inclusive if inclusive.present? 44 | result << other if other.present? 45 | result << "bulk #{bulk}" if bulk.present? 46 | result.compact.map(&:strip).join(', ').presence 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/arclight/normalized_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'arclight/exceptions' 4 | 5 | module Arclight 6 | ## 7 | # A simple utility class to normalize identifiers 8 | class NormalizedId 9 | # Accepts unused kwargs from the ead2_config.rb id to_field directive 10 | # (:title and :repository) so that applications can provide a custom 11 | # id_normalizer class to traject to form the collection id from these attributes. 12 | def initialize(id, **_kwargs) 13 | @id = id 14 | end 15 | 16 | def to_s 17 | normalize 18 | end 19 | 20 | private 21 | 22 | attr_reader :id 23 | 24 | def normalize 25 | raise Arclight::Exceptions::IDNotFound if id.blank? 26 | 27 | id.strip.tr('.', '-') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/arclight/normalized_title.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'arclight/exceptions' 4 | 5 | module Arclight 6 | ## 7 | # A utility class to normalize titles, typically by joining 8 | # the title and date, e.g., "My Title, 1990-2000" 9 | class NormalizedTitle 10 | # @param [String] `title` from the `unittitle` 11 | # @param [String] `date` from the `unitdate` 12 | def initialize(title, date = nil) 13 | @title = title.gsub(/\s*,\s*$/, '').strip if title.present? 14 | @date = date.strip if date.present? 15 | end 16 | 17 | # @return [String] the normalized title/date 18 | def to_s 19 | normalize 20 | end 21 | 22 | private 23 | 24 | attr_reader :title, :date, :default 25 | 26 | def normalize 27 | result = [title, date].compact.join(', ') 28 | raise Arclight::Exceptions::TitleNotFound if result.blank? 29 | 30 | result 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/arclight/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | # Custom routes for Arclight 5 | module Routes 6 | require 'arclight/routes/hierarchy' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/arclight/routes/hierarchy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Routes 5 | # Inject a hierarchy route for displaying the 6 | # components in the collection context 7 | class Hierarchy 8 | def initialize(defaults = {}) 9 | @defaults = defaults 10 | end 11 | 12 | def call(mapper, _options = {}) 13 | mapper.member do 14 | mapper.get 'hierarchy' 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/arclight/traject/nokogiri_namespaceless_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | module Traject 5 | # Provides a Traject Reader for XML Documents which removes the namespaces 6 | class NokogiriNamespacelessReader < ::Traject::NokogiriReader 7 | # Overrides the #each method (which is used for iterating through each Document) 8 | # @param args 9 | # @see ::Traject::NokogiriReader#each 10 | # @see Enumerable#each 11 | def each(*args) 12 | return to_enum(:each, *args) unless block_given? 13 | 14 | super do |doc| 15 | new_doc = doc.dup 16 | new_doc.remove_namespaces! 17 | yield new_doc 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/arclight/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arclight 4 | VERSION = '2.0.0.alpha' 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/arclight/templates/arclight.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bs-breadcrumb-divider: "»"; 3 | } 4 | 5 | // Arclight, imported from arclight frontend package 6 | @import 'arclight/app/assets/stylesheets/arclight/application'; 7 | -------------------------------------------------------------------------------- /lib/generators/arclight/templates/config/downloads.yml: -------------------------------------------------------------------------------- 1 | # 2 | # downloads.yml - Use the EAD's as the primary key and 3 | # provide the PDF and/or EAD (.xml) links. The 4 | # size value should be a String (shown as-is) or 5 | # the number of bytes in the download. 6 | # - Pass a template key to use a formatted string 7 | # which interpolates document accessors into the 8 | # url using the %{accessor} syntax. 9 | # - Pass a size_accessor key to pull the size of 10 | # the file from an accessor in the solr document 11 | # 12 | sample_unitid: 13 | pdf: 14 | href: 'http://example.com/sample.pdf' 15 | size: '1.23MB' 16 | ead: 17 | href: 'http://example.com/sample.xml' 18 | size: 123456 19 | # size_accessor: 'level' 20 | default: 21 | disabled: true 22 | pdf: 23 | template: 'http://example.com/%{unitid}.pdf' 24 | ead: 25 | template: 'http://example.com/%{unitid}.xml' 26 | -------------------------------------------------------------------------------- /lib/generators/arclight/templates/config/locales/arclight.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | blacklight: 3 | search: 4 | fields: 5 | abstract: Abstract 6 | creators: Creator 7 | extent: Extent 8 | language: Language 9 | prefercite: Preferred citation 10 | 11 | scopecontent: Scope and content 12 | bioghist: Biographical / historical 13 | acqinfo: Acquisition information 14 | appraisal: Appraisal information 15 | custodhist: Custodial history 16 | processinfo: Processing information 17 | arrangement: Arrangement 18 | accruals: Accruals 19 | phystech: Physical / technical requirements 20 | physloc: Physical location 21 | physdesc: Physical description 22 | physfacet: Physical facet 23 | dimensions: Dimensions 24 | indexes: Indexes 25 | descrules: Rules or conventions 26 | 27 | relatedmaterial: Related material 28 | separatedmaterial: Separated material 29 | otherfindaid: Other finding aids 30 | altformavail: Alternative form available 31 | originalsloc: Location of originals 32 | materialspec: Material specific details 33 | fileplan: File plan 34 | odd: Other descriptive data 35 | note: Note 36 | 37 | access_subjects: Subjects 38 | names_coll: Names 39 | places: Places 40 | 41 | containers: Containers 42 | names: Names 43 | 44 | restrictions: Restrictions 45 | terms: Terms of access 46 | parent_restrictions: Parent restrictions 47 | parent_terms: Parent terms of access 48 | repository_location: Location of this collection 49 | before_you_visit: Before you visit 50 | repository_contact: Contact 51 | 52 | facet: 53 | collection: Collection 54 | creator: Creator 55 | date_range: Date range 56 | level: Level 57 | names: Names 58 | repository: Repository 59 | place: Place 60 | subject: Subject 61 | access: Access 62 | -------------------------------------------------------------------------------- /lib/generators/arclight/update_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'generators/arclight/install_generator' 4 | 5 | module Arclight 6 | ## 7 | # Arclight Update generator. This subclasses the Install generator, so this is 8 | # intended to override behavior in the install generator that can allow the 9 | # downstream application to choose if they want to take our changes or not and 10 | # can choose to see a diff of our changes to help them decide. 11 | class Update < Arclight::Install 12 | source_root File.expand_path('templates', __dir__) 13 | 14 | def create_blacklight_catalog 15 | copy_file 'catalog_controller.rb', 'app/controllers/catalog_controller.rb' 16 | end 17 | 18 | def solr_config 19 | directory '../../../../solr', 'solr' 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arclight", 3 | "version": "2.0.0-alpha", 4 | "description": "The frontend for arclight", 5 | "main": "app/assets/javascript/arclight/arclight.js", 6 | "files": ["app/assets"], 7 | "devDependencies": { 8 | "eslint": "^8.26.0", 9 | "eslint-config-airbnb-base": "^15.0.0", 10 | "eslint-plugin-import": "^2.2.0", 11 | "sass": "^1.59.3", 12 | "bootstrap": "^5.2.0" 13 | }, 14 | "scripts": { 15 | "build": "sass ./app/assets/stylesheets/arclight/build.scss:./app/assets/builds/arclight.css --no-source-map --load-path=node_modules", 16 | "lint": "eslint './app/assets/javascripts/**/*.{js,es6}'", 17 | "lint:fix": "eslint --fix './app/assets/javascripts/**/*.{js,es6}'", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/projectblacklight/arclight.git" 23 | }, 24 | "author": "", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/projectblacklight/arclight/issues" 28 | }, 29 | "homepage": "https://library.stanford.edu/projects/arclight" 30 | } 31 | -------------------------------------------------------------------------------- /solr/conf/_rest_managed.json: -------------------------------------------------------------------------------- 1 | { 2 | "initArgs":{}, 3 | "managedList":[]} -------------------------------------------------------------------------------- /solr/conf/admin-extra.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /solr/conf/elevate.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /solr/conf/protwords.txt: -------------------------------------------------------------------------------- 1 | # The ASF licenses this file to You under the Apache License, Version 2.0 2 | # (the "License"); you may not use this file except in compliance with 3 | # the License. You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | #----------------------------------------------------------------------- 14 | # Use a protected word file to protect against the stemmer reducing two 15 | # unrelated words to the same base word. 16 | 17 | # Some non-words that normally won't be encountered, 18 | # just to test that they won't be stemmed. 19 | dontstems 20 | zwhacky 21 | 22 | -------------------------------------------------------------------------------- /solr/conf/spellings.txt: -------------------------------------------------------------------------------- 1 | pizza 2 | history 3 | -------------------------------------------------------------------------------- /solr/conf/stopwords.txt: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | #----------------------------------------------------------------------- 17 | # a couple of test stopwords to test that the words are really being 18 | # configured from this file: 19 | stopworda 20 | stopwordb 21 | 22 | #Standard english stop words taken from Lucene's StopAnalyzer 23 | a 24 | an 25 | and 26 | are 27 | as 28 | at 29 | be 30 | but 31 | by 32 | for 33 | if 34 | in 35 | into 36 | is 37 | it 38 | no 39 | not 40 | of 41 | on 42 | or 43 | s 44 | such 45 | t 46 | that 47 | the 48 | their 49 | then 50 | there 51 | these 52 | they 53 | this 54 | to 55 | was 56 | will 57 | with 58 | 59 | -------------------------------------------------------------------------------- /solr/conf/stopwords_en.txt: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | #----------------------------------------------------------------------- 17 | # a couple of test stopwords to test that the words are really being 18 | # configured from this file: 19 | stopworda 20 | stopwordb 21 | 22 | #Standard english stop words taken from Lucene's StopAnalyzer 23 | a 24 | an 25 | and 26 | are 27 | as 28 | at 29 | be 30 | but 31 | by 32 | for 33 | if 34 | in 35 | into 36 | is 37 | it 38 | no 39 | not 40 | of 41 | on 42 | or 43 | s 44 | such 45 | t 46 | that 47 | the 48 | their 49 | then 50 | there 51 | these 52 | they 53 | this 54 | to 55 | was 56 | will 57 | with 58 | 59 | -------------------------------------------------------------------------------- /solr/conf/stopwords_punctuation.txt: -------------------------------------------------------------------------------- 1 | # Punctuation characters we want to ignore as terms (i.e., when surrounded 2 | # by whitespace in a query, like 'fred : the puppy') in queries 3 | # ONLY FOR SINGLE TOKEN ANALYZED FIELDS 4 | # see https://issues.apache.org/jira/browse/SOLR-3085 5 | # Note that plusses and double hyphens are not treated as terms 6 | # per debugQuery 7 | : 8 | ; 9 | & 10 | / 11 | = 12 | > 13 | < 14 | , 15 | . 16 | ( 17 | ) 18 | … 19 | » 20 | § 21 | • 22 | · 23 | - 24 | -------------------------------------------------------------------------------- /solr/conf/synonyms.txt: -------------------------------------------------------------------------------- 1 | # The ASF licenses this file to You under the Apache License, Version 2.0 2 | # (the "License"); you may not use this file except in compliance with 3 | # the License. You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | #----------------------------------------------------------------------- 14 | #some test synonym mappings unlikely to appear in real input text 15 | aaa => aaaa 16 | bbb => bbbb1 bbbb2 17 | ccc => cccc1,cccc2 18 | a\=>a => b\=>b 19 | a\,a => b\,b 20 | fooaaa,baraaa,bazaaa 21 | 22 | # Some synonym groups specific to this example 23 | GB,gib,gigabyte,gigabytes 24 | MB,mib,megabyte,megabytes 25 | Television, Televisions, TV, TVs 26 | #notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming 27 | #after us won't split it into two words. 28 | 29 | # Synonym mappings can be used for spelling correction too 30 | pixima => pixma 31 | 32 | -------------------------------------------------------------------------------- /solr/conf/xslt/example_rss.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 23 | 24 | 26 | 27 | 32 | 33 | 34 | 35 | Example Solr RSS 2.0 Feed 36 | http://localhost:8983/solr 37 | 38 | This has been formatted by the sample "example_rss.xsl" transform - 39 | use your own XSLT to get a nicer RSS feed. 40 | 41 | en-us 42 | http://localhost:8983/solr 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | <xsl:value-of select="str[@name='name']"/> 54 | 55 | http://localhost:8983/solr/select?q=id: 56 | 57 | 58 | 59 | 60 | 61 | 62 | http://localhost:8983/solr/select?q=id: 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/arclight_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight do 6 | it 'has a version number' do 7 | expect(Arclight::VERSION).not_to be_nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/components/arclight/breadcrumb_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::BreadcrumbComponent, type: :component do 6 | let(:document) do 7 | SolrDocument.new( 8 | parent_ids_ssim: %w[abc123 abc123_def abc123_ghi], 9 | parent_unittitles_ssm: %w[ABC123 DEF GHI], 10 | ead_ssi: 'abc123', 11 | repository_ssm: 'my repository', 12 | _root_: 'abc123' 13 | ) 14 | end 15 | 16 | let(:render) do 17 | component.render_in(vc_test_controller.view_context) 18 | end 19 | 20 | let(:rendered) do 21 | Capybara::Node::Simple.new(render) 22 | end 23 | 24 | let(:attr) { {} } 25 | 26 | let(:component) { described_class.new(document: document, **attr) } 27 | 28 | context 'with a count' do 29 | let(:attr) { { count: 2 } } 30 | 31 | it 'renders only that many breadcrumb links' do 32 | expect(rendered).to have_css 'li', text: 'my repository' 33 | expect(rendered).to have_link 'ABC123', href: '/catalog/abc123' 34 | expect(rendered).to have_no_link 'DEF', href: '/catalog/abc123_def' 35 | expect(rendered).to have_no_link 'GHI', href: '/catalog/abc123_ghi' 36 | end 37 | 38 | it 'renders an ellipsis if there are more links than the count' do 39 | expect(render).to end_with '>…' 40 | end 41 | end 42 | 43 | context 'with an offset' do 44 | let(:attr) { { offset: 2 } } 45 | 46 | it 'skips some breadcrumb links' do 47 | expect(rendered).to have_no_selector 'li', text: 'my repository' 48 | expect(rendered).to have_no_link 'ABC123', href: '/catalog/abc123' 49 | expect(rendered).to have_link 'DEF', href: '/catalog/abc123_def' 50 | expect(rendered).to have_link 'GHI', href: '/catalog/abc123_ghi' 51 | end 52 | end 53 | 54 | it 'renders breadcrumb links' do 55 | expect(rendered).to have_css 'li', text: 'my repository' 56 | expect(rendered).to have_link 'DEF', href: '/catalog/abc123_def' 57 | expect(rendered).to have_link 'GHI', href: '/catalog/abc123_ghi' 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/components/arclight/collection_sidebar_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::CollectionSidebarComponent, type: :component do 6 | subject(:component) do 7 | described_class.new(document: document, 8 | partials: CatalogController.blacklight_config.show.metadata_partials, 9 | collection_presenter: collection_presenter) 10 | end 11 | 12 | before do 13 | render_inline(component) 14 | end 15 | 16 | let(:document) { instance_double(SolrDocument, root: 'foo') } 17 | let(:collection_presenter) { instance_double(Arclight::ShowPresenter, with_field_group: group_presenter) } 18 | let(:group_presenter) { instance_double(Arclight::ShowPresenter, fields_to_render: [double]) } 19 | 20 | it 'has navigation links' do 21 | expect(page).to have_link 'Summary' 22 | expect(page).to have_link 'Background' 23 | expect(page).to have_link 'Related' 24 | expect(page).to have_link 'Indexed terms' 25 | expect(page).to have_link 'Access and use' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/components/arclight/document_download_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::DocumentDownloadComponent, type: :component do 6 | subject(:component) { described_class.new(downloads: downloads) } 7 | 8 | before do 9 | render_inline(component) 10 | end 11 | 12 | let(:downloads) { instance_double(Arclight::DocumentDownloads, files: files) } 13 | let(:pdf_data) { { 'href' => 'http://example.com/documents/abc123.pdf' } } 14 | let(:pdf) { Arclight::DocumentDownloads::File.new(type: 'pdf', data: pdf_data, document: document) } 15 | let(:document) { SolrDocument.new(id: 'abc123') } 16 | 17 | context 'with no files' do 18 | let(:files) { [] } 19 | 20 | it 'renders nothing' do 21 | expect(page).to have_no_css('*') 22 | end 23 | end 24 | 25 | context 'with one file' do 26 | let(:files) { [pdf] } 27 | 28 | it 'renders a download link' do 29 | expect(page).to have_link 'Download finding aid', href: 'http://example.com/documents/abc123.pdf' 30 | end 31 | end 32 | 33 | context 'with multiple files' do 34 | let(:files) { [pdf, ead] } 35 | let(:ead_data) { { 'href' => 'http://example.com/documents/abc123.xml' } } 36 | let(:ead) { Arclight::DocumentDownloads::File.new(type: 'ead', data: ead_data, document: document) } 37 | 38 | it 'renders download links' do 39 | expect(page).to have_button 'Download' 40 | expect(page).to have_link 'Finding aid', href: 'http://example.com/documents/abc123.pdf' 41 | expect(page).to have_link 'EAD', href: 'http://example.com/documents/abc123.xml' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/components/arclight/expand_hierarchy_button_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::ExpandHierarchyButtonComponent, type: :component do 6 | subject(:component) { described_class.new(path: '/some/path') } 7 | 8 | before do 9 | render_inline(component) 10 | end 11 | 12 | it 'renders the button' do 13 | expect(page).to have_text('Expand') 14 | expect(page).to have_css('.btn.btn-secondary.btn-sm') 15 | end 16 | 17 | context 'with a custom class' do 18 | subject(:component) { described_class.new(path: '/path/to_file', classes: 'btn btn-primary') } 19 | 20 | it 'renders the button with the custom classes' do 21 | expect(page).to have_text('Expand') 22 | expect(page).to have_css('.btn.btn-primary') 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/components/arclight/hierarchy_toggle_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::HierarchyToggleComponent, type: :component do 6 | let(:document) do 7 | instance_double(SolrDocument, id: 'abc123', children?: has_children) 8 | end 9 | 10 | let(:rendered) do 11 | render_inline(described_class.new(document: document, expanded: expanded)) 12 | end 13 | 14 | context 'when the document has children' do 15 | let(:has_children) { true } 16 | 17 | context 'when expanded is true' do 18 | let(:expanded) { true } 19 | 20 | it 'does not include the .collapsed class' do 21 | expect(rendered.to_html).to include('class="al-toggle-view-children"') 22 | expect(rendered.to_html).not_to include('collapsed') 23 | end 24 | 25 | it 'renders the link with aria-expanded=true' do 26 | expect(rendered.to_html).to include('aria-expanded="true"') 27 | end 28 | end 29 | 30 | context 'when expanded is false' do 31 | let(:expanded) { false } 32 | 33 | it 'includes the .collapsed class' do 34 | expect(rendered.to_html).to include('class="al-toggle-view-children collapsed"') 35 | end 36 | 37 | it 'renders the link with aria-expanded=false' do 38 | expect(rendered.to_html).to include('aria-expanded="false"') 39 | end 40 | end 41 | end 42 | 43 | context 'when the document has no children' do 44 | let(:has_children) { false } 45 | let(:expanded) { false } 46 | 47 | it 'does not render the component' do 48 | expect(rendered.to_html).to be_empty 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/components/arclight/repository_location_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::RepositoryLocationComponent, type: :component do 6 | let(:field) do 7 | # rubocop:disable Rails/RedundantActiveRecordAllMethod 8 | instance_double(Blacklight::FieldPresenter, key: 'blah', document: nil, label: 'blah', values: [Arclight::Repository.all.first], render_field?: true) 9 | # rubocop:enable Rails/RedundantActiveRecordAllMethod 10 | end 11 | let(:render) do 12 | component.render_in(vc_test_controller.view_context) 13 | end 14 | 15 | let(:rendered) do 16 | Capybara::Node::Simple.new(render) 17 | end 18 | 19 | let(:component) { described_class.new(field: field) } 20 | 21 | it 'renders the repository location information' do 22 | expect(rendered).to have_css('.al-in-person-repository-name', text: 'My Repository') 23 | expect(rendered).to have_css('address .al-repository-street-address-building', text: 'My Building') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/components/arclight/search_bar_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::SearchBarComponent, type: :component do 6 | let(:render) do 7 | component.render_in(view_context) 8 | end 9 | let(:view_context) { vc_test_controller.view_context } 10 | 11 | let(:rendered) do 12 | Capybara::Node::Simple.new(render) 13 | end 14 | 15 | let(:params) { {} } 16 | let(:component) { described_class.new(url: '/', params: params) } 17 | 18 | describe 'within collection dropdown' do 19 | context 'when in a collection context on the search results page' do 20 | let(:params) { { f: { collection: ['some collection'] } } } 21 | 22 | it 'renders a name attribute on the select (so it will be sent through the form)' do 23 | expect(rendered).to have_select('f[collection][]') 24 | end 25 | 26 | it 'has the "this collection" option selected' do 27 | expect(rendered).to have_css('select option[selected]', text: 'this collection') 28 | end 29 | end 30 | 31 | context 'when in a collection context, e.g. on show page for a collection' do 32 | let(:document) { SolrDocument.new(id: 'abc123', collection: { docs: [{ normalized_title_ssm: ['some collection'] }] }) } 33 | 34 | before do 35 | allow(view_context).to receive(:current_context_document).and_return(document) 36 | end 37 | 38 | it 'renders a name attribute on the select (so it will be sent through the form)' do 39 | expect(rendered).to have_select('f[collection][]') 40 | end 41 | 42 | it 'has the "this collection" option selected' do 43 | expect(rendered).to have_css('select option[selected]', text: 'this collection') 44 | end 45 | end 46 | 47 | context 'when not in a collection context' do 48 | it 'does not render a name attribute on the select (because it does not need to be sent through the form)' do 49 | expect(rendered).to have_no_select 'name' 50 | end 51 | 52 | it 'has the "this collection" option disabled' do 53 | expect(rendered).to have_css('select option[disabled]', text: 'this collection') 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/controllers/arclight/repositories_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::RepositoriesController do 6 | routes { Arclight::Engine.routes } 7 | before do 8 | ENV['REPOSITORY_FILE'] = 'spec/fixtures/config/repositories.yml' 9 | end 10 | 11 | describe '#index' do 12 | it 'displays the repositories' do 13 | get :index 14 | expect(response).to be_successful 15 | end 16 | 17 | it 'assigns the view variable' do 18 | get :index 19 | repos = controller.instance_variable_get(:@repositories) 20 | expect(repos).to be_an(Array) 21 | expect(repos.first).to be_an(Arclight::Repository) 22 | expect(repos.size).to eq 5 23 | end 24 | end 25 | 26 | describe '#show' do 27 | it 'looks up the repository detail page' do 28 | get :show, params: { id: 'nlm' } 29 | repo = controller.instance_variable_get(:@repository) 30 | expect(repo).to be_an(Arclight::Repository) 31 | expect(repo.slug).to eq 'nlm' 32 | collections = controller.instance_variable_get(:@collections) 33 | expect(collections.first).to be_an(SolrDocument) 34 | expect(collections.find { |c| c.id == 'aoa271' }.unitid).to eq 'MS C 271' 35 | end 36 | 37 | it 'raises RecordNotFound if non-registered slug' do 38 | expect { get :show, params: { id: 'not-registered' } }.to raise_error( 39 | ActiveRecord::RecordNotFound 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/controllers/catalog_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe CatalogController do 6 | describe 'index action customizations' do 7 | context 'any other view' do 8 | it 'starts a search_session' do 9 | allow(controller).to receive(:search_results) 10 | session[:history] = [] 11 | get :index, params: { q: 'foo', view: 'list' } 12 | expect(session[:history]).not_to be_empty 13 | end 14 | 15 | it 'stores a preferred_view' do 16 | allow(controller).to receive(:search_results) 17 | session[:preferred_view] = 'list' 18 | get :index, params: { q: 'foo', view: 'gallery' } 19 | expect(session[:preferred_view]).to eq 'gallery' 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/features/aeon_web_ead_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Aeon Web EAD Request', :js do 6 | context 'when EAD URL template is provided' do 7 | it 'creates a request link' do 8 | visit solr_document_path 'm0198-xml' 9 | 10 | within '#m0198-xml_aspace_ref11_d0s-hierarchy-item' do 11 | click_on 'Pages 1-78' 12 | end 13 | expect(page).to have_css( 14 | 'a[href*="https://sample.request.com?Action=10&Form=31&Value=http%3A%2F%2Fexample.com%2FM0198.xml', 15 | text: 'Request' 16 | ) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/features/arclight_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Arclight' do 6 | it 'an index view is present with search results' do 7 | visit search_catalog_path q: '', search_field: 'all_fields' 8 | expect(page).to have_css '.document', count: 10 9 | end 10 | 11 | describe 'eadid with a period' do 12 | it 'is visitable with a hyphen' do 13 | visit solr_document_path('m0198-xml_aspace_ref11_d0s') 14 | expect(page).to have_css 'h1', text: 'Pages 1-78' 15 | end 16 | end 17 | 18 | describe 'show page' do 19 | it 'renders metadata to meet minumum DACS requirements for a component' 20 | end 21 | 22 | describe 'Search history' do 23 | it 'successfully navigates' do 24 | visit blacklight.search_history_path 25 | expect(page).to have_css 'h2', text: 'You have no search history' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/features/autocomplete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Autocomplete', :js do 6 | context 'site-wide search form' do 7 | it 'is configured properly to allow non-prefix autocomplete' do 8 | visit '/catalog' 9 | find_by_id('q').send_keys 'by-laws' 10 | expect(page).to have_content 'amendments to articles' 11 | send_keys(:arrow_down) 12 | send_keys(:tab) 13 | expect(page).to have_field('search for', with: 'amendments to articles of incorporation and revised constitution and by-laws, 1960') 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/features/bookmarks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Bookmarks' do 6 | it 'shows bookmarks as checkboxes', :js do 7 | visit solr_document_path('aoa271_aspace_a951375d104030369a993ff943f61a77') 8 | check 'Bookmark' 9 | click_on 'Bookmarks' 10 | 11 | visit solr_document_path('aoa271_aspace_a951375d104030369a993ff943f61a77') 12 | expect(page).to have_css('input[type="checkbox"][checked]') 13 | uncheck 'In Bookmarks' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/features/collection_context_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Collection context', :js do 6 | let(:doc_id) { 'aoa271_aspace_6ea193f778e553ca9ea0d00a3e5a1891' } 7 | 8 | before do 9 | visit solr_document_path(id: doc_id) 10 | end 11 | 12 | describe 'highly nested item' do 13 | it 'highlights the correct context item' do 14 | expect(page).to have_css '.al-hierarchy-highlight', text: 'Initial Phase' 15 | end 16 | 17 | it 'siblings are not expanded' do 18 | expect(page).to have_css '.al-toggle-view-children.collapsed[href="#collapsible-hierarchy-aoa271_aspace_b70574c7229e6f237f780579cc04595d"]' 19 | end 20 | 21 | it 'direct ancestors are expanded' do 22 | expect(page).to have_css '#collapsible-hierarchy-aoa271_aspace_f934f1add34289f28bd0feb478e68275.show', visible: :visible 23 | expect(page).to have_css '#collapsible-hierarchy-aoa271_aspace_238a0567431f36f49acea49ef576d408.show', visible: :visible 24 | expect(page).to have_css '#collapsible-hierarchy-aoa271_aspace_563a320bb37d24a9e1e6f7bf95b52671.show', visible: :visible 25 | end 26 | 27 | it 'siblings above are hidden' do 28 | expect(page).to have_no_css '#aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04' 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/features/compact_search_results_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Compact Search Results' do 6 | it 'As a user I should be able to view results in a compact display' do 7 | visit root_path 8 | click_on 'Search' 9 | 10 | expect(page).to have_css('.documents-list') 11 | expect(page).to have_css('h3.index_title', text: 'Alpha Omega Alpha Archives, 1894-1992') 12 | 13 | click_on 'Compact' 14 | 15 | expect(page).to have_no_css('.documents-list') 16 | expect(page).to have_css('.documents-compact') 17 | expect(page).to have_css('article.document', count: 10) 18 | within '.document-position-3' do 19 | # Has breadcrumbs 20 | expect(page).to have_css '.breadcrumb-links a', text: /National Library of/ 21 | # Has Containers 22 | expect(page).to have_css '.al-document-container', text: 'Box 1, Folder 1' 23 | # Has Online Content Indicator 24 | expect(page).to have_css '.al-online-content-icon' 25 | # Has Bookmark Control 26 | expect(page).to have_css 'form.bookmark-toggle' 27 | end 28 | end 29 | 30 | it 'Shows highlights in compact view' do 31 | visit search_catalog_path q: 'william root', search_field: 'name' 32 | click_on 'Compact' 33 | within '.document-position-1' do 34 | within '.al-document-highlight' do 35 | expect(page).to have_css 'em', text: 'William' 36 | expect(page).to have_css 'em', text: 'Root' 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/features/document_tools_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Document Tools' do 6 | context 'in search results' do 7 | it 'renders the bookmark option for all documents' do 8 | visit search_catalog_path q: '', search_field: 'all_fields' 9 | 10 | expect(page).to have_css('article form.bookmark-toggle', count: 10) 11 | end 12 | end 13 | 14 | context 'on the record view' do 15 | before { visit solr_document_path(doc_id) } 16 | 17 | context 'for collections' do 18 | let(:doc_id) { 'aoa271' } 19 | 20 | it 'does not render the bookmark option' do 21 | expect(page).to have_no_css('.al-document-title-bar form.bookmark-toggle') 22 | end 23 | end 24 | 25 | context 'for components' do 26 | let(:doc_id) { 'm0198-xml_aspace_ref11_d0s' } 27 | 28 | pending 'renders the bookmark option' do 29 | expect(page).to have_css('.al-document-title-bar form.bookmark-toggle') 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/features/google_form_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | xdescribe 'Google Form Request', :js do 6 | context 'when container is present' do 7 | context 'repository is requestable' do 8 | it 'form is present with filled out values' do 9 | visit solr_document_path 'aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04' 10 | 11 | within 'form' do 12 | expect(page).to have_css( 13 | 'input[name="entry.1980510262"][value$="catalog/aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04"]', 14 | visible: :hidden 15 | ) 16 | expect(page).to have_css('input[name="entry.619150170"][value="Alpha Omega Alpha Archives, 1894-1992"]', 17 | visible: :hidden) 18 | expect(page).to have_css 'input[name="entry.14428541"][value="Alpha Omega Alpha"]', visible: :hidden 19 | expect(page).to have_css 'input[name="entry.996397105"][value="aoa271"]', visible: :hidden 20 | expect(page).to have_css 'input[name="entry.1125277048"][value="Box 1 Folder 1"]', visible: :hidden 21 | expect(page).to have_css 'input[name="entry.862815208"][value$="William W. Root, n.d."]', visible: :hidden 22 | expect(page).to have_button 'Request' 23 | end 24 | end 25 | 26 | context 'repository is not requestable' do 27 | it 'form is absent' do 28 | visit solr_document_path 'm0198-xml_aspace_ref14_di4' 29 | expect(page).to have_no_css 'form' 30 | end 31 | end 32 | end 33 | end 34 | 35 | context 'when container is absent' do 36 | it 'form is absent' do 37 | visit solr_document_path 'aoa271_aspace_238a0567431f36f49acea49ef576d408' 38 | expect(page).to have_no_css 'form' 39 | end 40 | end 41 | 42 | context 'in search results' do 43 | it 'shows up when item is requestable' do 44 | visit search_catalog_path q: 'alpha', search_field: 'all_fields' 45 | expect(page).to have_css 'form[action*="https://docs.google.com"]', count: 4 46 | end 47 | end 48 | 49 | context 'in collection hierarchy' do 50 | it 'shows up in hierarchy' do 51 | visit solr_document_path 'aoa271' 52 | click_on 'Contents' 53 | first('.al-toggle-view-all').click 54 | within '#collection-context' do 55 | expect(page).to have_css 'form[action*="https://docs.google.com"]', count: 22 56 | end 57 | end 58 | 59 | it 'shows up in context' do 60 | visit solr_document_path 'aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04' 61 | within '#collection-context' do 62 | expect(page).to have_css 'form[action*="https://docs.google.com"]', count: 3 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/features/highlighted_search_results_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Highlighted search results' do 6 | describe 'when querying' do 7 | describe '#all_fields' do 8 | it 'highlights the snippets' do 9 | visit search_catalog_path q: 'student life', search_field: 'all_fields' 10 | within '.document-position-1' do 11 | within '.al-document-highlight' do 12 | expect(page).to have_css 'em', text: /^student$/, count: 2 13 | expect(page).to have_css 'em', text: 'students', count: 1 14 | expect(page).to have_css 'em', text: 'life', count: 1 15 | end 16 | end 17 | end 18 | 19 | it 'does not highlight the snippets on empty query' do 20 | visit search_catalog_path q: '', search_field: 'all_fields' 21 | within '.document-position-1' do 22 | expect(page).to have_no_css '.al-document-highlight' 23 | end 24 | end 25 | end 26 | 27 | describe '#name' do 28 | it 'highlights the snippets' do 29 | visit search_catalog_path q: 'william root', search_field: 'name' 30 | within '.document-position-1' do 31 | within '.al-document-highlight' do 32 | expect(page).to have_css 'em', text: 'William', count: 1 33 | expect(page).to have_css 'em', text: 'Root', count: 2 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/features/item_breadcrumbs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Item breadcrumb' do 6 | it 'results page shows navigable breadcrumbs' do 7 | visit search_catalog_path q: 'expansion', search_field: 'all_fields' 8 | document = page.all('article').find do |doc| 9 | doc.all('h3 a', text: 'Phase II: Expansion').present? 10 | end 11 | 12 | within document do 13 | within '.breadcrumb-links' do 14 | expect(page).to have_link 'National Library of Medicine. History of Medicine Division' 15 | expect(page).to have_link 'Alpha Omega Alpha Archives' 16 | expect(page).to have_link 'Series I: Administrative Records' 17 | expect(page).to have_link 'Reports' 18 | expect(page).to have_link 'Expansion Plan' 19 | click_on 'Expansion Plan' 20 | end 21 | end 22 | expect(page).to have_css 'h1', text: 'Expansion Plan' 23 | end 24 | 25 | it 'show page breadcrumbs' do 26 | visit solr_document_path id: 'aoa271_aspace_e8755922a9336970292ca817983e7139' 27 | expect(page).to have_css 'li.breadcrumb-item a', count: 5 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/features/many_component_ead_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Many component EAD' do 6 | describe 'hierarchy', :js do 7 | before { visit solr_document_path 'lc0100' } 8 | 9 | it 'includes all components' do 10 | within '#collection-context' do 11 | expect(page).to have_css 'li.al-collection-context', count: 202 12 | end 13 | end 14 | 15 | it 'includes all children' do 16 | within '#collection-context' do 17 | click_on 'View' 18 | click_on 'Expand' 19 | within '#collapsible-hierarchy-lc0100_aspace_327a75c226d44aa1a769edb4d2f13c6e' do 20 | expect(page).to have_css 'li.al-collection-context', count: 202 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/features/masthead_links_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Masthead links' do 6 | describe 'collection link' do 7 | it 'is not active when collection search is not activated' do 8 | visit search_catalog_path q: 'a brief', search_field: 'all_fields' 9 | within '.al-masthead' do 10 | expect(page).to have_no_css 'li.nav-item.active', text: 'Collections' 11 | end 12 | end 13 | 14 | it 'is active when collection search is activated' do 15 | visit search_catalog_path f: { level: ['Collection'] }, search_field: 'all_fields' 16 | within '.al-masthead' do 17 | expect(page).to have_css 'li.nav-item.active', text: 'Collections' 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/features/online_content_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Online Content' do 6 | let(:doc_id) { 'aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04' } 7 | 8 | before { visit solr_document_path(id: doc_id) } 9 | 10 | describe 'Viewer' do 11 | context 'embedded content' do 12 | let(:doc_id) { 'a0011-xml_aspace_ref6_lx4' } 13 | 14 | it 'renders digital object viewer initialization markup', :js do 15 | expect(page).to have_css( 16 | '.al-oembed-viewer[data-arclight-oembed-url-value="http://purl.stanford.edu/kc844kt2526"]', 17 | visible: :all 18 | ) 19 | end 20 | end 21 | 22 | context 'non-embeddable content' do 23 | let(:doc_id) { 'aoa271_aspace_843e8f9f22bac69872d0802d6fffbb04' } 24 | 25 | it 'renders a list of links', :js do 26 | expect(page).to have_link 'Folder of digitized stuff' 27 | expect(page).to have_link 'Letter from Christian B. Anfinsen' 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/features/repositories_page_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Repositores Page' do 6 | it 'is navigabe from the home page' do 7 | visit '/' 8 | 9 | within '.al-repositories' do 10 | expect(page).to have_css('.al-repository h2 a', text: 'My Repository') 11 | end 12 | 13 | expect(page.body).to include('Repositories - Arclight') 14 | end 15 | 16 | describe 'Repostory Show Page' do 17 | it 'is navigable form the Repositories page' do 18 | visit '/repositories' 19 | 20 | click_on 'Stanford University Libraries. Special Collections and University Archives' 21 | 22 | expect(page).to have_css('h2', text: 'Our Collections') 23 | end 24 | 25 | it 'links to all the repositories collections' do 26 | visit '/repositories' 27 | 28 | click_on 'Stanford University Libraries. Special Collections and University Archives' 29 | 30 | click_on 'View all of our collections' 31 | 32 | expect(page).to have_css('h2', text: 'Search Results') 33 | end 34 | 35 | it 'does not link the same page in the repository card header' do 36 | visit '/repositories' 37 | 38 | click_on 'Stanford University Libraries. Special Collections and University Archives' 39 | 40 | within '.al-repository' do 41 | expect(page).to have_no_css( 42 | 'h2 a', 43 | text: 'Stanford University Libraries. Special Collections and University Archives' 44 | ) 45 | end 46 | end 47 | 48 | it 'has a title title starting with the repository name' do 49 | visit '/repositories' 50 | 51 | click_on 'Stanford University Libraries. Special Collections and University Archives' 52 | 53 | expect(page.body).to include('Stanford University Libraries. Special Collections and University Archives - Arclight') 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/features/search_breadcrumb_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Search Breadcrumb' do 6 | context 'on regular search results' do 7 | it do 8 | visit search_catalog_path q: 'a brief', search_field: 'all_fields' 9 | within '.al-search-breadcrumb' do 10 | expect(page).to have_link 'Home' 11 | expect(page).to have_content 'Search results' 12 | end 13 | end 14 | 15 | it do 16 | visit search_catalog_path f: { level: ['Collection'] }, search_field: 'all_fields' 17 | within '.al-search-breadcrumb' do 18 | expect(page).to have_link 'Home' 19 | expect(page).to have_content 'Collections' 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/config/downloads.yml: -------------------------------------------------------------------------------- 1 | # 2 | # downloads.yml - Use the EAD's as the primary key and 3 | # provide the PDF and/or EAD (.xml) links. The 4 | # size value should be a String (shown as-is) or 5 | # the number of bytes in the download. 6 | # 7 | # 8 | MS C 271: 9 | pdf: 10 | href: 'http://example.com/MS+C+271.pdf' 11 | size: '1.23MB' 12 | ead: 13 | href: 'http://example.com/MS+C+271.xml' 14 | size: 123456 15 | default: 16 | # disabled: true 17 | pdf: 18 | template: 'http://example.com/%{unitid}.pdf' 19 | # size_accessor: finding_aid_size 20 | ead: 21 | template: 'http://example.com/%{unitid}.xml' 22 | -------------------------------------------------------------------------------- /spec/i18n_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'i18n/tasks' 4 | 5 | RSpec.describe 'I18n', type: :feature do 6 | let(:i18n) { I18n::Tasks::BaseTask.new } 7 | let(:missing_keys) { i18n.missing_keys } 8 | let(:unused_keys) { i18n.unused_keys } 9 | 10 | it 'does not have missing keys' do 11 | expect(missing_keys).to be_empty, 12 | "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" 13 | end 14 | 15 | it 'does not have unused keys' do 16 | expect(unused_keys).to be_empty, 17 | "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" 18 | end 19 | 20 | it 'files are normalized' do 21 | non_normalized = i18n.non_normalized_paths 22 | error_message = "The following files need to be normalized:\n" \ 23 | "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \ 24 | 'Please run `i18n-tasks normalize` to fix' 25 | expect(non_normalized).to be_empty, error_message 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/arclight/digital_object_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::DigitalObject do 6 | subject(:instance) do 7 | described_class.new(label: 'An object label', href: 'https://example.com/an-object-href') 8 | end 9 | 10 | describe 'label' do 11 | let(:empty_label) do 12 | described_class.new(label: '', href: 'https://example.com/an-object-href') 13 | end 14 | 15 | it 'uses href if label is blank' do 16 | expect(empty_label.href).to eq 'https://example.com/an-object-href' 17 | end 18 | end 19 | 20 | describe '#to_json' do 21 | it 'returns a json serialization of the object' do 22 | json = JSON.parse(instance.to_json) 23 | expect(json).to be_a Hash 24 | expect(json['label']).to eq 'An object label' 25 | end 26 | end 27 | 28 | describe "#{described_class}.from_json" do 29 | it 'returns an instance of the class given the parsed json' do 30 | deserialized = described_class.from_json(instance.to_json) 31 | expect(deserialized).to be_a described_class 32 | expect(deserialized.label).to eq 'An object label' 33 | end 34 | end 35 | 36 | describe '==' do 37 | let(:dissimilar) do 38 | described_class.new(label: 'A different label', href: 'https://example.com/an-object-href') 39 | end 40 | 41 | let(:similar) do 42 | described_class.new(label: 'An object label', href: 'https://example.com/an-object-href') 43 | end 44 | 45 | it 'is true when href and label are similar' do 46 | expect(instance).not_to eq dissimilar 47 | end 48 | 49 | it 'is false when objects have dissimilar labels' do 50 | expect(instance).to eq similar 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/arclight/level_label_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'arclight/level_label' 5 | 6 | RSpec.describe Arclight::LevelLabel do 7 | subject(:level_label) { described_class.new(level, other_level).to_s } 8 | 9 | context 'when level is collection' do 10 | let(:level) { 'collection' } 11 | let(:other_level) { nil } 12 | 13 | it 'capitalizes it' do 14 | expect(level_label).to eq 'Collection' 15 | end 16 | end 17 | 18 | context 'when level has a custom human-readable value defined' do 19 | let(:level) { 'recordgrp' } 20 | let(:other_level) { nil } 21 | 22 | it 'uses the human-readable form' do 23 | expect(level_label).to eq 'Record Group' 24 | end 25 | end 26 | 27 | context 'when level is otherlevel & one is specified' do 28 | let(:level) { 'otherlevel' } 29 | let(:other_level) { 'binder' } 30 | 31 | it 'capitalizes specified value' do 32 | expect(level_label).to eq 'Binder' 33 | end 34 | end 35 | 36 | context 'when level is otherlevel without one specified' do 37 | let(:level) { 'otherlevel' } 38 | let(:other_level) { nil } 39 | 40 | it 'uses Other' do 41 | expect(level_label).to eq 'Other' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/lib/arclight/missing_id_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'arclight/missing_id_strategy' 5 | 6 | RSpec.describe Arclight::MissingIdStrategy do 7 | subject(:strategy) { described_class.selected } 8 | 9 | it 'defaults to Arclight::HashAbsoluteXpath' do 10 | expect(strategy).to eq Arclight::HashAbsoluteXpath 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/lib/arclight/normalized_date_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::NormalizedDate do 6 | subject(:normalized_date) { described_class.new(date_inclusive, date_bulk, date_other).to_s } 7 | 8 | let(:date_inclusive) { ['1990-2000'] } 9 | let(:date_bulk) { '1999-2005' } 10 | let(:date_other) { 'Undated' } 11 | 12 | context 'under normal conditions' do 13 | it 'joins dates' do 14 | expect(normalized_date).to eq '1990-2000, Undated, bulk 1999-2005' 15 | end 16 | 17 | context 'multiple normalized dates' do 18 | let(:date_inclusive) { %w[1990 1992] } 19 | 20 | it 'are joined w/ a comma' do 21 | expect(normalized_date).to eq '1990, 1992, Undated, bulk 1999-2005' 22 | end 23 | end 24 | end 25 | 26 | context 'with special case dates' do 27 | # NOTE: This test is the only place where the code that exercises this is routable 28 | # This has to be a multidimensional array, and the resulting XML nodes sent in are always flat 29 | context 'multiples' do 30 | let(:date_inclusive) { [%w[1990-2000 2001-2002 2004]] } 31 | let(:date_bulk) { '1990-2004' } 32 | 33 | it 'uses compressed joined years' do 34 | expect(normalized_date).to eq '1990-2002, 2004, Undated, bulk 1990-2004' 35 | end 36 | end 37 | 38 | context 'undated' do 39 | let(:date_bulk) { 'n.d.' } 40 | 41 | it 'do not normalized term "undated"' do 42 | expect(normalized_date).to eq '1990-2000, Undated, bulk n.d.' 43 | end 44 | end 45 | 46 | context 'circa' do 47 | let(:date_bulk) { 'c.1995' } 48 | 49 | it 'do not normalized term "circa"' do 50 | expect(normalized_date).to eq '1990-2000, Undated, bulk c.1995' 51 | end 52 | end 53 | 54 | context 'no bulk' do 55 | let(:date_bulk) { nil } 56 | let(:date_other) { nil } 57 | 58 | it 'uses inclusive date only' do 59 | expect(normalized_date).to eq '1990-2000' 60 | end 61 | end 62 | 63 | context 'no inclusive or bulk but other' do 64 | let(:date_inclusive) { nil } 65 | let(:date_bulk) { nil } 66 | let(:date_other) { 'n.d.' } 67 | 68 | it 'uses other' do 69 | expect(normalized_date).to eq 'n.d.' 70 | end 71 | end 72 | 73 | context 'no inclusive but bulk' do 74 | let(:date_inclusive) { nil } 75 | 76 | it 'uses other and bulk' do 77 | expect(normalized_date).to eq 'Undated, bulk 1999-2005' 78 | end 79 | end 80 | 81 | context 'no information' do 82 | let(:date_inclusive) { nil } 83 | let(:date_bulk) { nil } 84 | let(:date_other) { nil } 85 | 86 | it 'does not know what to do' do 87 | expect(normalized_date).to be_nil 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/lib/arclight/normalized_id_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::NormalizedId do 6 | subject(:normalized_id) { described_class.new(id).to_s } 7 | 8 | context 'when the id as a period in it' do 9 | let(:id) { 'abc123.xml' } 10 | 11 | it 'replaces it with a hyphen' do 12 | expect(normalized_id).to eq 'abc123-xml' 13 | end 14 | end 15 | 16 | context 'when the id has extra space in it' do 17 | let(:id) { ' abc123 ' } 18 | 19 | it 'is stripped' do 20 | expect(normalized_id).to eq 'abc123' 21 | end 22 | end 23 | 24 | context 'when the id is nil' do 25 | let(:id) { nil } 26 | 27 | it do 28 | expect { normalized_id }.to raise_error( 29 | Arclight::Exceptions::IDNotFound, 30 | 'id must be present for all documents and components' 31 | ) 32 | end 33 | end 34 | 35 | context 'when additional keyword arguments are supplied' do 36 | subject(:normalized_id) do 37 | described_class.new('abc123.xml', unitid: 'abc-123', title: 'a title', repository: 'repo').to_s 38 | end 39 | 40 | it 'accepts the additional arguments without changing the output' do 41 | expect(normalized_id).to eq 'abc123-xml' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/models/arclight/requests/aeon_external_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::Requests::AeonExternalRequest do 6 | subject(:valid_object) { described_class.new(document, {}) } 7 | 8 | let(:config_hash) do 9 | { 10 | 'request_url' => 'https://example.com/aeon/aeon.dll', 11 | 'request_mappings' => { 12 | 'url_params' => { 13 | 'Action' => 11, 14 | 'Type' => 200 15 | }, 16 | 'static' => { 17 | 'SystemId' => 'ArcLight', 18 | 'ItemInfo1' => 'manuscript' 19 | }, 20 | 'accessor' => { 21 | 'ItemTitle' => 'collection_name' 22 | } 23 | } 24 | } 25 | end 26 | 27 | let(:config) do 28 | instance_double Arclight::Repository, 29 | request_config_for_type: config_hash 30 | end 31 | let(:document) do 32 | instance_double SolrDocument, 33 | repository_config: config, 34 | collection_name: 'Cool Document' 35 | end 36 | 37 | describe '#url' do 38 | it 'constructs from the repository config' do 39 | expect(valid_object.url).to eq 'https://example.com/aeon/aeon.dll?Action=11&Type=200' 40 | end 41 | end 42 | 43 | describe '#form_mapping' do 44 | it 'compiles from the repository config' do 45 | expect(valid_object.form_mapping).to eq('SystemId' => 'ArcLight', 46 | 'ItemInfo1' => 'manuscript', 47 | 'ItemTitle' => 'Cool Document') 48 | end 49 | end 50 | 51 | describe '#static_mappings' do 52 | it 'pulls from the repository config' do 53 | expect(valid_object.static_mappings).to eq('SystemId' => 'ArcLight', 54 | 'ItemInfo1' => 'manuscript') 55 | end 56 | end 57 | 58 | describe '#dynamic_mappings' do 59 | it 'pulls from the repository config' do 60 | expect(valid_object.dynamic_mappings).to eq('ItemTitle' => 'Cool Document') 61 | end 62 | end 63 | 64 | describe '#url_params' do 65 | it 'constructs from the repository config' do 66 | expect(valid_object.url_params).to eq('Action=11&Type=200') 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/models/arclight/requests/aeon_web_ead_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::Requests::AeonWebEad do 6 | subject(:valid_object) { described_class.new(document, 'http://example.com/sample.xml') } 7 | 8 | let(:config) do 9 | instance_double Arclight::Repository, 10 | request_config_for_type: { 11 | request_url: 'https://sample.request.com', 12 | request_mappings: 'Action=10&Form=31&Value=ead_url' 13 | }.with_indifferent_access 14 | end 15 | let(:document) { instance_double SolrDocument, repository_config: config } 16 | 17 | describe '#request_url' do 18 | it 'returns from the repository config' do 19 | expect(valid_object.request_url).to eq 'https://sample.request.com' 20 | end 21 | end 22 | 23 | describe '#url' do 24 | it 'constructs a url with params' do 25 | expect(valid_object.url).to eq 'https://sample.request.com?Action=10&Form=31&Value=http%3A%2F%2Fexample.com%2Fsample.xml' 26 | end 27 | end 28 | 29 | describe '#form_mapping' do 30 | subject(:form_mapping) { valid_object.form_mapping } 31 | 32 | it 'converts string from config to hash' do 33 | expect(form_mapping).to be_an Hash 34 | end 35 | 36 | it 'has valid key/value pairs' do 37 | expect(form_mapping).to include('Action' => '10', 'Form' => '31', 'Value' => 'http://example.com/sample.xml') 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/models/arclight/requests/google_form_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Arclight::Requests::GoogleForm do 6 | subject(:valid_object) { described_class.new(document, presenter, '') } 7 | 8 | let(:config) do 9 | instance_double Arclight::Repository, 10 | request_config_for_type: { request_url: 'https://docs.google.com/abc123', 11 | request_mappings: 'collection_name=abc&eadid=123' }.with_indifferent_access 12 | end 13 | let(:document) { instance_double SolrDocument, repository_config: config } 14 | let(:presenter) { instance_double Arclight::ShowPresenter, heading: 'Indiana Jones and the Last Crusade' } 15 | 16 | describe 'API' do 17 | it 'responds to needed methods for mapping' do 18 | %i[collection_name eadid containers title].each do |method| 19 | expect(valid_object).to respond_to(method) 20 | end 21 | end 22 | end 23 | 24 | describe '#url' do 25 | it 'returns from the repository config' do 26 | expect(valid_object.url).to eq 'https://docs.google.com/abc123' 27 | end 28 | end 29 | 30 | describe '#form_mapping' do 31 | subject(:form_mapping) { valid_object.form_mapping } 32 | 33 | it 'converts string from config to hash' do 34 | expect(form_mapping).to be_an Hash 35 | end 36 | 37 | it 'has valid key/value pairs' do 38 | expect(form_mapping).to include('collection_name' => 'abc', 'eadid' => '123') 39 | end 40 | end 41 | 42 | describe '#title' do 43 | it 'gets the heading from the presenter' do 44 | expect(valid_object.title).to eq 'Indiana Jones and the Last Crusade' 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/models/concerns/arclight/search_behavior_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Arclight::SearchBehavior do 6 | subject(:search_builder_instance) { search_builder.with(user_params) } 7 | 8 | let(:user_params) { {} } 9 | let(:solr_params) { {} } 10 | let(:context) { CatalogController.new } 11 | let(:search_builder_class) do 12 | Class.new(Blacklight::SearchBuilder).tap do |klass| 13 | include Blacklight::Solr::SearchBuilderBehavior 14 | klass.include(described_class) 15 | end 16 | end 17 | let(:search_builder) { search_builder_class.new(context) } 18 | 19 | describe '#add_highlighting' do 20 | it 'enables highlighting' do 21 | expect(search_builder_instance.add_highlighting(solr_params)).to include('hl' => true) 22 | end 23 | end 24 | 25 | describe '#add_grouping' do 26 | context 'when group is selected' do 27 | let(:user_params) { { group: 'true' } } 28 | 29 | it 'adds grouping params' do 30 | expect(search_builder_instance.add_grouping(solr_params)).to include(Arclight::Engine.config.catalog_controller_group_query_params) 31 | end 32 | end 33 | 34 | context 'when group is not selected' do 35 | it 'enables highlighting' do 36 | expect(search_builder_instance.add_grouping(solr_params)).not_to include( 37 | group: true 38 | ) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/presenters/arclight/index_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Arclight::IndexPresenter, type: :presenter do 6 | subject { presenter } 7 | 8 | let(:view_context) { ActionView::Base.new(nil, {}, nil) } 9 | 10 | let(:config) { Blacklight::Configuration.new } 11 | 12 | let(:presenter) { described_class.new(document, view_context, config) } 13 | 14 | let(:document) do 15 | SolrDocument.new(id: 1, 16 | 'normalized_title_ssm' => 'My Title, 1900-2000') 17 | end 18 | 19 | before do 20 | config.index.title_field = :normalized_title_ssm 21 | end 22 | 23 | describe '#label' do 24 | it 'uses normalized title' do 25 | expect(presenter.label).to eq 'My Title, 1900-2000' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/presenters/arclight/show_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Arclight::ShowPresenter, type: :presenter do 6 | subject { presenter } 7 | 8 | let(:view_context) { ActionView::Base.new(nil, {}, nil) } 9 | let(:config) { Blacklight::Configuration.new } 10 | 11 | let(:presenter) { described_class.new(document, view_context, config) } 12 | 13 | let(:document) do 14 | SolrDocument.new(id: 1, 15 | 'normalized_title_ssm' => ['My Title, 1900-2000']) 16 | end 17 | 18 | before do 19 | config.show.title_field = :normalized_title_ssm 20 | allow(view_context).to receive(:action_name).and_return(:show) 21 | end 22 | 23 | describe '#heading' do 24 | it 'uses normalized title' do 25 | expect(presenter.heading).to eq 'My Title, 1900-2000' 26 | end 27 | end 28 | 29 | describe '#with_field_group' do 30 | it 'is nil when none is set' do 31 | expect(presenter.send(:field_group)).to be_nil 32 | end 33 | 34 | it 'sets the field group based on the given field accessor (and returns the presenter)' do 35 | returned_presenter = presenter.with_field_group('a_group') 36 | expect(returned_presenter).to be_a Arclight::ShowPresenter 37 | expect(returned_presenter.send(:field_group)).to eq 'a_group' 38 | end 39 | end 40 | 41 | describe '#field_config' do 42 | it 'returns a field configuration (NullField in this context)' do 43 | if Blacklight::VERSION > '8' 44 | expect(presenter.send(:field_config, 'some_field')).to be_a Blacklight::Configuration::NullDisplayField 45 | else 46 | expect(presenter.send(:field_config, 'some_field')).to be_a Blacklight::Configuration::NullField 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/routing/arclight/repositories_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Vanity repositories routes' do 6 | routes { Arclight::Engine.routes } 7 | context 'repositories' do 8 | it '#index' do 9 | expect(get: '/repositories').to route_to( 10 | controller: 'arclight/repositories', 11 | action: 'index' 12 | ) 13 | end 14 | 15 | it '#show' do 16 | expect(get: '/repositories/my-slug').to route_to( 17 | controller: 'arclight/repositories', 18 | action: 'show', 19 | id: 'my-slug' 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/routing/collections_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Vanity collections route' do 6 | routes { Arclight::Engine.routes } 7 | it 'routes to collection search' do 8 | expect(get: '/collections').to route_to( 9 | 'f' => { 'level' => ['Collection'] }, 10 | controller: 'catalog', 11 | action: 'index' 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/controller_level_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ControllerLevelHelpers 4 | def search_state 5 | @search_state ||= Blacklight::SearchState.new(params, blacklight_config, controller) 6 | end 7 | 8 | def blacklight_configuration_context 9 | @blacklight_configuration_context ||= Blacklight::Configuration::Context.new(controller) 10 | end 11 | 12 | delegate :blacklight_config, to: :CatalogController 13 | end 14 | -------------------------------------------------------------------------------- /spec/test_app_templates/Gemfile.extra: -------------------------------------------------------------------------------- 1 | if ENV['BLACKLIGHT_VERSION'] == 'github' 2 | gem 'blacklight', github: 'projectblacklight/blacklight' 3 | elsif ENV['BLACKLIGHT_VERSION'] && !ENV['BLACKLIGHT_VERSION'].empty? 4 | gem 'blacklight', ENV['BLACKLIGHT_VERSION'] 5 | end 6 | -------------------------------------------------------------------------------- /spec/test_app_templates/lib/generators/test_app_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | 5 | # :nodoc: 6 | class TestAppGenerator < Rails::Generators::Base 7 | source_root './spec/test_app_templates' 8 | 9 | # if you need to generate any additional configuration 10 | # into the test app, this generator will be run immediately 11 | # after setting up the application 12 | 13 | def add_gems 14 | Bundler.with_clean_env do 15 | run 'bundle install' 16 | end 17 | end 18 | 19 | # This makes the assets available in the test app so that changes made in 20 | # local development can be picked up automatically 21 | def link_frontend 22 | run 'yarn link' 23 | end 24 | 25 | def run_blacklight_generator 26 | say_status('warning', 'GENERATING BL', :yellow) 27 | 28 | generate 'blacklight:install', '--devise' 29 | end 30 | 31 | def install_engine 32 | generate 'arclight:install --test' 33 | end 34 | 35 | def add_test_locales 36 | initializer 'test_locale_configuration.rb' do 37 | 'Blacklight::LocalePicker::Engine.config.available_locales = [:en, :es]' 38 | end 39 | end 40 | 41 | def add_custom_download 42 | config_download = <<~YML 43 | M0198: 44 | disabled: false 45 | ead: 46 | template: 'http://example.com/%{collection_unitid}.xml' 47 | YML 48 | inject_into_file 'config/downloads.yml', config_download, after: "template: 'http://example.com/%{unitid}.xml'\n" 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/views/_requests.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'arclight/_requests' do 6 | let(:document) { SolrDocument.new(id: 'abc123') } 7 | let(:config) { instance_double(Arclight::Repository) } 8 | let(:blacklight_config) { Blacklight::Configuration.new } 9 | 10 | before do 11 | allow(document).to receive_messages(repository_config: config, requestable?: true) 12 | allow(view).to receive_messages(blacklight_config: blacklight_config, action_name: 'show', document: document) 13 | end 14 | 15 | context 'with EAD documents which require Aeon requests' do 16 | let(:document_downloads) { instance_double(Arclight::DocumentDownloads::File) } 17 | 18 | before do 19 | allow(document_downloads).to receive(:href).and_return('https://sample.request.com') 20 | allow(document).to receive(:ead_file).and_return(document_downloads) 21 | allow(config).to receive_messages(available_request_types: [:aeon_web_ead], 22 | request_config_for_type: { 'request_url' => 'https://sample.request.com', 23 | 'request_mappings' => 'Action=10&Form=31&Value=ead_url' }) 24 | 25 | render 26 | end 27 | 28 | it 'renders links to the Aeon request form' do 29 | expect(rendered).to have_css '.al-request' 30 | expect(rendered).to have_css '.al-request a[href^="https://sample.request.com"]' 31 | end 32 | end 33 | 34 | context 'with EAD documents which require external Aeon requests' do 35 | let(:config_hash) do 36 | { 37 | 'request_url' => 'https://example.com/aeon/aeon.dll', 38 | 'request_mappings' => { 39 | 'url_params' => { 40 | 'Action' => 11, 41 | 'Type' => 200 42 | }, 43 | 'static' => { 44 | 'SystemId' => 'ArcLight', 45 | 'ItemInfo1' => 'manuscript' 46 | }, 47 | 'accessor' => { 48 | 'ItemTitle' => 'collection_name' 49 | } 50 | } 51 | } 52 | end 53 | 54 | before do 55 | allow(config).to receive_messages(available_request_types: [:aeon_external_request_endpoint], request_config_for_type: config_hash) 56 | render 57 | end 58 | 59 | it 'renders links to the external Aeon request endpoint' do 60 | expect(rendered).to have_css '.al-request-form' 61 | expect(rendered).to have_css '.al-request-form[action^="https://example.com/aeon/aeon.dll"]' 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/views/repositories/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'arclight/repositories/show' do 6 | let(:test_data) { Arclight::Repository.find_by(slug: 'sample') } 7 | 8 | before do 9 | ENV['REPOSITORY_FILE'] = 'spec/fixtures/config/repositories.yml' 10 | assign(:repository, test_data) 11 | assign(:collections, []) 12 | allow(view).to receive(:search_action_url).and_return('/') 13 | end 14 | 15 | context 'renders a repository detail page' do 16 | before { render } 17 | 18 | it 'has the repository card' do 19 | expect(rendered).to have_css('.al-repository h2', text: /My Repository/) 20 | end 21 | 22 | it 'has breadcrumbs' do 23 | expect(rendered).to have_css('.al-search-breadcrumb', text: /My Repository/) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /tasks/arclight.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solr_wrapper' 4 | require 'engine_cart/rake_task' 5 | require 'rspec/core/rake_task' 6 | require 'arclight' 7 | 8 | class DependencyNotInstalled < StandardError; end 9 | 10 | # Build with our opinionated defaults if none are provided. 11 | rails_options = ENV.fetch('ENGINE_CART_RAILS_OPTIONS', '') 12 | rails_options = "#{rails_options} -a propshaft" unless rails_options.match?(/-a\s|--asset-pipeline/) 13 | rails_options = "#{rails_options} -j importmap" unless rails_options.match?(/-j\s|--javascript/) 14 | rails_options = "#{rails_options} --css bootstrap" unless rails_options.match?(/--css/) 15 | ENV['ENGINE_CART_RAILS_OPTIONS'] = rails_options 16 | 17 | desc 'Run test suite' 18 | task ci: %w[arclight:generate] do 19 | SolrWrapper.wrap do |solr| 20 | solr.with_collection do 21 | Rake::Task['arclight:seed'].invoke 22 | within_test_app do 23 | system 'bin/rake spec:prepare' 24 | end 25 | Rake::Task['spec'].invoke 26 | end 27 | end 28 | end 29 | 30 | desc 'Run Eslint' 31 | task :eslint do 32 | raise DependencyNotInstalled, 'ESLint not found. Please run yarn install.' unless File.exist?('./node_modules/.bin/eslint') 33 | 34 | exit 1 unless system './node_modules/.bin/eslint app/assets/javascripts' 35 | end 36 | 37 | namespace :arclight do 38 | desc 'Generate a test application' 39 | task generate: %w[engine_cart:generate] 40 | 41 | desc 'Run Solr and Blacklight for interactive development' 42 | task :server, %i[rails_server_args] do |_t, args| 43 | if File.exist? EngineCart.destination 44 | within_test_app do 45 | system 'bundle update' 46 | end 47 | else 48 | Rake::Task['engine_cart:generate'].invoke 49 | end 50 | 51 | print 'Starting Solr...' 52 | SolrWrapper.wrap do |solr| 53 | puts 'done.' 54 | solr.with_collection do 55 | Rake::Task['arclight:seed'].invoke 56 | within_test_app do 57 | system "bin/dev #{args[:rails_server_args]}" 58 | end 59 | end 60 | end 61 | end 62 | 63 | desc 'Seed fixture data to Solr' 64 | task :seed do 65 | puts 'Seeding index with data from spec/fixtures/ead...' 66 | Dir.glob('spec/fixtures/ead/*').each do |dir| 67 | next unless File.directory?(dir) 68 | 69 | within_test_app do 70 | # Sets the REPOSITORY_ID to the name of the file's containing directory 71 | system("REPOSITORY_ID=#{File.basename(dir)} " \ 72 | "REPOSITORY_FILE=#{Arclight::Engine.root}/spec/fixtures/config/repositories.yml " \ 73 | "DIR=#{Arclight::Engine.root}/#{dir} " \ 74 | 'SOLR_URL=http://127.0.0.1:8983/solr/blacklight-core ' \ 75 | 'rake arclight:index_dir') 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem 'arclight' 4 | 5 | after_bundle do 6 | generate 'blacklight:install', '--devise' 7 | generate 'arclight:install', '-f' 8 | 9 | rake 'db:migrate' 10 | end 11 | --------------------------------------------------------------------------------