From f122635caf4d720598e33f522f9ca81b258d0449 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Mon, 10 Aug 2026 16:14:14 -0400 Subject: [PATCH 01/16] Upgrade administrate to 1.0.0 (#14) * Clear administrate 1.0 upgrade blockers Vendors an in-house ActsAsTaggableField replacing the unmaintained administrate-field-acts_as_taggable gem (last released 2021, hard-pins administrate < 1.0.0), preserving the same selectize-based tag UI. Bumps administrate-field-jsonb to 0.4.8, which allows administrate < 2.0 instead of < 1.0.0. Also fixes two unrelated pre-existing bugs found while adding test coverage: reversed accessible_action? arguments in the media show and mentions edit views, and a flat-vs-nested iteration bug in the mentions form that raised NoMethodError on new/edit. Media's show page also called a nonexistent Medium#url; it now prefers the attached ActiveStorage image, falling back to the link column. Co-Authored-By: Claude Sonnet 5 * Upgrade administrate to 1.0.0 Restores sprockets-rails/sassc-rails as direct dependencies - administrate 1.0 dropped them transitively (it now bundles its own precompiled assets), but the app's own asset pipeline (app/assets/config/manifest.js) relies on them directly, and administrate-field-jsonb's stylesheet chain needs real Sass compilation. Drops the now-dead `//= require selectize` manifest line; administrate 1.0 bundles its own copy inside its single built JS file. Also fixes the admin system-test harness: it authenticated by embedding credentials in the visited URL (this API-only app has no session-based admin login), which worked fine until Turbo - newly bundled in 1.0 - started calling history.replaceState on every page load. Chrome rejects replaceState targets carrying userinfo, and since Administrate's JS is one bundled IIFE, that uncaught exception aborted everything after it, including selectize's own setup. Real admin users never hit this (their location.href never carries credentials - Chrome only uses them for the auth handshake), but it broke the tests. Switched to Selenium's CDP-based driver.register(username:, password:) so system specs authenticate without touching the URL. Verified manually in a real browser in addition to the full suite (322 examples, 0 failures). Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- Gemfile | 11 +++- Gemfile.lock | 34 +++++------ app/assets/config/manifest.js | 4 +- .../javascripts/acts_as_taggable_field.js | 20 +++++++ .../stylesheets/acts_as_taggable_field.css | 9 +++ app/dashboards/mention_dashboard.rb | 2 +- app/fields/acts_as_taggable_field.rb | 56 +++++++++++++++++ app/views/admin/media/show.html.erb | 5 +- app/views/admin/mentions/_form.html.erb | 10 ++-- app/views/admin/mentions/edit.html.erb | 2 +- .../acts_as_taggable_field/_form.html.erb | 6 ++ .../acts_as_taggable_field/_index.html.erb | 1 + .../acts_as_taggable_field/_show.html.erb | 5 ++ spec/fields/acts_as_taggable_field_spec.rb | 60 +++++++++++++++++++ spec/requests/admin/about_pages_spec.rb | 4 +- spec/requests/admin/media_spec.rb | 7 --- spec/requests/admin/mentions_spec.rb | 12 ---- spec/support/capybara.rb | 11 ++++ spec/system/admin/letters_spec.rb | 13 ++-- 19 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/acts_as_taggable_field.js create mode 100644 app/assets/stylesheets/acts_as_taggable_field.css create mode 100644 app/fields/acts_as_taggable_field.rb create mode 100644 app/views/fields/acts_as_taggable_field/_form.html.erb create mode 100644 app/views/fields/acts_as_taggable_field/_index.html.erb create mode 100644 app/views/fields/acts_as_taggable_field/_show.html.erb create mode 100644 spec/fields/acts_as_taggable_field_spec.rb diff --git a/Gemfile b/Gemfile index b667cf6..d293c44 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,12 @@ gem 'administrate' gem 'administrate-field-jsonb' gem 'trix-rails', require: 'trix' +# administrate < 1.0 pulled these in transitively via sassc-rails; administrate 1.0 +# bundles its own assets and dropped that dependency, but the app's own asset +# pipeline (app/assets/config/manifest.js) still relies on Sprockets/Sass directly. +gem 'sprockets-rails' +gem 'sassc-rails' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem 'debug', platforms: %i[mri mingw x64_mingw] @@ -67,6 +73,9 @@ group :test do # chromedriver binary itself (Selenium Manager) - no separate webdriver gem needed. gem 'capybara' gem 'selenium-webdriver' + # CDP support, needed for Selenium's driver.register(username:, password:) - used to + # authenticate admin system specs without embedding credentials in the visited URL. + gem 'selenium-devtools' end gem 'pundit', '~> 2.2' @@ -79,8 +88,6 @@ gem 'rspec_api_documentation', '~> 6.1' gem 'acts-as-taggable-on', '~> 11.0.0' -gem 'administrate-field-acts_as_taggable' - gem 'administrate-field-list', '~> 0.0.6' gem 'httparty', '~> 0.20.0' diff --git a/Gemfile.lock b/Gemfile.lock index 609e992..2fb177b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,22 +78,16 @@ GEM zeitwerk (>= 2.4, < 3.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) - administrate (0.20.1) - actionpack (>= 6.0, < 8.0) - actionview (>= 6.0, < 8.0) - activerecord (>= 6.0, < 8.0) - jquery-rails (~> 4.6.0) + administrate (1.0.0) + actionpack (>= 6.0, < 9.0) + actionview (>= 6.0, < 9.0) + activerecord (>= 6.0, < 9.0) kaminari (~> 1.2.2) - sassc-rails (~> 2.1) - selectize-rails (~> 0.6) administrate-field-active_storage (0.4.2) administrate (>= 0.2.2) rails (>= 7.0) - administrate-field-acts_as_taggable (0.0.4) - acts-as-taggable-on (>= 6.0) - administrate (< 1.0.0) - administrate-field-jsonb (0.4.6) - administrate (< 1.0.0) + administrate-field-jsonb (0.4.8) + administrate (< 2.0) rails (>= 4.2) administrate-field-list (0.0.6) administrate @@ -205,10 +199,6 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-rails (4.6.0) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) json (2.7.2) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -408,7 +398,8 @@ GEM activemodel (>= 6.1) hashie securerandom (0.3.1) - selectize-rails (0.12.6) + selenium-devtools (0.151.0) + selenium-webdriver (~> 4.2) selenium-webdriver (4.46.0) base64 (~> 0.2) logger (~> 1.4) @@ -428,8 +419,9 @@ GEM fugit (~> 1.8, >= 1.11.1) globalid (>= 1.0.1) sidekiq (>= 6.5.0) - sprockets (4.2.1) + sprockets (4.2.2) concurrent-ruby (~> 1.0) + logger rack (>= 2.2.4, < 4) sprockets-rails (3.5.2) actionpack (>= 6.1) @@ -439,7 +431,7 @@ GEM strscan (3.1.0) systemu (2.6.5) thor (1.3.1) - tilt (2.4.0) + tilt (2.8.0) timeout (0.4.1) trix-rails (2.4.0) rails (> 4.1) @@ -481,7 +473,6 @@ DEPENDENCIES addressable (~> 2.8) administrate administrate-field-active_storage (~> 0.4.1) - administrate-field-acts_as_taggable administrate-field-jsonb administrate-field-list (~> 0.0.6) aws-sdk-rails (~> 5.0) @@ -512,11 +503,14 @@ DEPENDENCIES rubocop-rails rubocop-rspec rubocop-rspec_rails + sassc-rails searchkick + selenium-devtools selenium-webdriver shoulda-matchers sidekiq (>= 7.2.2, < 8) sidekiq-cron (~> 2.4.0) + sprockets-rails trix-rails tzinfo-data uuid (~> 2.3) diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 41bd56d..7dc11fe 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -4,10 +4,8 @@ //= link_tree ../../../vendor/javascript .js //= link administrate-field-jsonb/application.css //= link administrate-field-jsonb/application.js -//= link administrate-field-taggable/application.js -//= link administrate-field-taggable/application.css +//= link acts_as_taggable_field.js //= link administrate-field-active_storage/application.css -//= require selectize //= require trix // application.js // application.css diff --git a/app/assets/javascripts/acts_as_taggable_field.js b/app/assets/javascripts/acts_as_taggable_field.js new file mode 100644 index 0000000..bf7e5cb --- /dev/null +++ b/app/assets/javascripts/acts_as_taggable_field.js @@ -0,0 +1,20 @@ +// Selectize init for ActsAsTaggableField's text input, ported from the +// unmaintained administrate-field-acts_as_taggable gem's application.js. +$(function() { + $(".field-unit--acts-as-taggable-field .field-unit__field > input").each(function(elem) { + var $this = $(this); + var opts = $this.data('tag-options'); + + $this.selectize({ + delimiter: ", ", + persist: false, + options: opts, + create: function(input) { + return { + value: input, + text: input + }; + } + }); + }) +}); diff --git a/app/assets/stylesheets/acts_as_taggable_field.css b/app/assets/stylesheets/acts_as_taggable_field.css new file mode 100644 index 0000000..6867434 --- /dev/null +++ b/app/assets/stylesheets/acts_as_taggable_field.css @@ -0,0 +1,9 @@ +.taggable_field .tag { + text-align: center; + font-size: 0.85em; + margin: 0 3px 3px 0; + padding: 2px 6px; + background: #f2f2f2; + color: #303030; + border: 0 solid #d0d0d0; +} diff --git a/app/dashboards/mention_dashboard.rb b/app/dashboards/mention_dashboard.rb index b3283c4..abae96a 100644 --- a/app/dashboards/mention_dashboard.rb +++ b/app/dashboards/mention_dashboard.rb @@ -18,7 +18,7 @@ class MentionDashboard < Administrate::BaseDashboard id: Field::Number, created_at: Field::DateTime, updated_at: Field::DateTime, - tags: Field::ActsAsTaggable + tags: ActsAsTaggableField }.freeze # COLLECTION_ATTRIBUTES diff --git a/app/fields/acts_as_taggable_field.rb b/app/fields/acts_as_taggable_field.rb new file mode 100644 index 0000000..528a3a3 --- /dev/null +++ b/app/fields/acts_as_taggable_field.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'administrate/field/text' +require 'administrate/engine' + +# In-house replacement for the unmaintained administrate-field-acts_as_taggable gem +# (last released 2021, hard-pins `administrate < 1.0.0`). Behavior is ported as-is +# from that gem's lib/administrate/field/acts_as_taggable.rb. +class ActsAsTaggableField < Administrate::Field::Text + class Engine < ::Rails::Engine + if defined?(Administrate::Engine) + Administrate::Engine.add_javascript 'acts_as_taggable_field' + Administrate::Engine.add_stylesheet 'acts_as_taggable_field' + end + end + + def context + options.fetch(:context, @attribute) + end + + # acts_as_taggable_on exposes a `_list` virtual attribute + # (e.g. `tag_list` for the `:tags` context) for reading/writing a comma-delimited + # tag string - this field operates on that attribute rather than the raw `tags` + # association. + def attribute + "#{super.to_s.singularize}_list" + end + + def self.permitted_attribute(attr, **_opts) + "#{attr.to_s.singularize}_list" + end + + def tags + data + end + + def name + context.to_s + end + + def delimited + tags.join(', ').to_s + end + + def truncate + delimited[0...truncation_length] + end + + def tag_options + return [] unless defined?(ActsAsTaggableOn::Tag) + + ActsAsTaggableOn::Tag.for_context(context).order(:name).map do |t| + { text: t.name, value: t.name } + end + end +end diff --git a/app/views/admin/media/show.html.erb b/app/views/admin/media/show.html.erb index 17a3432..a2435a1 100644 --- a/app/views/admin/media/show.html.erb +++ b/app/views/admin/media/show.html.erb @@ -25,12 +25,13 @@ as well as a link to its edit page. t("administrate.actions.edit_resource", name: page.page_title), [:edit, namespace, page.resource], class: "button", - ) if accessible_action?(:edit) && accessible_action?(:edit, page.resource) %> + ) if accessible_action?(page.resource, :edit) %>
- <%= page.resource.alt_text %> + <% media_url = page.resource.image.attached? ? url_for(page.resource.image) : page.resource.link %> + <%= page.resource.alt_text %>
<%= page.resource.caption %>
<% page.attributes.each do |title, attributes| %> diff --git a/app/views/admin/mentions/_form.html.erb b/app/views/admin/mentions/_form.html.erb index 5d798ea..3ee5e47 100644 --- a/app/views/admin/mentions/_form.html.erb +++ b/app/views/admin/mentions/_form.html.erb @@ -33,10 +33,12 @@ and renders all form fields for a resource's editable attributes. <% end %> - <% page.attributes(controller.action_name).each do |attribute| -%> -
- <%= render_field attribute, f: f %> -
+ <% page.attributes(controller.action_name).each do |title, attributes| -%> + <% attributes.each do |attribute| %> +
+ <%= render_field attribute, f: f %> +
+ <% end -%> <% end -%>
diff --git a/app/views/admin/mentions/edit.html.erb b/app/views/admin/mentions/edit.html.erb index 62e2be2..f443901 100644 --- a/app/views/admin/mentions/edit.html.erb +++ b/app/views/admin/mentions/edit.html.erb @@ -34,7 +34,7 @@ It displays a header, and renders the `_form` partial to do the heavy lifting. t("administrate.actions.show_resource", name: page.page_title), [namespace, page.resource], class: "button", - ) if accessible_action?(:show) && accessible_action?(:show, page.resource) %> + ) if accessible_action?(page.resource, :show) %>
diff --git a/app/views/fields/acts_as_taggable_field/_form.html.erb b/app/views/fields/acts_as_taggable_field/_form.html.erb new file mode 100644 index 0000000..f84fb00 --- /dev/null +++ b/app/views/fields/acts_as_taggable_field/_form.html.erb @@ -0,0 +1,6 @@ +
+ <%= f.label field.attribute %> +
+
+ <%= f.text_field field.attribute, value: field.delimited, data: { tag_options: field.tag_options } %> +
diff --git a/app/views/fields/acts_as_taggable_field/_index.html.erb b/app/views/fields/acts_as_taggable_field/_index.html.erb new file mode 100644 index 0000000..d683ccc --- /dev/null +++ b/app/views/fields/acts_as_taggable_field/_index.html.erb @@ -0,0 +1 @@ +<%= field.truncate %> diff --git a/app/views/fields/acts_as_taggable_field/_show.html.erb b/app/views/fields/acts_as_taggable_field/_show.html.erb new file mode 100644 index 0000000..0651443 --- /dev/null +++ b/app/views/fields/acts_as_taggable_field/_show.html.erb @@ -0,0 +1,5 @@ +
+ <% field.tags.each do |tag| %> + <%= tag.name -%> + <% end %> +
diff --git a/spec/fields/acts_as_taggable_field_spec.rb b/spec/fields/acts_as_taggable_field_spec.rb new file mode 100644 index 0000000..7abbc56 --- /dev/null +++ b/spec/fields/acts_as_taggable_field_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'administrate/field/text' + +RSpec.describe ActsAsTaggableField do + describe '#attribute' do + it 'singularizes the underlying attribute and appends _list' do + field = described_class.new(:tags, [], nil) + expect(field.attribute).to eq('tag_list') + end + end + + describe '.permitted_attribute' do + it 'singularizes the attribute and appends _list' do + expect(described_class.permitted_attribute(:tags)).to eq('tag_list') + end + end + + describe '#tags' do + it 'returns the raw data' do + field = described_class.new(:tags, %w[a b], nil) + expect(field.tags).to eq(%w[a b]) + end + end + + describe '#delimited' do + it 'joins tag names with a comma and space' do + mention = create(:mention, tag_list: 'foo, bar') + field = described_class.new(:tags, mention.tags, nil) + + expect(field.delimited).to eq('foo, bar') + end + + it 'returns an empty string when there are no tags' do + field = described_class.new(:tags, [], nil) + expect(field.delimited).to eq('') + end + end + + describe '#truncate' do + it 'truncates the delimited tag list to the default length' do + mention = create(:mention, tag_list: 'a' * 60) + field = described_class.new(:tags, mention.tags, nil) + + expect(field.truncate.length).to eq(50) + end + end + + describe '#tag_options' do + it 'lists tags used in the tags context, sorted by name' do + create(:mention, tag_list: 'zebra, apple') + field = described_class.new(:tags, [], nil) + + expect(field.tag_options).to eq( + [{ text: 'apple', value: 'apple' }, { text: 'zebra', value: 'zebra' }] + ) + end + end +end diff --git a/spec/requests/admin/about_pages_spec.rb b/spec/requests/admin/about_pages_spec.rb index 716a164..d3159e9 100644 --- a/spec/requests/admin/about_pages_spec.rb +++ b/spec/requests/admin/about_pages_spec.rb @@ -46,10 +46,10 @@ expect(response.body).to include(%(href="#{new_admin_about_page_path}")) end - it 'still renders the "destroy" link on the show page' do + it 'still renders the "destroy" button on the show page' do about_page = create(:about_page) get admin_about_page_path(about_page), headers: admin_auth_headers - expect(response.body).to include(%(href="#{admin_about_page_path(about_page)}")) + expect(response.body).to include(%(action="#{admin_about_page_path(about_page)}")) .and include('button--danger') end end diff --git a/spec/requests/admin/media_spec.rb b/spec/requests/admin/media_spec.rb index 164afbd..96a15c7 100644 --- a/spec/requests/admin/media_spec.rb +++ b/spec/requests/admin/media_spec.rb @@ -18,13 +18,6 @@ describe 'GET show' do it 'renders successfully when authenticated' do - pending( - 'known bug: admin/media/show.html.erb:28 calls accessible_action?(:edit) with one ' \ - 'argument before the correct two-argument call - Administrate::ApplicationHelper#' \ - 'accessible_action? requires (target, action_name), so this raises ArgumentError ' \ - "before the &&'d correct call, or page.resource.url (line 33, Medium has no #url " \ - 'method), is ever reached' - ) medium = create(:medium) get admin_medium_path(medium), headers: admin_auth_headers expect(response).to have_http_status(:ok) diff --git a/spec/requests/admin/mentions_spec.rb b/spec/requests/admin/mentions_spec.rb index d071911..bc9506c 100644 --- a/spec/requests/admin/mentions_spec.rb +++ b/spec/requests/admin/mentions_spec.rb @@ -26,12 +26,6 @@ describe 'GET new' do it 'renders successfully when authenticated' do - pending( - 'known bug: admin/mentions/_form.html.erb:36 iterates page.attributes(...) as a ' \ - 'flat list of fields, but it actually yields [title, attributes] pairs (see ' \ - 'admin/letters/_form.html.erb for the correct nested form), so `attribute.html_class` ' \ - 'raises NoMethodError on the Array' - ) get new_admin_mention_path, headers: admin_auth_headers expect(response).to have_http_status(:ok) end @@ -39,12 +33,6 @@ describe 'GET edit' do it 'renders successfully when authenticated' do - pending( - 'known bug: admin/mentions/edit.html.erb:37 calls ' \ - 'accessible_action?(:show) with one argument before the correct two-argument call - ' \ - 'Administrate::ApplicationHelper#accessible_action? requires (target, action_name), so ' \ - 'this raises ArgumentError before the &&\'d correct call is ever reached' - ) mention = create(:mention) get edit_admin_mention_path(mention), headers: admin_auth_headers expect(response).to have_http_status(:ok) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index dfddde6..0346da3 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -24,5 +24,16 @@ config.before(:each, :js, type: :system) do driven_by Capybara.javascript_driver + + # Admin::ApplicationController gates every admin route behind HTTP Basic Auth, and + # this app has no session-based login to drive instead. Visiting with credentials + # embedded in the URL (http://user:pass@host/path) used to work, but Administrate + # 1.0 bundles Turbo, which calls history.replaceState on every page load - Chrome + # rejects replaceState targets that carry userinfo, and since Administrate's JS is + # one bundled IIFE, that uncaught exception aborts everything after it, including + # selectize's own setup. Real admin users never hit this (their location.href never + # carries credentials - Chrome only uses them for the auth handshake). Registering + # credentials via CDP instead keeps the visited URL clean and sidesteps it entirely. + Capybara.current_session.driver.browser.register(username: 'test', password: 'test') end end diff --git a/spec/system/admin/letters_spec.rb b/spec/system/admin/letters_spec.rb index 03b77fd..1b865bb 100644 --- a/spec/system/admin/letters_spec.rb +++ b/spec/system/admin/letters_spec.rb @@ -9,11 +9,6 @@ # break (jquery-ujs -> Turbo/Stimulus, changed asset bundling in 1.0) - and the only # way to actually verify it is a real browser, since no request spec executes JS. RSpec.describe 'Admin::Letters entity picker', :js do - def visit_with_basic_auth(path) - server = Capybara.current_session.server - visit "http://test:test@#{server.host}:#{server.port}#{path}" - end - def selectize "jQuery('.field-unit--has-many-through-field select')[0].selectize" end @@ -21,7 +16,7 @@ def selectize it 'initializes jQuery and the selectize widget on the entities field' do letter = create(:letter) - visit_with_basic_auth(edit_admin_letter_path(letter)) + visit edit_admin_letter_path(letter) expect(page.execute_script('return window.jQuery !== undefined')).to be true expect(page).to have_css('.selectize-control') @@ -33,7 +28,7 @@ def selectize entity = create(:person_entity, label: 'Zzz System Spec Findable Entity') Entity.reindex - visit_with_basic_auth(edit_admin_letter_path(letter)) + visit edit_admin_letter_path(letter) expect(page).to have_css('.selectize-control') page.execute_script("#{selectize}.addItem('#{entity.id}')") @@ -49,7 +44,7 @@ def selectize letter.save! Entity.reindex - visit_with_basic_auth(edit_admin_letter_path(letter)) + visit edit_admin_letter_path(letter) expect(page).to have_css(".item[data-value='#{entity.id}']") find(".item[data-value='#{entity.id}']").click @@ -68,7 +63,7 @@ def selectize letter.save! Entity.reindex - visit_with_basic_auth(edit_admin_letter_path(letter)) + visit edit_admin_letter_path(letter) expect(page).to have_css(".item[data-value='#{entity.id}']") find(".item[data-value='#{entity.id}']").click From b6473ef36b755ed0162ba286ab0d7c16b9262cdb Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 09:15:30 -0400 Subject: [PATCH 02/16] Clean out unused code and fill in gap in test coverage. --- Gemfile | 22 +-- Gemfile.lock | 21 --- .../admin/application_controller.rb | 2 - app/policies/application_policy.rb | 53 ------- config/initializers/rspec_api_documention.rb | 15 -- spec/acceptance_helper.rb | 12 -- spec/jobs/import_data_job_spec.rb | 7 - spec/models/big_sam_spec.rb | 7 - spec/models/collection_spec.rb | 7 - spec/models/event_type_spec.rb | 7 - spec/models/file_folder_spec.rb | 7 - spec/models/letter_collection_spec.rb | 7 - spec/models/letter_destination_spec.rb | 7 - spec/models/letter_file_spec.rb | 7 - spec/models/letter_origin_spec.rb | 7 - spec/models/letter_owner_spec.rb | 7 - spec/models/letter_publisher_spec.rb | 7 - spec/models/letter_recipient_spec.rb | 7 - spec/models/letter_sender_spec.rb | 7 - spec/models/mention_spec.rb | 7 - spec/requests/repositories_spec.rb | 130 ++++------------ .../markdow_example.mustache | 85 ----------- .../api_blueprint_index.mustache | 80 ---------- .../html_example.mustache | 140 ------------------ .../html_index.mustache | 33 ----- .../markdown_example.mustache | 85 ----------- .../markdown_index.mustache | 15 -- .../slate_example.mustache | 84 ----------- .../slate_index.mustache | 8 - .../textile_example.mustache | 80 ---------- .../textile_index.mustache | 15 -- 31 files changed, 31 insertions(+), 947 deletions(-) delete mode 100644 app/policies/application_policy.rb delete mode 100644 config/initializers/rspec_api_documention.rb delete mode 100644 spec/acceptance_helper.rb delete mode 100644 spec/jobs/import_data_job_spec.rb delete mode 100644 spec/models/big_sam_spec.rb delete mode 100644 spec/models/collection_spec.rb delete mode 100644 spec/models/event_type_spec.rb delete mode 100644 spec/models/file_folder_spec.rb delete mode 100644 spec/models/letter_collection_spec.rb delete mode 100644 spec/models/letter_destination_spec.rb delete mode 100644 spec/models/letter_file_spec.rb delete mode 100644 spec/models/letter_origin_spec.rb delete mode 100644 spec/models/letter_owner_spec.rb delete mode 100644 spec/models/letter_publisher_spec.rb delete mode 100644 spec/models/letter_recipient_spec.rb delete mode 100644 spec/models/letter_sender_spec.rb delete mode 100644 spec/models/mention_spec.rb delete mode 100644 templates/rspec_api_documentation/markdow_example.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/api_blueprint_index.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/html_example.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/html_index.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/markdown_example.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/markdown_index.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/slate_example.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/slate_index.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/textile_example.mustache delete mode 100644 templates/rspec_api_documentation/rspec_api_documentation/textile_index.mustache diff --git a/Gemfile b/Gemfile index d293c44..9b3924d 100644 --- a/Gemfile +++ b/Gemfile @@ -8,8 +8,6 @@ ruby '3.3.4' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 7.2.1' -gem 'csv' - # Use postgresql as the database for Active Record gem 'pg', '~> 1.1' @@ -20,18 +18,6 @@ gem 'searchkick' # Use the Puma web server [https://github.com/puma/puma] gem 'puma', '~> 5.0' -# Build JSON APIs with ease [https://github.com/rails/jbuilder] -# gem 'jbuilder' - -# Use Redis adapter to run Action Cable in production -# gem 'redis', '~> 4.0' - -# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] -# gem 'kredis' - -# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem 'bcrypt', '~> 3.1.7' - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] @@ -51,8 +37,8 @@ gem 'trix-rails', require: 'trix' # administrate < 1.0 pulled these in transitively via sassc-rails; administrate 1.0 # bundles its own assets and dropped that dependency, but the app's own asset # pipeline (app/assets/config/manifest.js) still relies on Sprockets/Sass directly. -gem 'sprockets-rails' gem 'sassc-rails' +gem 'sprockets-rails' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem @@ -78,14 +64,10 @@ group :test do gem 'selenium-devtools' end -gem 'pundit', '~> 2.2' - gem 'kaminari', '~> 1.2' gem 'jbuilder', '~> 2.11' -gem 'rspec_api_documentation', '~> 6.1' - gem 'acts-as-taggable-on', '~> 11.0.0' gem 'administrate-field-list', '~> 0.0.6' @@ -102,8 +84,6 @@ gem 'active_record_doctor', '~> 1.10' gem 'sidekiq', '>=7.2.2', '<8' -gem 'uuid', '~> 2.3' - gem 'addressable', '~> 2.8' gem 'importmap-rails', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 2fb177b..a5c643d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -131,7 +131,6 @@ GEM cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) - csv (3.3.0) date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -217,8 +216,6 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - macaddr (1.7.2) - systemu (~> 2.6.5) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -236,7 +233,6 @@ GEM multi_json (1.21.1) multi_xml (0.7.1) bigdecimal (~> 3.1) - mustache (1.1.1) namae (1.2.0) racc (~> 1.7) net-http (0.9.1) @@ -274,8 +270,6 @@ GEM public_suffix (6.0.1) puma (5.6.8) nio4r (~> 2.0) - pundit (2.4.0) - activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) rack (2.2.9) @@ -331,10 +325,6 @@ GEM roo (2.10.1) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) - rspec (3.13.0) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) rspec-core (3.13.0) rspec-support (~> 3.13.0) rspec-expectations (3.13.2) @@ -352,10 +342,6 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rspec_api_documentation (6.1.0) - activesupport (>= 3.0.0) - mustache (~> 1.0, >= 0.99.4) - rspec (~> 3.0) rubocop (1.65.1) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -429,7 +415,6 @@ GEM sprockets (>= 3.0.0) stringio (3.1.1) strscan (3.1.0) - systemu (2.6.5) thor (1.3.1) tilt (2.8.0) timeout (0.4.1) @@ -441,8 +426,6 @@ GEM unicode-display_width (2.5.0) uri (1.1.1) useragent (0.16.10) - uuid (2.3.9) - macaddr (~> 1.0) webrick (1.8.1) websocket (1.2.11) websocket-driver (0.7.6) @@ -479,7 +462,6 @@ DEPENDENCIES aws-sdk-ses (~> 1.0) bootsnap capybara - csv debug elasticsearch (~> 8) factory_bot_rails @@ -492,12 +474,10 @@ DEPENDENCIES namae (~> 1.1) pg (~> 1.1) puma (~> 5.0) - pundit (~> 2.2) rack-cors rails (~> 7.2.1) roo (~> 2.9) rspec-rails - rspec_api_documentation (~> 6.1) rubocop rubocop-factory_bot rubocop-rails @@ -513,7 +493,6 @@ DEPENDENCIES sprockets-rails trix-rails tzinfo-data - uuid (~> 2.3) RUBY VERSION ruby 3.3.4p94 diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 40afcd6..cba7fbe 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -8,8 +8,6 @@ # you're free to overwrite the RESTful controller actions. module Admin class ApplicationController < Administrate::ApplicationController - # include Administrate::Punditize - before_action :authenticate_admin before_action do diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb deleted file mode 100644 index d91c019..0000000 --- a/app/policies/application_policy.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class ApplicationPolicy - attr_reader :user, :record - - def initialize(user, record) - @user = user - @record = record - end - - def index? - true - end - - def show? - true - end - - def create? - true - end - - def new? - true - end - - def update? - true - end - - def edit? - true - end - - def destroy? - true - end - - class Scope - def initialize(user, scope) - @user = user - @scope = scope - end - - def resolve - raise NotImplementedError, "You must define #resolve in #{self.class}" - end - - private - - attr_reader :user, :scope - end -end diff --git a/config/initializers/rspec_api_documention.rb b/config/initializers/rspec_api_documention.rb deleted file mode 100644 index da707a1..0000000 --- a/config/initializers/rspec_api_documention.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Monkey patch to properly display json :( -# https://github.com/zipmark/rspec_api_documentation/issues/456 -module RspecApiDocumentation - class RackTestClient < ClientBase - def response_body - if last_response.headers['Content-Type'].include?('json') - last_response.body.encode('utf-8') - else - '[binary data]' - end - end - end -end diff --git a/spec/acceptance_helper.rb b/spec/acceptance_helper.rb deleted file mode 100644 index 53dba02..0000000 --- a/spec/acceptance_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'rspec_api_documentation/dsl' - -RspecApiDocumentation.configure do |config| - config.format = %i[html markdown] - config.template_path = Rails.root.join('templates', 'rspec_api_documentation') - config.keep_source_order = true - config.docs_dir = Rails.public_path.join('docs') - config.api_name = 'Beckett Data API Documentation' -end diff --git a/spec/jobs/import_data_job_spec.rb b/spec/jobs/import_data_job_spec.rb deleted file mode 100644 index b858132..0000000 --- a/spec/jobs/import_data_job_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ImportDataJob do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/big_sam_spec.rb b/spec/models/big_sam_spec.rb deleted file mode 100644 index 8cb2214..0000000 --- a/spec/models/big_sam_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BigSam do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb deleted file mode 100644 index c6bfd2c..0000000 --- a/spec/models/collection_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Collection do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/event_type_spec.rb b/spec/models/event_type_spec.rb deleted file mode 100644 index 042fcf6..0000000 --- a/spec/models/event_type_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe EventType do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/file_folder_spec.rb b/spec/models/file_folder_spec.rb deleted file mode 100644 index 751e941..0000000 --- a/spec/models/file_folder_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FileFolder do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_collection_spec.rb b/spec/models/letter_collection_spec.rb deleted file mode 100644 index 33ff10f..0000000 --- a/spec/models/letter_collection_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterCollection do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_destination_spec.rb b/spec/models/letter_destination_spec.rb deleted file mode 100644 index 61fd41f..0000000 --- a/spec/models/letter_destination_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterDestination do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_file_spec.rb b/spec/models/letter_file_spec.rb deleted file mode 100644 index 5db75d9..0000000 --- a/spec/models/letter_file_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterFile do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_origin_spec.rb b/spec/models/letter_origin_spec.rb deleted file mode 100644 index 69658c6..0000000 --- a/spec/models/letter_origin_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterOrigin do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_owner_spec.rb b/spec/models/letter_owner_spec.rb deleted file mode 100644 index bbab35d..0000000 --- a/spec/models/letter_owner_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterOwner do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_publisher_spec.rb b/spec/models/letter_publisher_spec.rb deleted file mode 100644 index b136a6c..0000000 --- a/spec/models/letter_publisher_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterPublisher do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_recipient_spec.rb b/spec/models/letter_recipient_spec.rb deleted file mode 100644 index 356335d..0000000 --- a/spec/models/letter_recipient_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterRecipient do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/letter_sender_spec.rb b/spec/models/letter_sender_spec.rb deleted file mode 100644 index 29a3ed8..0000000 --- a/spec/models/letter_sender_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LetterSender do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb deleted file mode 100644 index a302cff..0000000 --- a/spec/models/mention_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Mention do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/requests/repositories_spec.rb b/spec/requests/repositories_spec.rb index 4f3cd24..a3db5f5 100644 --- a/spec/requests/repositories_spec.rb +++ b/spec/requests/repositories_spec.rb @@ -2,128 +2,58 @@ require 'rails_helper' -# This spec was generated by rspec-rails when you ran the scaffold generator. -# It demonstrates how one might use RSpec to test the controller code that -# was generated by Rails when you ran the scaffold generator. -# -# It assumes that the implementation code is generated by the rails scaffold -# generator. If you are using any extension libraries to generate different -# controller code, this generated spec may or may not pass. -# -# It only uses APIs available in rails and/or rspec-rails. There are a number -# of tools you can use to make these specs even more expressive, but we're -# sticking to rails and rspec-rails APIs to keep things simple and stable. - RSpec.describe '/repositories' do - # This should return the minimal set of attributes required to create a valid - # Repository. As you add validations to Repository, be sure to - # adjust the attributes here as well. - let(:valid_attributes) { - skip('Add a hash of attributes valid for your model') - } - - let(:invalid_attributes) { - skip('Add a hash of attributes invalid for your model') - } - - # This should return the minimal set of values that should be in the headers - # in order to pass any filters (e.g. authentication) defined in - # RepositoriesController, or in your router and rack - # middleware. Be sure to keep this updated too. - let(:valid_headers) { - {} - } - describe 'GET /index' do - it 'renders a successful response' do - Repository.create! valid_attributes - get repositories_url, headers: valid_headers, as: :json + it 'renders a successful response with all repositories' do + create_list(:repository, 3) + get repositories_url, as: :json expect(response).to be_successful + expect(json.count).to eq(3) end end describe 'GET /show' do - it 'renders a successful response' do - repository = Repository.create! valid_attributes + it 'renders the requested repository' do + repository = create(:repository) get repository_url(repository), as: :json expect(response).to be_successful + expect(json[:id]).to eq(repository.id) + expect(json[:label]).to eq(repository.label) end - end - - describe 'POST /create' do - context 'with valid parameters' do - it 'creates a new Repository' do - expect { - post repositories_url, - params: { repository: valid_attributes }, headers: valid_headers, as: :json - }.to change(Repository, :count).by(1) - end - it 'renders a JSON response with the new repository' do - post repositories_url, - params: { repository: valid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:created) - expect(response.content_type).to match(a_string_including('application/json')) - end + it 'renders a not_found response for an unknown id' do + get repository_url(SecureRandom.uuid), as: :json + expect(response).to have_http_status(:not_found) end + end - context 'with invalid parameters' do - it 'does not create a new Repository' do - expect { - post repositories_url, - params: { repository: invalid_attributes }, as: :json - }.not_to change(Repository, :count) - end - - it 'renders a JSON response with errors for the new repository' do - post repositories_url, - params: { repository: invalid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to match(a_string_including('application/json')) - end + # RepositoriesController only overrides index/show - create/update/destroy fall + # through to ApplicationController's defaults, which all `head :not_implemented`. + describe 'POST /create' do + it 'does not create a new Repository' do + expect { + post repositories_url, params: { repository: { label: 'New Repository' } }, as: :json + }.not_to change(Repository, :count) + expect(response).to have_http_status(:not_implemented) end end describe 'PATCH /update' do - context 'with valid parameters' do - let(:new_attributes) { - skip('Add a hash of attributes valid for your model') - } - - it 'updates the requested repository' do - repository = Repository.create! valid_attributes - patch repository_url(repository), - params: { repository: new_attributes }, headers: valid_headers, as: :json - repository.reload - skip('Add assertions for updated state') - end - - it 'renders a JSON response with the repository' do - repository = Repository.create! valid_attributes - patch repository_url(repository), - params: { repository: new_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:ok) - expect(response.content_type).to match(a_string_including('application/json')) - end - end - - context 'with invalid parameters' do - it 'renders a JSON response with errors for the repository' do - repository = Repository.create! valid_attributes - patch repository_url(repository), - params: { repository: invalid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to match(a_string_including('application/json')) - end + it 'does not update the requested repository' do + repository = create(:repository, label: 'Original') + patch repository_url(repository), params: { repository: { label: 'Changed' } }, as: :json + expect(response).to have_http_status(:not_implemented) + expect(repository.reload.label).to eq('Original') end end describe 'DELETE /destroy' do - it 'destroys the requested repository' do - repository = Repository.create! valid_attributes + it 'does not destroy the requested repository' do + repository = create(:repository) expect { - delete repository_url(repository), headers: valid_headers, as: :json - }.to change(Repository, :count).by(-1) + delete repository_url(repository), as: :json + }.not_to change(Repository, :count) + expect(response).to have_http_status(:not_implemented) end end end diff --git a/templates/rspec_api_documentation/markdow_example.mustache b/templates/rspec_api_documentation/markdow_example.mustache deleted file mode 100644 index 16263d0..0000000 --- a/templates/rspec_api_documentation/markdow_example.mustache +++ /dev/null @@ -1,85 +0,0 @@ -# {{ resource_name }} API -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} - -## {{ description }} - -### {{ http_method }} {{ route }} -{{# explanation }} - -{{ explanation }} -{{/ explanation }} -{{# has_parameters? }} - -### Parameters - -| Name | Description | Required | Scope | Default | -|------|-------------|----------|-------|---------| -{{# parameters }} -| {{ name }} | {{ description }} | {{ required }} | {{ scope }} | | {{ default }} -{{/ parameters }} - -{{/ has_parameters? }} -{{# has_response_fields? }} - -### Response Fields - -| Name | Description | Scope | -|------|-------------|-------| -{{# response_fields }} -| {{ name }} | {{ description }} | {{ scope }} | -{{/ response_fields }} - -{{/ has_response_fields? }} -{{# requests }} -### Request - -#### Headers - -
{{ request_headers_text }}
- -#### Route - -
{{ request_method }} {{ request_path }}
-{{# request_query_parameters_text }} - -#### Query Parameters - -
{{ request_query_parameters_text }}
-{{/ request_query_parameters_text }} -{{# request_body }} - -#### Body - -
{{{ request_body }}}
-{{/ request_body }} -{{# curl }} - -#### cURL - -
{{ curl }}
-{{/ curl }} - -{{# response_status }} -### Response - -#### Headers - -
{{ response_headers_text }}
- -#### Status - -
{{ response_status }} {{ response_status_text}}
- -{{# response_body }} -#### Body - -~~~ -{{{ response_body }}} -~~~ - -{{/ response_body }} -{{/ response_status }} -{{/ requests }} \ No newline at end of file diff --git a/templates/rspec_api_documentation/rspec_api_documentation/api_blueprint_index.mustache b/templates/rspec_api_documentation/rspec_api_documentation/api_blueprint_index.mustache deleted file mode 100644 index 865f24a..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/api_blueprint_index.mustache +++ /dev/null @@ -1,80 +0,0 @@ -FORMAT: 1A -# {{ api_name }} -{{# sections }} - -# Group {{ resource_name }} -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} -{{# description }} - -{{ description }} -{{/ description }} -{{# routes }} - -## {{ route_name }} [{{ route }}] -{{# description }} - -description: {{ description }} -{{/ description }} -{{# explanation }} - -explanation: {{ explanation }} -{{/ explanation }} -{{# has_parameters? }} - -+ Parameters -{{# parameters }} - + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }} -{{/ parameters }} -{{/ has_parameters? }} -{{# has_attributes? }} - -+ Attributes (object) -{{# attributes }} - + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }} -{{/ attributes }} -{{/ has_attributes? }} -{{# http_methods }} - -### {{ description }} [{{ http_method }}] -{{# examples }} -{{# requests }} -{{# has_request? }} - -+ Request {{ description }}{{# request_content_type }} ({{ request_content_type }}){{/ request_content_type }} -{{/ has_request? }} -{{# request_headers_text }} - - + Headers - - {{{ request_headers_text }}} -{{/ request_headers_text }} -{{# request_body }} - - + Body - - {{{ request_body }}} -{{/ request_body }} -{{# has_response? }} - -+ Response {{ response_status }} ({{ response_content_type }}) -{{/ has_response? }} -{{# response_headers_text }} - - + Headers - - {{{ response_headers_text }}} -{{/ response_headers_text }} -{{# response_body }} - - + Body - - {{{ response_body }}} -{{/ response_body }} -{{/ requests }} -{{/ examples }} -{{/ http_methods }} -{{/ routes }} -{{/ sections }} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/html_example.mustache b/templates/rspec_api_documentation/rspec_api_documentation/html_example.mustache deleted file mode 100644 index 47474e0..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/html_example.mustache +++ /dev/null @@ -1,140 +0,0 @@ - - - - {{resource_name}} API - - - - -
-

{{resource_name}} API

- {{# resource_explanation }} - -

{{{ resource_explanation }}}

- {{/ resource_explanation }} - -
-

{{ description }}

-

{{ http_method }} {{ route }}

- {{# explanation }} -

- {{{ explanation }}} -

- {{/ explanation }} - - {{# has_parameters? }} -

Parameters

- - - - - - - - - - {{# parameters }} - - - {{# scope }} - {{ scope }}[{{ name }}] - {{/ scope }} - {{^ scope }} - {{ name }} - {{/ scope }} - - - - - {{/ parameters }} - -
NameDescriptionDefault
- {{ description }} - - {{ default }} -
- {{/ has_parameters? }} - - {{# has_response_fields? }} -

Response Fields

- - - - - - - - - - - {{# response_fields }} - - - - - - - {{/ response_fields }} - -
NameDefaultDescriptionNot Null
- {{# scope }} - {{ scope }}[{{ name }}] - {{/ scope }} - {{^ scope }} - {{ name }} - {{/ scope }} - - {{ default }} - - {{ description }} - - {{ not_null }} -
- {{/ has_response_fields? }} - - {{# requests }} -

Request

- - {{# request_headers_text }} -

Headers

-
{{ request_headers_text }}
- {{/ request_headers_text }} - -

Route

-
{{ request_method }} {{ request_path }}
- - {{# request_query_parameters_text }} -

Query Parameters

-
{{ request_query_parameters_text }}
- {{/ request_query_parameters_text }} - - {{# request_body }} -

Body

-
{{{ request_body }}}
- {{/ request_body }} - - {{# curl }} -

cURL

-
{{ curl }}
- {{/ curl }} - - {{# response_status }} -

Response

- {{# response_headers_text }} -

Headers

-
{{ response_headers_text }}
- {{/ response_headers_text }} -

Status

-
{{ response_status }} {{ response_status_text}}
- {{# response_body }} -

Body

-
{{ response_body }}
- {{/ response_body }} - {{/ response_status }} - {{/ requests }} -
-
- - diff --git a/templates/rspec_api_documentation/rspec_api_documentation/html_index.mustache b/templates/rspec_api_documentation/rspec_api_documentation/html_index.mustache deleted file mode 100644 index aaedde2..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/html_index.mustache +++ /dev/null @@ -1,33 +0,0 @@ - - - - {{ api_name }} - - - - -
-

{{ api_name }}

- {{{ api_explanation }}} - {{# sections }} -
-

{{ resource_name }}

- {{# resource_explanation }} - -

{{{ resource_explanation }}}

- {{/ resource_explanation }} - - -
- {{/ sections }} -
- - diff --git a/templates/rspec_api_documentation/rspec_api_documentation/markdown_example.mustache b/templates/rspec_api_documentation/rspec_api_documentation/markdown_example.mustache deleted file mode 100644 index 474ab56..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/markdown_example.mustache +++ /dev/null @@ -1,85 +0,0 @@ -# {{ resource_name }} API -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} - -## {{ description }} - -### {{ http_method }} {{ route }} -{{# explanation }} - -{{ explanation }} -{{/ explanation }} -{{# has_parameters? }} - -### Parameters - -| Name | Description | Required | Default | -|------|-------------|----------|---------| -{{# parameters }} -| {{ name }} | {{ description }} | {{ required }} | {{ default }} -{{/ parameters }} - -{{/ has_parameters? }} -{{# has_response_fields? }} - -### Response Fields - -| Name | Default | Description | Not Null | -|------|---------|-------------|----------| -{{# response_fields }} -| {{ name }} | {{ default }} | {{ description }} | {{ not_null }} | -{{/ response_fields }} - -{{/ has_response_fields? }} -{{# requests }} -### Request - -#### Headers - -
{{ request_headers_text }}
- -#### Route - -
{{ request_method }} {{ request_path }}
-{{# request_query_parameters_text }} - -#### Query Parameters - -
{{ request_query_parameters_text }}
-{{/ request_query_parameters_text }} -{{# request_body }} - -#### Body - -
{{{ request_body }}}
-{{/ request_body }} -{{# curl }} - -#### cURL - -
{{ curl }}
-{{/ curl }} - -{{# response_status }} -### Response - -#### Headers - -
{{ response_headers_text }}
- -#### Status - -
{{ response_status }} {{ response_status_text}}
- -{{# response_body }} -#### Body - -~~~ -{{{ response_body }}} -~~~ - -{{/ response_body }} -{{/ response_status }} -{{/ requests }} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/markdown_index.mustache b/templates/rspec_api_documentation/rspec_api_documentation/markdown_index.mustache deleted file mode 100644 index c88754d..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/markdown_index.mustache +++ /dev/null @@ -1,15 +0,0 @@ -# {{ api_name }} -{{{ api_explanation }}} - -{{# sections }} -## {{ resource_name }} -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} - -{{# examples }} -* [{{ description }}]({{ dirname }}/{{ filename }}) -{{/ examples }} - -{{/ sections }} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/slate_example.mustache b/templates/rspec_api_documentation/rspec_api_documentation/slate_example.mustache deleted file mode 100644 index 01fafb9..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/slate_example.mustache +++ /dev/null @@ -1,84 +0,0 @@ -## {{ description }} - -{{# explanation }} -{{{ explanation }}} -{{/ explanation }} - -### Request - -#### Endpoint - -{{# requests}} -```plaintext -{{ request_method }} {{ request_path }} -{{ request_headers_text }} -``` -{{/ requests}} - -`{{ http_method }} {{ route }}` - -#### Parameters - -{{# requests}} -{{# request_query_parameters_text }} - -```json -{{ request_query_parameters_text }} -``` -{{/ request_query_parameters_text }} -{{# request_body }} - -```json -{{{ request_body }}} -``` -{{/ request_body }} - -{{# has_parameters? }} - -| Name | Description | -|:-----|:------------| -{{# parameters }} -| {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} {{# required }}*required*{{/ required }} | {{{ description }}} | -{{/ parameters }} - -{{/ has_parameters? }} -{{^ has_parameters? }} -None known. -{{/ has_parameters? }} - -{{# response_status}} - -### Response - -```plaintext -{{ response_headers_text }} -{{ response_status }} {{ response_status_text}} -``` - -{{# response_body}} - -```json -{{{ response_body }}} -``` -{{/response_body}} - -{{/ response_status}} - -{{# has_response_fields? }} - -#### Fields - -| Name | Description | -|:-----------|:--------------------| -{{# response_fields }} -| {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} | {{{ description }}} | -{{/ response_fields }} - -{{/ has_response_fields? }} - -{{# curl }} -```shell -{{{ curl }}} -``` -{{/ curl }} -{{/ requests}} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/slate_index.mustache b/templates/rspec_api_documentation/rspec_api_documentation/slate_index.mustache deleted file mode 100644 index be0e5ae..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/slate_index.mustache +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: {{ api_name }} -language_tabs: - - json: JSON - - shell: cURL ---- - -{{{ api_explanation }}} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/textile_example.mustache b/templates/rspec_api_documentation/rspec_api_documentation/textile_example.mustache deleted file mode 100644 index a7e022b..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/textile_example.mustache +++ /dev/null @@ -1,80 +0,0 @@ -h1. {{ resource_name }} API -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} - -h2. {{ description }} - -h3. {{ http_method }} {{ route }} - -{{# explanation }} -{{ explanation }} - -{{/ explanation }} -{{# has_parameters? }} -h3. Parameters -{{# parameters }} - -Name : {{ name }} {{# required }} *- required -*{{/ required }} -Description : {{ description }} -{{/ parameters }} - -{{/ has_parameters? }} -{{# has_response_fields? }} -h3. Response Fields -{{# response_fields }} - -Name : {{ name }} -Description : {{ description }} -{{/ response_fields }} - -{{/ has_response_fields? }} -{{# requests }} -h3. Request - -h4. Headers - -
{{ request_headers_text }}
- -h4. Route - -
{{ request_method }} {{ request_path }}
- -{{# request_query_parameters_text }} -h4. Query Parameters - -
{{ request_query_parameters_text }}
- -{{/ request_query_parameters_text }} -{{# request_body }} -h4. Body - -
{{{ request_body }}}
- -{{/ request_body }} -{{# curl }} -h4. cURL - -
{{ curl }}
- -{{/ curl }} -{{# response_status }} -h3. Response - -h4. Headers - -
{{ response_headers_text }}
- -h4. Status - -
{{ response_status }} {{ response_status_text}}
- -{{# response_body }} -h4. Body - -
{{{ response_body }}}
- -{{/ response_body }} -{{/ response_status }} -{{/ requests }} diff --git a/templates/rspec_api_documentation/rspec_api_documentation/textile_index.mustache b/templates/rspec_api_documentation/rspec_api_documentation/textile_index.mustache deleted file mode 100644 index 564b410..0000000 --- a/templates/rspec_api_documentation/rspec_api_documentation/textile_index.mustache +++ /dev/null @@ -1,15 +0,0 @@ -h1. {{ api_name }} -{{{ api_explanation }}} - -{{# sections }} -h2. {{ resource_name }} -{{# resource_explanation }} - -{{{ resource_explanation }}} -{{/ resource_explanation }} - -{{# examples }} -* "{{ description }}":{{ dirname }}/{{ filename }} -{{/ examples }} - -{{/ sections }} From 366a41b82da932588f21aa4a9eb8ad88c5cc814a Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 10:55:08 -0400 Subject: [PATCH 03/16] Include publisher in index --- app/models/published_letter.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/published_letter.rb b/app/models/published_letter.rb index 69ec01b..487764b 100644 --- a/app/models/published_letter.rb +++ b/app/models/published_letter.rb @@ -30,10 +30,10 @@ class PublishedLetter < ApplicationRecord has_many :letter_languages, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy has_many :languages, -> { distinct }, through: :letter_languages - belongs_to :letter_file, foreign_key: 'letter_id', inverse_of: :letter, optional: true - belongs_to :file_folder, foreign_key: 'letter_id', inverse_of: :letter, optional: true - belongs_to :letter_owner, foreign_key: 'letter_id', inverse_of: :letter, optional: true - belongs_to :letter_publisher, foreign_key: 'letter_id', inverse_of: :letter, optional: true + belongs_to :letter_file, foreign_key: 'letter_id', inverse_of: :letters, optional: true + belongs_to :file_folder, foreign_key: 'letter_id', inverse_of: :letters, optional: true + belongs_to :letter_owner, foreign_key: 'letter_id', inverse_of: :letters, optional: true + belongs_to :letter_publisher, foreign_key: 'letter_id', inverse_of: :letters, optional: true def self.default_scope where(published: true) @@ -53,7 +53,8 @@ def search_data repositories: repositories.select(&:published).map(&:label), languages: languages.map(&:label), published:, - volume: volume.to_s + volume: volume.to_s, + publisher: letter_publisher } end From 6e4581ca7f7b7bdc2bd96c78046b31fba4268df3 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 11:04:46 -0400 Subject: [PATCH 04/16] Index the publisher's label --- app/models/published_letter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/published_letter.rb b/app/models/published_letter.rb index 487764b..2253916 100644 --- a/app/models/published_letter.rb +++ b/app/models/published_letter.rb @@ -54,7 +54,7 @@ def search_data languages: languages.map(&:label), published:, volume: volume.to_s, - publisher: letter_publisher + publisher: letter_publisher.label } end From 0f4aea0a02647e6cd91e2a81bcbeeade0cb428c8 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 11:16:19 -0400 Subject: [PATCH 05/16] Index publisher label --- app/models/published_letter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/published_letter.rb b/app/models/published_letter.rb index 2253916..f50edc6 100644 --- a/app/models/published_letter.rb +++ b/app/models/published_letter.rb @@ -54,7 +54,7 @@ def search_data languages: languages.map(&:label), published:, volume: volume.to_s, - publisher: letter_publisher.label + publisher: letter_publisher&.label } end From 14fd1e7cd6ca98395f593bcf0bfd471ef7a32f6d Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 10:01:25 -0400 Subject: [PATCH 06/16] Patch and security bumps within existing Gemfile constraints Ran bundler-audit against the lockfile: 174 known-CVE advisory entries across 21 gems, mostly Rails-core ReDoS/XSS/path-traversal issues fixed by moving within the existing 7.2.x line, plus a large batch of nokogiri memory-safety issues. Used `bundle update --patch` rather than a plain `bundle update` after the latter silently pulled several unpinned direct dependencies (searchkick, rspec-rails, shoulda-matchers, rubocop) up whole major versions - those are explicitly deferred to their own follow-up work, not bundled in here. Beyond the patch-only pass, targeted a few specific fixes for CVEs a patch-level bump couldn't reach: nokogiri (1.16->1.19, several High-severity memory-safety issues), loofah/rails-html-sanitizer (both back this app's own RichTextField sanitizer), rexml, websocket-driver, net-imap, msgpack, and addressable (needed relaxing its Gemfile pin from `~> 2.8` to `~> 2.9`). Two gems still have open advisories, both requiring a major-version bump this pass deliberately defers: - httparty 0.20.0: High-severity SSRF/API-key-leakage CVE, fixed in >= 0.24.0. Every call site (lib/import.rb, app/jobs/import_data_job.rb) hits a single hardcoded host with no user- or redirect-controlled URL, so practical exploitability here looks low, but flagging clearly rather than silently leaving it. - puma 5.6.9: two High-severity PROXY-protocol-parsing CVEs, fixed in ~> 7.2.1 or >= 8.0.2. config/puma.rb never enables proxy_protocol on any bind directive, so the vulnerable code path isn't reachable as configured. Also fixed 5 mechanical rubocop offenses (frozen_string_literal comments, string style, ENV.fetch) that the toolchain patch bump newly surfaced. Left 3 Metrics/MethodLength and 1 Rails/I18nLocaleTexts finding alone - pre-existing code the stricter toolchain now flags differently, not something to refactor silently as a side effect of a dependency bump. Co-Authored-By: Claude Sonnet 5 --- Gemfile | 2 +- Gemfile.lock | 295 ++++++++++++----------- app/workers/resave_all_letters_worker.rb | 2 + config/importmap.rb | 4 +- spec/jobs/load_big_sam_job_spec.rb | 2 + spec/rails_helper.rb | 2 +- 6 files changed, 164 insertions(+), 143 deletions(-) diff --git a/Gemfile b/Gemfile index 9b3924d..a0bbdca 100644 --- a/Gemfile +++ b/Gemfile @@ -84,7 +84,7 @@ gem 'active_record_doctor', '~> 1.10' gem 'sidekiq', '>=7.2.2', '<8' -gem 'addressable', '~> 2.8' +gem 'addressable', '~> 2.9' gem 'importmap-rails', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index a5c643d..b88e9c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,83 +1,86 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.1) - actionpack (= 7.2.1) - activesupport (= 7.2.1) + actioncable (7.2.3.2) + actionpack (= 7.2.3.2) + activesupport (= 7.2.3.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.1) - actionpack (= 7.2.1) - activejob (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + actionmailbox (7.2.3.2) + actionpack (= 7.2.3.2) + activejob (= 7.2.3.2) + activerecord (= 7.2.3.2) + activestorage (= 7.2.3.2) + activesupport (= 7.2.3.2) mail (>= 2.8.0) - actionmailer (7.2.1) - actionpack (= 7.2.1) - actionview (= 7.2.1) - activejob (= 7.2.1) - activesupport (= 7.2.1) + actionmailer (7.2.3.2) + actionpack (= 7.2.3.2) + actionview (= 7.2.3.2) + activejob (= 7.2.3.2) + activesupport (= 7.2.3.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.1) - actionview (= 7.2.1) - activesupport (= 7.2.1) + actionpack (7.2.3.2) + actionview (= 7.2.3.2) + activesupport (= 7.2.3.2) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.1) - actionpack (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + actiontext (7.2.3.2) + actionpack (= 7.2.3.2) + activerecord (= 7.2.3.2) + activestorage (= 7.2.3.2) + activesupport (= 7.2.3.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.1) - activesupport (= 7.2.1) + actionview (7.2.3.2) + activesupport (= 7.2.3.2) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_doctor (1.14.0) activerecord (>= 4.2.0) - activejob (7.2.1) - activesupport (= 7.2.1) + activejob (7.2.3.2) + activesupport (= 7.2.3.2) globalid (>= 0.3.6) - activemodel (7.2.1) - activesupport (= 7.2.1) - activerecord (7.2.1) - activemodel (= 7.2.1) - activesupport (= 7.2.1) + activemodel (7.2.3.2) + activesupport (= 7.2.3.2) + activerecord (7.2.3.2) + activemodel (= 7.2.3.2) + activesupport (= 7.2.3.2) timeout (>= 0.4.0) - activestorage (7.2.1) - actionpack (= 7.2.1) - activejob (= 7.2.1) - activerecord (= 7.2.1) - activesupport (= 7.2.1) + activestorage (7.2.3.2) + actionpack (= 7.2.3.2) + activejob (= 7.2.3.2) + activerecord (= 7.2.3.2) + activesupport (= 7.2.3.2) marcel (~> 1.0) - activesupport (7.2.1) + activesupport (7.2.3.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1) + minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) acts-as-taggable-on (11.0.0) activerecord (>= 7.0, < 8.0) zeitwerk (>= 2.4, < 3.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) administrate (1.0.0) actionpack (>= 6.0, < 9.0) actionview (>= 6.0, < 9.0) @@ -92,7 +95,7 @@ GEM administrate-field-list (0.0.6) administrate rails (>= 5.0) - ast (2.4.2) + ast (2.4.3) aws-eventstream (1.4.0) aws-partitions (1.1278.0) aws-sdk-core (3.254.1) @@ -111,9 +114,10 @@ GEM aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - base64 (0.2.0) - bigdecimal (3.1.8) - bootsnap (1.18.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (3.1.9) + bootsnap (1.18.6) msgpack (~> 1.2) builder (3.3.0) capybara (3.40.0) @@ -125,19 +129,20 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - concurrent-ruby (1.3.4) + cgi (0.5.2) + concurrent-ruby (1.3.8) connection_pool (2.4.1) - crass (1.0.6) + crass (1.0.7) cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) - date (3.3.4) + date (3.5.1) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.5.1) - drb (2.2.1) - elastic-transport (8.5.2) + drb (2.2.3) + elastic-transport (8.5.3) faraday (< 3) multi_json elasticsearch (8.19.3) @@ -146,33 +151,33 @@ GEM ostruct elasticsearch-api (8.19.3) multi_json - erubi (1.13.0) - et-orbi (1.4.0) + erubi (1.13.1) + et-orbi (1.4.1) tzinfo - factory_bot (6.4.6) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) faker (3.4.2) i18n (>= 1.8.11, < 2) - faraday (2.14.2) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger faraday-net_http (3.4.4) net-http (~> 0.5) - ffi (1.17.0-aarch64-linux-gnu) - ffi (1.17.0-aarch64-linux-musl) - ffi (1.17.0-arm-linux-gnu) - ffi (1.17.0-arm-linux-musl) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86-linux-gnu) - ffi (1.17.0-x86-linux-musl) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) - ffi (1.17.0-x86_64-linux-musl) - fugit (1.12.2) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fugit (1.12.3) et-orbi (~> 1.4) raabro (~> 1.4) globalid (1.2.1) @@ -181,24 +186,24 @@ GEM httparty (0.20.0) mime-types (~> 3.0) multi_xml (>= 0.5.2) - i18n (1.14.5) + i18n (1.14.8) concurrent-ruby (~> 1.0) image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.7.2) - irb (1.14.0) + irb (1.14.3) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - json (2.7.2) + json (2.7.6) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -211,9 +216,9 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - language_server-protocol (3.17.0.3) - logger (1.6.0) - loofah (2.22.0) + language_server-protocol (3.17.0.6) + logger (1.6.6) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -225,91 +230,101 @@ GEM matrix (0.4.3) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0820) + mime-types-data (3.2024.1203) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.1) - msgpack (1.7.2) + mini_portile2 (2.8.9) + minitest (5.25.5) + msgpack (1.8.4) multi_json (1.21.1) - multi_xml (0.7.1) + multi_xml (0.7.2) bigdecimal (~> 3.1) namae (1.2.0) racc (~> 1.7) net-http (0.9.1) uri (>= 0.11.1) - net-imap (0.4.14) + net-imap (0.6.6) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.0) + net-smtp (0.5.1) net-protocol - nio4r (2.7.3) - nokogiri (1.16.7-aarch64-linux) + nio4r (2.7.5) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.16.7-arm-linux) + nokogiri (1.19.4-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.16.7-arm64-darwin) + nokogiri (1.19.4-arm-linux-musl) racc (~> 1.4) - nokogiri (1.16.7-x86-linux) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) + nokogiri (1.19.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.3) parallel (1.26.3) - parser (3.3.4.2) + parser (3.3.12.0) ast (~> 2.4.1) racc - pg (1.5.7) + pg (1.5.9) psych (5.1.2) stringio - public_suffix (6.0.1) - puma (5.6.8) + public_suffix (7.0.5) + puma (5.6.9) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.23) rack-cors (2.0.2) rack (>= 2.0.0) rack-session (1.0.2) rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rackup (1.0.0) + rackup (1.0.1) rack (< 3) webrick - rails (7.2.1) - actioncable (= 7.2.1) - actionmailbox (= 7.2.1) - actionmailer (= 7.2.1) - actionpack (= 7.2.1) - actiontext (= 7.2.1) - actionview (= 7.2.1) - activejob (= 7.2.1) - activemodel (= 7.2.1) - activerecord (= 7.2.1) - activestorage (= 7.2.1) - activesupport (= 7.2.1) + rails (7.2.3.2) + actioncable (= 7.2.3.2) + actionmailbox (= 7.2.3.2) + actionmailer (= 7.2.3.2) + actionpack (= 7.2.3.2) + actiontext (= 7.2.3.2) + actionview (= 7.2.3.2) + activejob (= 7.2.3.2) + activemodel (= 7.2.3.2) + activerecord (= 7.2.3.2) + activestorage (= 7.2.3.2) + activesupport (= 7.2.3.2) bundler (>= 1.15.0) - railties (= 7.2.1) + railties (= 7.2.3.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.2.1) - actionpack (= 7.2.1) - activesupport (= 7.2.1) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.2.3.2) + actionpack (= 7.2.3.2) + activesupport (= 7.2.3.2) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) @@ -317,23 +332,22 @@ GEM psych (>= 4.0.0) redis-client (0.23.2) connection_pool - regexp_parser (2.9.2) - reline (0.5.9) + regexp_parser (2.9.3) + reline (0.5.12) io-console (~> 0.5) - rexml (3.3.6) - strscan + rexml (3.4.4) roo (2.10.1) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) - rspec-core (3.13.0) + rspec-core (3.13.6) rspec-support (~> 3.13.0) - rspec-expectations (3.13.2) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.4) + rspec-rails (6.1.5) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -341,7 +355,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.1) + rspec-support (3.13.7) rubocop (1.65.1) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -353,22 +367,22 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.1) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) rubocop-factory_bot (2.26.1) rubocop (~> 1.61) - rubocop-rails (2.26.0) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.4) + rubocop-rspec (3.0.5) rubocop (~> 1.61) rubocop-rspec_rails (2.30.0) rubocop (~> 1.61) rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) - ruby-vips (2.2.2) + ruby-vips (2.2.5) ffi (~> 1.12) logger rubyzip (2.3.2) @@ -383,7 +397,7 @@ GEM searchkick (5.3.1) activemodel (>= 6.1) hashie - securerandom (0.3.1) + securerandom (0.3.2) selenium-devtools (0.151.0) selenium-webdriver (~> 4.2) selenium-webdriver (4.46.0) @@ -394,12 +408,12 @@ GEM websocket (~> 1.0) shoulda-matchers (6.4.0) activesupport (>= 5.2.0) - sidekiq (7.3.8) + sidekiq (7.3.10) base64 - connection_pool (>= 2.3.0) + connection_pool (>= 2.3.0, < 3) logger - rack (>= 2.2.4) - redis-client (>= 0.22.2) + rack (>= 2.2.4, < 3.3) + redis-client (>= 0.23.0, < 1) sidekiq-cron (2.4.0) cronex (>= 0.13.0) fugit (~> 1.8, >= 1.11.1) @@ -413,27 +427,28 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - stringio (3.1.1) - strscan (3.1.0) - thor (1.3.1) + stringio (3.1.9) + thor (1.3.2) tilt (2.8.0) - timeout (0.4.1) + timeout (0.6.1) trix-rails (2.4.0) rails (> 4.1) + tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode (0.4.4.5) unicode-display_width (2.5.0) uri (1.1.1) - useragent (0.16.10) - webrick (1.8.1) + useragent (0.16.11) + webrick (1.8.2) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.8.2) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.17) + zeitwerk (2.6.18) PLATFORMS aarch64-linux @@ -453,7 +468,7 @@ PLATFORMS DEPENDENCIES active_record_doctor (~> 1.10) acts-as-taggable-on (~> 11.0.0) - addressable (~> 2.8) + addressable (~> 2.9) administrate administrate-field-active_storage (~> 0.4.1) administrate-field-jsonb diff --git a/app/workers/resave_all_letters_worker.rb b/app/workers/resave_all_letters_worker.rb index a84ef58..5d95750 100644 --- a/app/workers/resave_all_letters_worker.rb +++ b/app/workers/resave_all_letters_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ResaveAllLettersWorker include Sidekiq::Job diff --git a/config/importmap.rb b/config/importmap.rb index 7fdb198..7c2f1f5 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # Pin npm packages by running ./bin/importmap pin 'application' pin 'moment' # @2.30.1 -pin "js-datepicker" # @5.18.3 +pin 'js-datepicker' # @5.18.3 diff --git a/spec/jobs/load_big_sam_job_spec.rb b/spec/jobs/load_big_sam_job_spec.rb index 4271655..aaf26b0 100644 --- a/spec/jobs/load_big_sam_job_spec.rb +++ b/spec/jobs/load_big_sam_job_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' require 'fileutils' diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a9bc56b..4c3b332 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -49,7 +49,7 @@ # ActiveStorage::Current is a CurrentAttributes subclass — its values are # reset by ActiveSupport::Executor after each inline job. Setting url_options # in before(:each) ensures it's present whenever a spec calls .url. - config.before { ActiveStorage::Current.url_options = { host: ENV['RAILS_HOST'] } } + config.before { ActiveStorage::Current.url_options = { host: ENV.fetch('RAILS_HOST', nil) } } # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = [Rails.root.join('spec', 'fixtures'), Rails.root.join('spec', 'fixtures', 'files')] From bbc20bd0aa60b4f153bb4452fca90b69d1d57d95 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 10:21:14 -0400 Subject: [PATCH 07/16] Bump Ruby 3.3.4 -> 3.4.10 Rails 8 only requires Ruby >= 3.2 (this app is already well past that), so this is independent of the Rails bump - isolating "does the app work on new Ruby" from "does the app work on new Rails" as separate questions before compounding both. Re-adds the csv gem, removed earlier as an unused direct dependency: csv stopped being a Ruby default gem in 3.4+, and httparty 0.20.0 requires it internally without declaring it as its own dependency (confirmed - the app booted with a real deprecation warning on 3.3 before this bump, and boots silently now). Verified: full suite (304 examples, 0 failures), rubocop (same 4 pre-existing findings as before, nothing new from the Ruby bump itself), clean app boot. Still on Rails 7.2.3.2 for this commit. Co-Authored-By: Claude Sonnet 5 --- .ruby-version | 2 +- Gemfile | 6 +++++- Gemfile.lock | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.ruby-version b/.ruby-version index a0891f5..84d6c67 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.4 +3.4.10 diff --git a/Gemfile b/Gemfile index a0bbdca..4dcc509 100644 --- a/Gemfile +++ b/Gemfile @@ -3,11 +3,15 @@ source 'https://rubygems.org' git_source(:github) {|_repo| "https://github.com/#{repo}.git" } -ruby '3.3.4' +ruby '3.4.10' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 7.2.1' +# csv stopped being a Ruby default gem in 3.4+; httparty requires it internally +# without declaring it as a dependency, so it must be explicit here. +gem 'csv' + # Use postgresql as the database for Active Record gem 'pg', '~> 1.1' diff --git a/Gemfile.lock b/Gemfile.lock index b88e9c3..5058f4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -136,6 +136,7 @@ GEM cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) + csv (3.3.6) date (3.5.1) debug (1.9.2) irb (~> 1.10) @@ -477,6 +478,7 @@ DEPENDENCIES aws-sdk-ses (~> 1.0) bootsnap capybara + csv debug elasticsearch (~> 8) factory_bot_rails @@ -510,7 +512,7 @@ DEPENDENCIES tzinfo-data RUBY VERSION - ruby 3.3.4p94 + ruby 3.4.10p104 BUNDLED WITH 2.5.11 From b2f1ec0c2fb064008ed8a679dc237ae0d614f04f Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 11:09:26 -0400 Subject: [PATCH 08/16] Bump Rails 7.2.3.2 -> 8.1.3.1 acts-as-taggable-on had to move too: 11.0.0 hard-pins activerecord < 8.0, which makes it impossible to install alongside Rails 8 at all - not a discretionary bump like the others deferred in the earlier patch-bump commit, a forced one. Bumped to ~> 13.0 (12->13 has no breaking changes to the tag_list/for_context API ActsAsTaggableField depends on, per its CHANGELOG - only dropped-Ruby-version and added-Rails-version support). Ran `bin/rails app:update` and reviewed its diff file-by-file rather than accepting it wholesale: declined every config file overwrite (all just show cosmetic/new-default-flag differences from this app's deliberate customizations - CORS rules, custom filter_parameters, Sidekiq queue adapter, etc. - confirmed by diffing against a fresh Rails 8.1 scaffold app, nothing there is actually required for Rails 8 to work), dropped the generated bin/ci + config/ci.rb (assumes MiniTest via `bin/rails test`, this app uses RSpec with its own GitHub Actions workflow), and deleted 3 generated ActiveStorage migrations that are pure no-ops here (this app's existing single `create_active_storage_tables` migration, dated 2022, already creates everything they would). Keeps the generated, all-commented-out new_framework_defaults_8_1.rb checklist and config.load_defaults at 7.0 - deliberately not adopting new defaults as part of this bump, per Rails' own recommended upgrade path. Manual verification in a real browser (RAILS_ENV=test server, since dev credentials aren't available here) caught a real regression the automated suite had missed: ActsAsTaggableField's own field spec constructed the field directly with pre-fetched data, and the mentions request spec's factory had no tags by default, so neither ever exercised Administrate's actual field-construction path. Administrate::Field::Base#read_value (new in 1.0, works differently than in 0.20.1) lazily fetches data via `resource.try(attribute)` when data is nil - and that `attribute` call is polymorphic, landing on ActsAsTaggableField's own #attribute override (returns "tag_list", for form-building) instead of the raw :tags key. That fetched mention.tag_list (name strings) instead of mention.tags (real Tag records), and _show.html.erb's `tag.name` raised NoMethodError on a String - reproducible on develop today with any tagged mention, not something this Rails bump introduced. Fixed by overriding #read_value to use the raw @attribute ivar instead of the overridden method, and added regression coverage at both the field-unit level (constructing the field the way Administrate really does) and the request-spec level (a tagged mention through index/show/edit). Verified: full suite (308 examples, 0 failures - reset the local test DB after manual browser testing polluted it with non-transactional data), system specs passing (letters entity picker jQuery/selectize still initializes correctly), rubocop clean, and a real browser click-through across letters/mentions/entities admin pages. Co-Authored-By: Claude Sonnet 5 --- Gemfile | 12 +- Gemfile.lock | 135 +++++++++--------- app/fields/acts_as_taggable_field.rb | 16 +++ bin/dev | 2 + bin/rubocop | 8 ++ .../new_framework_defaults_8_1.rb | 76 ++++++++++ spec/fields/acts_as_taggable_field_spec.rb | 18 +++ spec/requests/admin/mentions_spec.rb | 24 ++++ 8 files changed, 222 insertions(+), 69 deletions(-) create mode 100644 bin/dev create mode 100644 bin/rubocop create mode 100644 config/initializers/new_framework_defaults_8_1.rb diff --git a/Gemfile b/Gemfile index 4dcc509..e00eeee 100644 --- a/Gemfile +++ b/Gemfile @@ -6,12 +6,16 @@ git_source(:github) {|_repo| "https://github.com/#{repo}.git" } ruby '3.4.10' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' -gem 'rails', '~> 7.2.1' +gem 'rails', '~> 8.1.3' # csv stopped being a Ruby default gem in 3.4+; httparty requires it internally # without declaring it as a dependency, so it must be explicit here. gem 'csv' +# benchmark won't be a default gem starting in Ruby 4.0; mini_magick requires it +# internally without declaring it as a dependency. +gem 'benchmark' + # Use postgresql as the database for Active Record gem 'pg', '~> 1.1' @@ -72,7 +76,11 @@ gem 'kaminari', '~> 1.2' gem 'jbuilder', '~> 2.11' -gem 'acts-as-taggable-on', '~> 11.0.0' +# 11.0.0 hard-pins activerecord < 8.0, which makes it impossible to install +# alongside Rails 8 at all - not a discretionary bump, a forced one. 12->13 has +# no breaking changes to the tag_list/for_context API ActsAsTaggableField uses +# (see CHANGELOG.md), only dropped-Ruby-version and added-Rails-version support. +gem 'acts-as-taggable-on', '~> 13.0' gem 'administrate-field-list', '~> 0.0.6' diff --git a/Gemfile.lock b/Gemfile.lock index 5058f4d..3b4bc5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,83 +1,84 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.3.2) - actionpack (= 7.2.3.2) - activesupport (= 7.2.3.2) + action_text-trix (2.1.19) + railties + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.3.2) - actionpack (= 7.2.3.2) - activejob (= 7.2.3.2) - activerecord (= 7.2.3.2) - activestorage (= 7.2.3.2) - activesupport (= 7.2.3.2) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) - actionmailer (7.2.3.2) - actionpack (= 7.2.3.2) - actionview (= 7.2.3.2) - activejob (= 7.2.3.2) - activesupport (= 7.2.3.2) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3.2) - actionview (= 7.2.3.2) - activesupport (= 7.2.3.2) - cgi + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.3.2) - actionpack (= 7.2.3.2) - activerecord (= 7.2.3.2) - activestorage (= 7.2.3.2) - activesupport (= 7.2.3.2) + actiontext (8.1.3.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3.2) - activesupport (= 7.2.3.2) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_doctor (1.14.0) activerecord (>= 4.2.0) - activejob (7.2.3.2) - activesupport (= 7.2.3.2) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (7.2.3.2) - activesupport (= 7.2.3.2) - activerecord (7.2.3.2) - activemodel (= 7.2.3.2) - activesupport (= 7.2.3.2) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (7.2.3.2) - actionpack (= 7.2.3.2) - activejob (= 7.2.3.2) - activerecord (= 7.2.3.2) - activesupport (= 7.2.3.2) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (7.2.3.2) + activesupport (8.1.3.1) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) - minitest (>= 5.1, < 6) + minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - acts-as-taggable-on (11.0.0) - activerecord (>= 7.0, < 8.0) + uri (>= 0.13.1) + acts-as-taggable-on (13.0.0) + activerecord (>= 7.1, < 8.2) zeitwerk (>= 2.4, < 3.0) addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) @@ -129,7 +130,6 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - cgi (0.5.2) concurrent-ruby (1.3.8) connection_pool (2.4.1) crass (1.0.7) @@ -222,12 +222,13 @@ GEM loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.1) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.2.1) matrix (0.4.3) mime-types (3.5.2) mime-types-data (~> 3.2015) @@ -296,20 +297,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.2.3.2) - actioncable (= 7.2.3.2) - actionmailbox (= 7.2.3.2) - actionmailer (= 7.2.3.2) - actionpack (= 7.2.3.2) - actiontext (= 7.2.3.2) - actionview (= 7.2.3.2) - activejob (= 7.2.3.2) - activemodel (= 7.2.3.2) - activerecord (= 7.2.3.2) - activestorage (= 7.2.3.2) - activesupport (= 7.2.3.2) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 7.2.3.2) + railties (= 8.1.3.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -317,10 +318,9 @@ GEM rails-html-sanitizer (1.7.1) loofah (~> 2.25, >= 2.25.2) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.2.3.2) - actionpack (= 7.2.3.2) - activesupport (= 7.2.3.2) - cgi + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -468,7 +468,7 @@ PLATFORMS DEPENDENCIES active_record_doctor (~> 1.10) - acts-as-taggable-on (~> 11.0.0) + acts-as-taggable-on (~> 13.0) addressable (~> 2.9) administrate administrate-field-active_storage (~> 0.4.1) @@ -476,6 +476,7 @@ DEPENDENCIES administrate-field-list (~> 0.0.6) aws-sdk-rails (~> 5.0) aws-sdk-ses (~> 1.0) + benchmark bootsnap capybara csv @@ -492,7 +493,7 @@ DEPENDENCIES pg (~> 1.1) puma (~> 5.0) rack-cors - rails (~> 7.2.1) + rails (~> 8.1.3) roo (~> 2.9) rspec-rails rubocop diff --git a/app/fields/acts_as_taggable_field.rb b/app/fields/acts_as_taggable_field.rb index 528a3a3..ca78488 100644 --- a/app/fields/acts_as_taggable_field.rb +++ b/app/fields/acts_as_taggable_field.rb @@ -18,6 +18,22 @@ def context options.fetch(:context, @attribute) end + # Administrate::Field::Base#read_value (called from #initialize, before + # #attribute is overridden below has a chance to matter) lazily fetches data via + # `resource.try(attribute)` - and that `attribute` call is polymorphic, so it hits + # our own override below instead of the raw `:tags` attribute key, fetching + # `resource.tag_list` (an array of tag-name strings) instead of `resource.tags` + # (real ActsAsTaggableOn::Tag records). #tags/#delimited/#truncate all expect the + # latter, so this re-derives the same logic using @attribute (the raw ivar) rather + # than the overridden #attribute method. + def read_value(data) + if options.key?(:getter) + return options[:getter].respond_to?(:call) ? options[:getter].call(self) : resource.try(options[:getter]) + end + + data.nil? ? resource.try(@attribute) : data + end + # acts_as_taggable_on exposes a `_list` virtual attribute # (e.g. `tag_list` for the `:tags` context) for reading/writing a comma-delimited # tag string - this field operates on that attribute rather than the raw `tags` diff --git a/bin/dev b/bin/dev new file mode 100644 index 0000000..5f91c20 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/rubocop b/bin/rubocop new file mode 100644 index 0000000..5a20504 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/config/initializers/new_framework_defaults_8_1.rb b/config/initializers/new_framework_defaults_8_1.rb new file mode 100644 index 0000000..8f20895 --- /dev/null +++ b/config/initializers/new_framework_defaults_8_1.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 8.1 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `8.1`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Skips escaping HTML entities and line separators. When set to `false`, the +# JSON renderer no longer escapes these to improve performance. +# +# Example: +# class PostsController < ApplicationController +# def index +# render json: { key: "\u2028\u2029<>&" } +# end +# end +# +# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"

<>&"}` with the config +# set to `false`. +# +# Applications that want to keep the escaping behavior can set the config to `true`. +#++ +# Rails.configuration.action_controller.escape_json_responses = false + +### +# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON. +# +# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019. +# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset. +#++ +# Rails.configuration.active_support.escape_js_separators_in_json = false + +### +# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values +# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or +# `primary_key`) to fall back on. +# +# The current behavior of not raising an error has been deprecated, and this configuration option will be removed in +# Rails 8.2. +#++ +# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true + +### +# Controls how Rails handles path relative URL redirects. +# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError` +# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities. +# +# Example: +# redirect_to "example.com" # Raises UnsafeRedirectError +# redirect_to "@attacker.com" # Raises UnsafeRedirectError +# redirect_to "/safe/path" # Works correctly +# +# Applications that want to allow these redirects can set the config to `:log` (previous default) +# to only log warnings, or `:notify` to send ActiveSupport notifications. +#++ +# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise + +### +# Use a Ruby parser to track dependencies between Action View templates +#++ +# Rails.configuration.action_view.render_tracker = :ruby + +### +# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields +# included in `button_to` forms will omit the `autocomplete="off"` attribute. +# +# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`. +#++ +# Rails.configuration.action_view.remove_hidden_field_autocomplete = true diff --git a/spec/fields/acts_as_taggable_field_spec.rb b/spec/fields/acts_as_taggable_field_spec.rb index 7abbc56..d82b9bd 100644 --- a/spec/fields/acts_as_taggable_field_spec.rb +++ b/spec/fields/acts_as_taggable_field_spec.rb @@ -22,6 +22,24 @@ field = described_class.new(:tags, %w[a b], nil) expect(field.tags).to eq(%w[a b]) end + + it 'fetches real Tag records from the resource when constructed the way Administrate ' \ + 'actually builds fields (data: nil, resource: given), not tag_list name strings' do + # Administrate::Field::Base#read_value calls `resource.try(attribute)` when data is + # nil - and that `attribute` call is polymorphic, landing on this class's own + # #attribute override (returns "tag_list") rather than the raw :tags attribute this + # field was constructed with. A prior version of #read_value used that overridden + # #attribute directly, so this fetched mention.tag_list (tag-name strings) instead of + # mention.tags (real Tag records), and _show.html.erb's `tag.name` raised + # NoMethodError on a String. Only reproducible by exercising the real construction + # path (data: nil) - constructing with data already populated skips read_value + # entirely, which is why this went undetected until manual browser verification. + mention = create(:mention, tag_list: 'battle, correspondence') + field = described_class.new(:tags, nil, nil, resource: mention) + + expect(field.tags).to all(be_a(ActsAsTaggableOn::Tag)) + expect(field.tags.map(&:name)).to contain_exactly('battle', 'correspondence') + end end describe '#delimited' do diff --git a/spec/requests/admin/mentions_spec.rb b/spec/requests/admin/mentions_spec.rb index bc9506c..10363c3 100644 --- a/spec/requests/admin/mentions_spec.rb +++ b/spec/requests/admin/mentions_spec.rb @@ -14,6 +14,16 @@ get admin_mentions_path, headers: admin_auth_headers expect(response).to have_http_status(:ok) end + + # Regression test: ActsAsTaggableField's #tags previously blew up on a real tagged + # mention outside of a bare smoke test, since the untagged factory default never + # exercised the field's actual data-fetching path (see acts_as_taggable_field_spec.rb). + it 'renders the tags of a tagged mention' do + create(:mention, tag_list: 'battle, correspondence') + get admin_mentions_path, headers: admin_auth_headers + expect(response).to have_http_status(:ok) + expect(response.body).to include('battle').and include('correspondence') + end end describe 'GET show' do @@ -22,6 +32,13 @@ get admin_mention_path(mention), headers: admin_auth_headers expect(response).to have_http_status(:ok) end + + it 'renders the tags of a tagged mention' do + mention = create(:mention, tag_list: 'battle, correspondence') + get admin_mention_path(mention), headers: admin_auth_headers + expect(response).to have_http_status(:ok) + expect(response.body).to include('battle').and include('correspondence') + end end describe 'GET new' do @@ -37,5 +54,12 @@ get edit_admin_mention_path(mention), headers: admin_auth_headers expect(response).to have_http_status(:ok) end + + it 'pre-populates the tag list input of a tagged mention' do + mention = create(:mention, tag_list: 'battle, correspondence') + get edit_admin_mention_path(mention), headers: admin_auth_headers + expect(response).to have_http_status(:ok) + expect(response.body).to include('value="battle, correspondence"') + end end end From 49b2aeff450a4b7d6c72999a849246a3ce3cafda Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 12 Aug 2026 09:39:32 -0400 Subject: [PATCH 09/16] Add other publishers to index --- app/models/concerns/letter_common.rb | 42 ++++++++++++++++++---------- app/models/letter.rb | 32 ++------------------- app/models/published_letter.rb | 39 ++++++-------------------- spec/models/published_letter_spec.rb | 9 ++++++ 4 files changed, 47 insertions(+), 75 deletions(-) diff --git a/app/models/concerns/letter_common.rb b/app/models/concerns/letter_common.rb index 8cb701d..cb0ff27 100644 --- a/app/models/concerns/letter_common.rb +++ b/app/models/concerns/letter_common.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Metrics/BlockLength - # Common stuff for indexing module LetterCommon extend ActiveSupport::Concern @@ -9,18 +7,34 @@ module LetterCommon included do attr_accessor :tags - # attr_readonly :content + has_many :mentions, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :entities, -> { distinct }, through: :mentions + + has_many :letter_destinations, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :destinations, -> { distinct }, through: :letter_destinations, source: :entity + + has_many :letter_senders, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :senders, -> { distinct }, through: :letter_senders, source: :entity + + has_many :letter_origins, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :origins, -> { distinct }, through: :letter_origins, source: :entity + + has_many :letter_recipients, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :recipients, -> { distinct }, through: :letter_recipients, source: :entity - # scope :published, lambda { - # includes(:repositories) - # .references(:repositories) - # .where( - # repositories: { - # published: true - # } - # ) - # # .where('letters.date BETWEEN ? AND ?', DateTime.new(1957), DateTime.new(1965, 12).at_end_of_month) - # } + has_many :letter_repositories, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :repositories, -> { distinct }, through: :letter_repositories + + has_many :letter_collections, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :collections, -> { distinct }, through: :letter_collections + + has_many :letter_languages, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy + has_many :languages, -> { distinct }, through: :letter_languages + + belongs_to :letter_file, inverse_of: :letters, optional: true + belongs_to :file_folder, inverse_of: :letters, optional: true + belongs_to :letter_owner, inverse_of: :letters, optional: true + belongs_to :letter_publisher, inverse_of: :letters, optional: true scope :published, -> { where(published: true) } @@ -100,5 +114,3 @@ def volume_title end end end - -# rubocop:enable Metrics/BlockLength diff --git a/app/models/letter.rb b/app/models/letter.rb index f4c780d..6fa0fcb 100644 --- a/app/models/letter.rb +++ b/app/models/letter.rb @@ -8,35 +8,6 @@ class Letter < ApplicationRecord before_destroy :remove_published after_save :reindex_published - has_many :mentions, dependent: :destroy - has_many :entities, -> { distinct }, through: :mentions - - has_many :letter_destinations, dependent: :destroy - has_many :destinations, -> { distinct }, through: :letter_destinations, source: :entity - - has_many :letter_senders, dependent: :destroy - has_many :senders, -> { distinct }, through: :letter_senders, source: :entity - - has_many :letter_origins, dependent: :destroy - has_many :origins, -> { distinct }, through: :letter_origins, source: :entity - - has_many :letter_recipients, dependent: :destroy - has_many :recipients, -> { distinct }, through: :letter_recipients, source: :entity - - has_many :letter_repositories, dependent: :destroy - has_many :repositories, -> { distinct }, through: :letter_repositories - - has_many :letter_collections, dependent: :destroy - has_many :collections, -> { distinct }, through: :letter_collections - - has_many :letter_languages, dependent: :destroy - has_many :languages, -> { distinct }, through: :letter_languages - - belongs_to :letter_file, optional: true - belongs_to :file_folder, optional: true - belongs_to :letter_owner, optional: true - belongs_to :letter_publisher, optional: true - def search_data { id_path: url_path, @@ -51,7 +22,8 @@ def search_data repositories: repositories.map(&:label), languages: languages.map(&:label), published:, - volume: volume.to_s + volume: volume.to_s, + fuck: 'me' } end diff --git a/app/models/published_letter.rb b/app/models/published_letter.rb index f50edc6..9f1936a 100644 --- a/app/models/published_letter.rb +++ b/app/models/published_letter.rb @@ -6,35 +6,6 @@ class PublishedLetter < ApplicationRecord self.table_name = 'letters' - has_many :mentions, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :entities, -> { distinct }, through: :mentions - - has_many :letter_destinations, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :destinations, -> { distinct }, through: :letter_destinations, source: :entity - - has_many :letter_senders, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :senders, -> { distinct }, through: :letter_senders, source: :entity - - has_many :letter_origins, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :origins, -> { distinct }, through: :letter_origins, source: :entity - - has_many :letter_recipients, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :recipients, -> { distinct }, through: :letter_recipients, source: :entity - - has_many :letter_repositories, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :repositories, -> { distinct }, through: :letter_repositories - - has_many :letter_collections, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :collections, -> { distinct }, through: :letter_collections - - has_many :letter_languages, foreign_key: 'letter_id', inverse_of: :letter, dependent: :destroy - has_many :languages, -> { distinct }, through: :letter_languages - - belongs_to :letter_file, foreign_key: 'letter_id', inverse_of: :letters, optional: true - belongs_to :file_folder, foreign_key: 'letter_id', inverse_of: :letters, optional: true - belongs_to :letter_owner, foreign_key: 'letter_id', inverse_of: :letters, optional: true - belongs_to :letter_publisher, foreign_key: 'letter_id', inverse_of: :letters, optional: true - def self.default_scope where(published: true) end @@ -54,11 +25,19 @@ def search_data languages: languages.map(&:label), published:, volume: volume.to_s, - publisher: letter_publisher&.label + other_publishers: } end def should_index? published end + + def other_publishers + return if letter_publisher.nil? + + doc = Nokogiri::HTML(letter_publisher.label) + text = doc.css('i').map(&:text) + text ? text.map(&:strip) : [letter_publisher.label] + end end diff --git a/spec/models/published_letter_spec.rb b/spec/models/published_letter_spec.rb index 6fc8e53..80176cc 100644 --- a/spec/models/published_letter_spec.rb +++ b/spec/models/published_letter_spec.rb @@ -42,4 +42,13 @@ expect(published_letter.repositories.count).to eq(2) expect(published_letter.search_data[:repositories].count).to eq(1) end + + it 'represents previous publisher' do + publisher = create(:letter_publisher, + label: 'Alan, "Letters" Endgame , Voice Ed. Maurice. (1998) 37.') + letter = create(:published_letter, letter_publisher: publisher) + published_letter = described_class.find(letter.id) + expect(published_letter.search_data[:other_publishers]).to(include('Endgame')) + expect(published_letter.search_data[:other_publishers]).to(include('Voice')) + end end From 9f83b3c91b5bd7d16d7f16de6758b3e1584ebe53 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 12 Aug 2026 09:39:54 -0400 Subject: [PATCH 10/16] Bump Ruby version for build --- .github/workflows/test.yml | 2 +- Dockerfile | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8544b80..8dd7ebc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,7 @@ jobs: - name: Install system requirements run: | sudo apt update - sudo apt install -y postgresql-client + sudo apt install -y postgresql-client libvips42 - name: Bundle Install run: | diff --git a/Dockerfile b/Dockerfile index 64a860f..acf172b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.3.4 +FROM ruby:3.4.10 ARG RAILS_MASTER_KEY ARG REDIS_URL @@ -6,6 +6,7 @@ ARG REDIS_URL RUN apt-get update \ && apt-get install -y --no-install-recommends \ postgresql-client \ + libvips42 \ && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app From 6d360ec46e33d0080e79939443d2e5b4e3c9e9f9 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 12:50:10 -0400 Subject: [PATCH 11/16] Bump roo 2.10.1 -> 3.0.0 and httparty 0.20.0 -> 0.24.2 Both deferred from the earlier dependency-hardening PR since they're major version bumps needing their own verification. roo 3.0 has no API-level breaking changes to spreadsheet reading (open/cell access/sheet iteration) per its changelog - just needs Ruby 3.1+ (have 3.4.10) and pulls rubyzip to 3.x, unconstrained by anything else in the Gemfile. httparty 0.24.2 fixes a High-severity SSRF/API-key-leakage CVE (GHSA-hm5p-x4rq-38w4) by restricting credential-forwarding on redirects to same-host. Every call site in this app (lib/import.rb, app/jobs/import_data_job.rb) hits a single hardcoded host directly, with no base_uri set and no credentials passed, so no behavior change expected - verified via full suite + clean boot (no dedicated spec coverage exists for either file either way). Verified: full suite (308 examples, 0 failures), rubocop clean. Co-Authored-By: Claude Sonnet 5 --- Gemfile | 4 ++-- Gemfile.lock | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index e00eeee..87e87f9 100644 --- a/Gemfile +++ b/Gemfile @@ -84,9 +84,9 @@ gem 'acts-as-taggable-on', '~> 13.0' gem 'administrate-field-list', '~> 0.0.6' -gem 'httparty', '~> 0.20.0' +gem 'httparty', '~> 0.24.0' -gem 'roo', '~> 2.9' +gem 'roo', '~> 3.0' gem 'namae', '~> 1.1' diff --git a/Gemfile.lock b/Gemfile.lock index 3b4bc5b..a70b141 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -184,8 +184,9 @@ GEM globalid (1.2.1) activesupport (>= 6.1) hashie (5.0.0) - httparty (0.20.0) - mime-types (~> 3.0) + httparty (0.24.2) + csv + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) i18n (1.14.8) concurrent-ruby (~> 1.0) @@ -230,17 +231,14 @@ GEM net-smtp marcel (1.2.1) matrix (0.4.3) - mime-types (3.5.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2024.1203) mini_magick (4.13.2) mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.4) multi_json (1.21.1) - multi_xml (0.7.2) - bigdecimal (~> 3.1) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) namae (1.2.0) racc (~> 1.7) net-http (0.9.1) @@ -337,9 +335,12 @@ GEM reline (0.5.12) io-console (~> 0.5) rexml (3.4.4) - roo (2.10.1) + roo (3.0.0) + base64 (~> 0.2) + csv (~> 3) + logger (~> 1) nokogiri (~> 1) - rubyzip (>= 1.3.0, < 3.0.0) + rubyzip (>= 3.0.0, < 4.0.0) rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) @@ -386,7 +387,7 @@ GEM ruby-vips (2.2.5) ffi (~> 1.12) logger - rubyzip (2.3.2) + rubyzip (3.4.1) sassc (2.4.0) ffi (~> 1.9) sassc-rails (2.1.2) @@ -484,7 +485,7 @@ DEPENDENCIES elasticsearch (~> 8) factory_bot_rails faker - httparty (~> 0.20.0) + httparty (~> 0.24.0) image_processing (~> 1.2) importmap-rails (~> 2.0) jbuilder (~> 2.11) @@ -494,7 +495,7 @@ DEPENDENCIES puma (~> 5.0) rack-cors rails (~> 8.1.3) - roo (~> 2.9) + roo (~> 3.0) rspec-rails rubocop rubocop-factory_bot From fe02be2557ef47c3432ba3b59cac2a15b137cc21 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 13:30:17 -0400 Subject: [PATCH 12/16] Bump searchkick 5.3.1 -> 6.1.2 No breaking changes to the .search API (where/load/order) per the changelog. Removed its hard dependency on the elasticsearch gem (now optional, bring-your-own client) - decoupled from any future ES9 bump, not blocked by it. Surfaced two real, pre-existing bugs neither caught by tests before: - LettersController/EntitiesController's `before_action :reindex` calls `Letter.reindex`/`Entity.reindex` (class-level, full reindex) with no explicit mode, relying on Searchkick's default mode resolution to pick a synchronous reindex before the search that immediately follows. That resolution (Searchkick::Index#reindex_records) falls back to the model's own `callbacks:` searchkick option when no mode is given - and Searchable sets `callbacks: :async`. Whether that fallback applied to explicit `.reindex` calls the same way in 5.x is unclear, but it does now: the reindex enqueued an async job instead of running inline, and the search that followed sometimes ran before it completed, intermittently missing the ES mapping entirely ("No mapping found for [date]"). Fixed by passing `mode: :inline` explicitly in both controllers' reindex methods. - LettersController#letters_from_results called `letter.orgins` (typo for `origins`) on a Searchkick::HashWrapper. 5.x's HashWrapper silently returned nil for the typo'd key; 6.x raises NoMethodError instead. Fixed the typo and added regression coverage - nothing had ever asserted on the `origins` key in a letters index response before. Verified: full suite (309 examples, 0 failures), rubocop clean. Co-Authored-By: Claude Sonnet 5 --- Gemfile.lock | 6 ++---- app/controllers/entities_controller.rb | 8 ++++++-- app/controllers/letters_controller.rb | 10 +++++++--- spec/requests/letters_spec.rb | 11 +++++++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a70b141..d4da143 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,7 +183,6 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - hashie (5.0.0) httparty (0.24.2) csv mini_mime (>= 1.0.0) @@ -396,9 +395,8 @@ GEM sprockets (> 3.0) sprockets-rails tilt - searchkick (5.3.1) - activemodel (>= 6.1) - hashie + searchkick (6.1.2) + activemodel (>= 7.2) securerandom (0.3.2) selenium-devtools (0.151.0) selenium-webdriver (~> 4.2) diff --git a/app/controllers/entities_controller.rb b/app/controllers/entities_controller.rb index 6ffd2c7..073cd32 100644 --- a/app/controllers/entities_controller.rb +++ b/app/controllers/entities_controller.rb @@ -69,8 +69,12 @@ def letters private def reindex - Entity.reindex if ENV['RAILS_ENV'] == 'test' - PublishedEntity.reindex if ENV['RAILS_ENV'] == 'test' + # mode: :inline forces a synchronous reindex regardless of Searchable's + # `callbacks: :async` option - searchkick's default mode resolution falls back to + # the model's callbacks option (see Searchkick::Index#reindex_records), so without + # this the search immediately below could run against a not-yet-indexed record. + Entity.reindex(mode: :inline) if ENV['RAILS_ENV'] == 'test' + PublishedEntity.reindex(mode: :inline) if ENV['RAILS_ENV'] == 'test' rescue Searchkick::Error end diff --git a/app/controllers/letters_controller.rb b/app/controllers/letters_controller.rb index 94434e9..96f1ed8 100644 --- a/app/controllers/letters_controller.rb +++ b/app/controllers/letters_controller.rb @@ -55,7 +55,7 @@ def letters_from_results(results) volume: letter.volume, recipients: letter.recipients, destinations: letter.destinations, - origins: letter.orgins, + origins: letter.origins, mentions: letter.mentions } end @@ -134,8 +134,12 @@ def set_filters # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def reindex - Letter.reindex if ENV['RAILS_ENV'] == 'test' - PublishedLetter.reindex if ENV['RAILS_ENV'] == 'test' + # mode: :inline forces a synchronous reindex regardless of Searchable's + # `callbacks: :async` option - searchkick's default mode resolution falls back to + # the model's callbacks option (see Searchkick::Index#reindex_records), so without + # this the search immediately below could run against a not-yet-indexed record. + Letter.reindex(mode: :inline) if ENV['RAILS_ENV'] == 'test' + PublishedLetter.reindex(mode: :inline) if ENV['RAILS_ENV'] == 'test' end # Only allow a list of trusted parameters through. diff --git a/spec/requests/letters_spec.rb b/spec/requests/letters_spec.rb index 04ef8bf..81844e8 100644 --- a/spec/requests/letters_spec.rb +++ b/spec/requests/letters_spec.rb @@ -102,6 +102,17 @@ expect(json[:letters].count).to eq(9) end + # Regression test: LettersController#letters_from_results called `letter.orgins` + # (typo for `origins`) on a Searchkick::HashWrapper. Searchkick 5.x silently + # returned nil for the typo'd key; 6.x raises NoMethodError instead, surfacing a + # bug that was always there but never covered - nothing asserted on the `origins` + # key in a response before this. + it 'includes the origins of a letter' do + create(:published_letter, origins: create_list(:place_entity, 1, label: 'Reynoldstown')) + get "#{letters_url}.json" + expect(json[:letters].first[:origins]).to eq(['Reynoldstown']) + end + it 'returns letters with start_date param' do create_list(:published_letter, 10) create(:published_letter, date: DateTime.new(1961, 2, 22)) From 9c91f9ba2befa1edc3e3978fe1bc659c2f8287f5 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 14:28:20 -0400 Subject: [PATCH 13/16] Bump image_processing 1.14.0 -> 2.0.3 Fixes several real RCE vulnerabilities (remote code execution via user-influenced operation names through #operation/#method_missing/#send, and unsafe minimagick loader/saver option names). mini_magick/ruby-vips became soft dependencies in 2.0 and must be added explicitly now - image_processing 1.x's gemspec hard-required both unconditionally. Initially added mini_magick, assuming it was the active processor since Rails defaults ActiveStorage.variant_processor to :mini_magick. That was wrong: this app calls config.load_defaults 7.0 (config/application.rb), and Rails' 7.0 defaults block (config/application/configuration.rb) unconditionally sets config.active_storage.variant_processor = :vips - vips has been the actually-active processor the whole time, with mini_magick present only as an unused transitive dependency. Confirmed by generating a real image variant, not just booting cleanly. Added ruby-vips instead. Verified: full suite (309 examples, 0 failures), rubocop clean, and an actual end-to-end variant generation via `rails runner` (attach a real image, call .variant(...).processed, confirm it produces a real blob) - not just a clean boot, since this exact class of bug (config silently depending on a gem that happened to be present transitively) only shows up when the feature is actually exercised. Co-Authored-By: Claude Sonnet 5 --- Gemfile | 8 +++++++- Gemfile.lock | 10 ++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 87e87f9..e8d561c 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,13 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'bootsnap', require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -gem 'image_processing', '~> 1.2' +gem 'image_processing', '~> 2.0' +# mini_magick/ruby-vips became soft dependencies of image_processing in 2.0 and must be +# added explicitly. This app's config.load_defaults 7.0 (config/application.rb) sets +# config.active_storage.variant_processor = :vips - vips is the actually-active processor, +# not mini_magick (previously present only as an unused transitive dependency of +# image_processing 1.x's gemspec, which required both backends unconditionally). +gem 'ruby-vips', '~> 2.0' # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible gem 'rack-cors' diff --git a/Gemfile.lock b/Gemfile.lock index d4da143..4c480d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -189,9 +189,7 @@ GEM multi_xml (>= 0.5.2) i18n (1.14.8) concurrent-ruby (~> 1.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) - ruby-vips (>= 2.0.17, < 3) + image_processing (2.0.3) importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -230,7 +228,6 @@ GEM net-smtp marcel (1.2.1) matrix (0.4.3) - mini_magick (4.13.2) mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -383,7 +380,7 @@ GEM rubocop (~> 1.61) rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) - ruby-vips (2.2.5) + ruby-vips (2.3.0) ffi (~> 1.12) logger rubyzip (3.4.1) @@ -484,7 +481,7 @@ DEPENDENCIES factory_bot_rails faker httparty (~> 0.24.0) - image_processing (~> 1.2) + image_processing (~> 2.0) importmap-rails (~> 2.0) jbuilder (~> 2.11) kaminari (~> 1.2) @@ -500,6 +497,7 @@ DEPENDENCIES rubocop-rails rubocop-rspec rubocop-rspec_rails + ruby-vips (~> 2.0) sassc-rails searchkick selenium-devtools From a44780d6345ee0593c1e7872b8957a64a776aa4e Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 14:48:29 -0400 Subject: [PATCH 14/16] Bump puma 5.6.9 -> 8.0.2 Reviewed all four majors' upgrade guides (6.0/7.0/8.0-Upgrade.md) for anything that could affect this app: hook renames, removed Server#min/ max_threads, HTTP method allowlisting, response header lowercasing - none of it touches config/puma.rb (no hooks, no workers/cluster mode configured) or anything this app depends on. The one real behavioral change: Puma 8 defaults to binding IPv6 (::) instead of IPv4 (0.0.0.0) when a non-loopback IPv6 interface is available. This app deploys to AWS ECS/Fargate, and whether that networking setup handles the new default cleanly isn't verifiable from here. Puma's own upgrade guide recommends pinning the bind explicitly to preserve exact current behavior - added `port ENV.fetch('PORT', 3000), '0.0.0.0'` to config/puma.rb rather than gambling on it. Confirmed via a real (non-test- suite) server boot that it actually listens on 0.0.0.0 as pinned. No dependency conflicts - puma itself has no rack version constraint at all, unlike sidekiq 8 (which hard-requires rack >= 3.2.0 and was deferred alongside rack-cors for that reason). Verified: full suite (309 examples, 0 failures) including system specs, which boot a real Puma server via Capybara/Selenium - the exact code path Puma's own 6.0 changelog warned could break for gems depending on removed Server internals. rubocop clean. Co-Authored-By: Claude Sonnet 5 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- config/puma.rb | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index e8d561c..f75148a 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem 'elasticsearch', '~> 8' gem 'searchkick' # Use the Puma web server [https://github.com/puma/puma] -gem 'puma', '~> 5.0' +gem 'puma', '~> 8.0' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock index 4c480d9..963bd93 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -277,7 +277,7 @@ GEM psych (5.1.2) stringio public_suffix (7.0.5) - puma (5.6.9) + puma (8.0.2) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) @@ -487,7 +487,7 @@ DEPENDENCIES kaminari (~> 1.2) namae (~> 1.1) pg (~> 1.1) - puma (~> 5.0) + puma (~> 8.0) rack-cors rails (~> 8.1.3) roo (~> 3.0) diff --git a/config/puma.rb b/config/puma.rb index 1713441..7cdef17 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -17,7 +17,10 @@ # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch('PORT', 3000) +# Puma 8 defaults to binding IPv6 (::) instead of IPv4 (0.0.0.0) when a non-loopback +# IPv6 interface is available. Pinning the host explicitly preserves this app's +# existing IPv4 behavior regardless of Puma version or the host's network interfaces. +port ENV.fetch('PORT', 3000), '0.0.0.0' # Specifies the `environment` that Puma will run in. # From ae62bfade57e755cb3f4ba4af8458f5ec0cb0ef1 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Tue, 11 Aug 2026 15:01:23 -0400 Subject: [PATCH 15/16] Bump dev/test toolchain: rspec-rails, shoulda-matchers, rubocop family rspec-rails 6.1.5 -> 8.0.4, shoulda-matchers 6.4.0 -> 8.0.1, rubocop 1.65.1 -> 1.89.0, rubocop-{rails,rspec,factory_bot,rspec_rails} to latest. All dev/test-only, no production runtime impact - verified by running the actual test suite and linter rather than reading changelogs, since a failure here surfaces immediately as a spec or lint failure. Applied the handful of newly-surfaced, genuinely safe rubocop fixes (redundant safe-navigation chains, a no-op Array#flatten after a string-returning #map, order('position') -> order(:position)) - all verified equivalent by reading the surrounding code, not blindly autocorrected. Deliberately left Rails/StrongParametersExpect alone (18 call sites across 6 controllers) even though most are auto-correctable: params.expect(...) raises ActionController::ParameterMissing/ParameterInvalid where bare params[:key] silently returns nil, a real behavior change across every public-facing endpoint in this app. That's its own deliberate audit, not a mechanical sweep. Also left Naming/PredicateMethod (flags a pre-existing dead `# TODO: Add authentication logic here` stub) and RSpec/Output (a false positive - it's rails_helper.rb boot-time boilerplate that prints and hard-exits on pending migrations, not a leftover debug print in a spec) - both pre-existing, out of scope here. Verified: full suite (309 examples, 0 failures) both before and after the manual fixes, rubocop clean except the deliberately-deferred items above. Co-Authored-By: Claude Sonnet 5 --- Gemfile.lock | 66 ++++++++++++++++------------ app/dashboards/entity_dashboard.rb | 1 + app/jobs/load_big_sam_job.rb | 3 +- app/models/concerns/entity_common.rb | 4 +- app/models/entity.rb | 4 +- app/models/faq.rb | 2 +- spec/factories/entities.rb | 3 -- spec/jobs/load_big_sam_job_spec.rb | 2 +- 8 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 963bd93..c7357ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -216,6 +216,7 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_server-protocol (3.17.0.6) + lint_roller (1.1.0) logger (1.6.6) loofah (2.25.2) crass (~> 1.0.2) @@ -274,6 +275,7 @@ GEM ast (~> 2.4.1) racc pg (1.5.9) + prism (1.9.0) psych (5.1.2) stringio public_suffix (7.0.5) @@ -345,40 +347,46 @@ GEM rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.5) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) - rubocop (1.65.1) + rubocop (1.89.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.3) - parser (>= 3.3.1.0) - rubocop-factory_bot (2.26.1) - rubocop (~> 1.61) - rubocop-rails (2.26.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-factory_bot (2.28.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rails (2.36.0) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.52.0, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.5) - rubocop (~> 1.61) - rubocop-rspec_rails (2.30.0) - rubocop (~> 1.61) - rubocop-rspec (~> 3, >= 3.0.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rspec (3.10.2) + lint_roller (~> 1.1) + regexp_parser (>= 2.0) + rubocop (~> 1.86, >= 1.86.2) + rubocop-rspec_rails (2.32.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) ruby-progressbar (1.13.0) ruby-vips (2.3.0) ffi (~> 1.12) @@ -403,8 +411,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - shoulda-matchers (6.4.0) - activesupport (>= 5.2.0) + shoulda-matchers (8.0.1) + activesupport (>= 7.2) sidekiq (7.3.10) base64 connection_pool (>= 2.3.0, < 3) diff --git a/app/dashboards/entity_dashboard.rb b/app/dashboards/entity_dashboard.rb index 2c6101d..d55f8aa 100644 --- a/app/dashboards/entity_dashboard.rb +++ b/app/dashboards/entity_dashboard.rb @@ -4,6 +4,7 @@ class EntityDashboard < Administrate::BaseDashboard include ActionView::Helpers::SanitizeHelper + # ATTRIBUTE_TYPES # a hash that describes the type of each of the model's fields. # diff --git a/app/jobs/load_big_sam_job.rb b/app/jobs/load_big_sam_job.rb index e033577..191f072 100644 --- a/app/jobs/load_big_sam_job.rb +++ b/app/jobs/load_big_sam_job.rb @@ -5,6 +5,7 @@ class LoadBigSamJob < ApplicationJob include ActionView::Helpers::SanitizeHelper + queue_as :default # Raised to abandon a single row (e.g. an unparseable date) without treating it as a @@ -372,7 +373,7 @@ def get_entity(label: nil, type: nil, return_nil: false) def get_person(name) entity = nil names = Namae.parse(name).first - if names&.given && names&.family + if names&.given && names.family names.family = "Van #{names.family}" if names.particle&.downcase == 'van' names.family = "von #{names.family}" if names.particle&.downcase == 'von' names = mc_or_mac?(names) diff --git a/app/models/concerns/entity_common.rb b/app/models/concerns/entity_common.rb index 86adf35..1416591 100644 --- a/app/models/concerns/entity_common.rb +++ b/app/models/concerns/entity_common.rb @@ -198,7 +198,7 @@ def short_display lines.push("Date #{date_str}") if date_str.present? end - paragraphs = lines.map {|line| "

#{line}

" }.flatten.join.strip + paragraphs = lines.map {|line| "

#{line}

" }.join.strip Loofah.fragment("
#{paragraphs}
").scrub!(:prune).to_html end @@ -299,7 +299,7 @@ def full_display rows.push("See Also#{link_list}") if links.present? end - table_rows = rows.map {|row| "#{row}" }.flatten.join.strip + table_rows = rows.map {|row| "#{row}" }.join.strip Loofah.fragment("#{table_rows}
").scrub!(:prune).to_html end diff --git a/app/models/entity.rb b/app/models/entity.rb index dafc7b7..37287a6 100644 --- a/app/models/entity.rb +++ b/app/models/entity.rb @@ -104,9 +104,9 @@ def concat_label "#{event_types&.map(&:titleize)&.join(',')} #{description}".strip end when 'person' - if label && (last_name.nil? && first_name.nil?) + if label && last_name.nil? && first_name.nil? names = Namae.parse label - if names&.first&.given && names&.first&.family + if names&.first&.given && names&.first.family self.first_name = names.first.given self.last_name = names.first.family end diff --git a/app/models/faq.rb b/app/models/faq.rb index 8778860..1c07a89 100644 --- a/app/models/faq.rb +++ b/app/models/faq.rb @@ -4,7 +4,7 @@ class Faq < ApplicationRecord before_save :initial_position after_save :update_position - default_scope { order('position') } + default_scope { order(:position) } private diff --git a/spec/factories/entities.rb b/spec/factories/entities.rb index 865c590..52e8793 100644 --- a/spec/factories/entities.rb +++ b/spec/factories/entities.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Layout/LineLength - FactoryBot.define do factory :entity do label { Faker::Movies::HitchhikersGuideToTheGalaxy.character } @@ -170,4 +168,3 @@ end end end -# rubocop:enable Layout/LineLength diff --git a/spec/jobs/load_big_sam_job_spec.rb b/spec/jobs/load_big_sam_job_spec.rb index aaf26b0..87464dd 100644 --- a/spec/jobs/load_big_sam_job_spec.rb +++ b/spec/jobs/load_big_sam_job_spec.rb @@ -262,7 +262,7 @@ def valid_row(overrides = {}) skipped = job.instance_variable_get(:@row_skipped) expect(skipped.size).to eq(1) expect(skipped.first[:code]).to eq('BAD') - expect(skipped.first[:reason]).to match(/bad date/) + expect(skipped.first[:reason]).to include('bad date') expect(Letter.find_by(legacy_pk: 955).code).to eq('GOOD') end From bfd971fa8906907cc04855cdaafc36cab87310f9 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Thu, 13 Aug 2026 09:57:35 -0400 Subject: [PATCH 16/16] Exclude empty strings from other publishers --- Gemfile.lock | 1 + app/models/published_letter.rb | 2 +- spec/models/published_letter_spec.rb | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9af0f51..c7357ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -483,6 +483,7 @@ DEPENDENCIES benchmark bootsnap capybara + csv debug elasticsearch (~> 8) factory_bot_rails diff --git a/app/models/published_letter.rb b/app/models/published_letter.rb index 9f1936a..d95a00b 100644 --- a/app/models/published_letter.rb +++ b/app/models/published_letter.rb @@ -38,6 +38,6 @@ def other_publishers doc = Nokogiri::HTML(letter_publisher.label) text = doc.css('i').map(&:text) - text ? text.map(&:strip) : [letter_publisher.label] + text ? text.map(&:strip).reject!(&:empty?) : [letter_publisher.label] end end diff --git a/spec/models/published_letter_spec.rb b/spec/models/published_letter_spec.rb index 80176cc..67a63a5 100644 --- a/spec/models/published_letter_spec.rb +++ b/spec/models/published_letter_spec.rb @@ -45,10 +45,12 @@ it 'represents previous publisher' do publisher = create(:letter_publisher, - label: 'Alan, "Letters" Endgame , Voice Ed. Maurice. (1998) 37.') + label: 'Alan, "Letters" Endgame , Voice Ed. Maurice. (1998) 37.') letter = create(:published_letter, letter_publisher: publisher) published_letter = described_class.find(letter.id) + expect(published_letter.search_data[:other_publishers].count).to(eq(2)) expect(published_letter.search_data[:other_publishers]).to(include('Endgame')) expect(published_letter.search_data[:other_publishers]).to(include('Voice')) + expect(published_letter.search_data[:other_publishers]).not_to(include(' ')) end end