diff --git a/app/assets/javascripts/source_filters.js b/app/assets/javascripts/source_filters.js new file mode 100644 index 000000000..c8d91541e --- /dev/null +++ b/app/assets/javascripts/source_filters.js @@ -0,0 +1,36 @@ +var SourceFilters = { + lastId: function () { + var existing_list_item_ids = $(".source-filter-form").map(function (i, c) { return $(c).data("id-in-filter-list") }); + if (existing_list_item_ids.length == 0) return 0; + return Math.max.apply(null, existing_list_item_ids) + 1; + }, + + add: function () { + var new_form = $($('#source-filter-template').clone().html().replace(/REPLACE_ME/g, SourceFilters.lastId())); + new_form.appendTo('#source-filter-list'); + + return false; // Stop form being submitted + }, + + addBlockFilter: function () { + var new_form = $($('#source-filter-template').clone().html().replace(/REPLACE_ME/g, SourceFilters.lastId()).replace(/allow/, 'block')); + new_form.appendTo('#source-block-list'); + + return false; // Stop form being submitted + }, + + delete: function () { + $(this).parents('.source-filter-form').fadeOut().find("input[name$='[_destroy]']").val("true"); + } +}; + +document.addEventListener("turbolinks:load", function () { + $('#source-filters') + .on('click', '#add-source-filter-btn', SourceFilters.add) + .on('click', '#add-source-filter-btn-label', SourceFilters.add) + .on('click', '.delete-source-filter-btn', SourceFilters.delete); + $('#source-block-filters') + .on('click', '#add-source-block-filter-btn', SourceFilters.addBlockFilter) + .on('click', '#add-source-block-filter-btn-label', SourceFilters.addBlockFilter) + .on('click', '.delete-source-filter-btn', SourceFilters.delete); +}); diff --git a/app/assets/stylesheets/sources.scss b/app/assets/stylesheets/sources.scss new file mode 100644 index 000000000..3d96aba25 --- /dev/null +++ b/app/assets/stylesheets/sources.scss @@ -0,0 +1,9 @@ +.source-filter-form { + display: flex; + gap: 1em; + margin-bottom: 4px; + + label { + margin-right: 4px; + } +} \ No newline at end of file diff --git a/app/controllers/sources_controller.rb b/app/controllers/sources_controller.rb index cd35bf435..5d4edddfb 100644 --- a/app/controllers/sources_controller.rb +++ b/app/controllers/sources_controller.rb @@ -1,6 +1,6 @@ class SourcesController < ApplicationController - before_action :set_source, except: [:index, :new, :create, :check_exists] - before_action :set_content_provider, except: [:index, :check_exists] + before_action :set_source, except: %i[index new create check_exists] + before_action :set_content_provider, except: %i[index check_exists] before_action :set_breadcrumbs include SearchableIndex @@ -65,8 +65,8 @@ def check_exists end else respond_to do |format| - format.html { render :nothing => true, :status => 200, :content_type => 'text/html' } - format.json { render json: {}, :status => 200, :content_type => 'application/json' } + format.html { render nothing: true, status: 200, content_type: 'text/html' } + format.json { render json: {}, status: 200, content_type: 'application/json' } end end end @@ -75,6 +75,7 @@ def check_exists # PATCH/PUT /sources/1.json def update authorize @source + respond_to do |format| if @source.update(source_params) @source.create_activity(:update, owner: current_user) if @source.log_update_activity? @@ -94,8 +95,10 @@ def destroy @source.create_activity :destroy, owner: current_user @source.destroy respond_to do |format| - format.html { redirect_to policy(Source).index? ? sources_path : content_provider_path(@content_provider), - notice: 'Source was successfully deleted.' } + format.html do + redirect_to policy(Source).index? ? sources_path : content_provider_path(@content_provider), + notice: 'Source was successfully deleted.' + end format.json { head :no_content } end end @@ -106,7 +109,7 @@ def test @source.test_job_id = job_id respond_to do |format| - format.json { render json: { id: job_id }} + format.json { render json: { id: job_id } } end end @@ -150,11 +153,11 @@ def set_content_provider # Never trust parameters from the scary internet, only allow the white list through. def source_params - permitted = [:url, :method, :token, :default_language, :enabled] + permitted = %i[url method token default_language enabled source_filters] permitted << :approval_status if policy(@source || Source).approve? permitted << :content_provider_id if policy(Source).index? - params.require(:source).permit(permitted) + params.require(:source).permit(permitted, source_filters_attributes: %i[id filter_mode filter_by filter_value _destroy]) end def set_breadcrumbs @@ -164,7 +167,7 @@ def set_breadcrumbs add_breadcrumb 'Sources', content_provider_path(@content_provider, anchor: 'sources') if params[:id] - add_breadcrumb @source.title, content_provider_source_path(@content_provider, @source) if (@source && !@source.new_record?) + add_breadcrumb @source.title, content_provider_source_path(@content_provider, @source) if @source && !@source.new_record? add_breadcrumb action_name.capitalize.humanize, request.path unless action_name == 'show' elsif action_name != 'index' add_breadcrumb action_name.capitalize.humanize, request.path @@ -173,5 +176,4 @@ def set_breadcrumbs super end end - end diff --git a/app/models/source.rb b/app/models/source.rb index 889bf44a0..3d8459d89 100644 --- a/app/models/source.rb +++ b/app/models/source.rb @@ -17,12 +17,13 @@ class Source < ApplicationRecord belongs_to :user belongs_to :content_provider + has_many :source_filters, dependent: :destroy validates :url, :method, presence: true validates :url, url: true validates :approval_status, inclusion: { in: APPROVAL_STATUS.values } - validates :method, inclusion: { in: -> (_) { TeSS::Config.user_ingestion_methods } }, - unless: -> { User.current_user&.is_admin? || User.current_user&.has_role?(:scraper_user) } + validates :method, inclusion: { in: ->(_) { TeSS::Config.user_ingestion_methods } }, + unless: -> { User.current_user&.is_admin? || User.current_user&.has_role?(:scraper_user) } validates :default_language, controlled_vocabulary: { dictionary: 'LanguageDictionary', allow_blank: true } validate :check_method @@ -31,6 +32,8 @@ class Source < ApplicationRecord before_update :log_approval_status_change before_update :reset_approval_status + accepts_nested_attributes_for :source_filters, allow_destroy: true + if TeSS::Config.solr_enabled # :nocov: searchable do @@ -45,7 +48,7 @@ class Source < ApplicationRecord ingestor_title end string :content_provider do - self.content_provider.try(:title) + content_provider.try(:title) end string :node, multiple: true do associated_nodes.pluck(:name) @@ -73,18 +76,16 @@ def ingestor_class end def self.facet_fields - field_list = %w( content_provider node method enabled approval_status ) + field_list = %w[content_provider node method enabled approval_status] field_list.delete('node') unless TeSS::Config.feature['nodes'] field_list end def self.check_exists(source_params) - given_source = self.new(source_params) + given_source = new(source_params) source = nil - if given_source.url.present? - source = self.find_by_url(given_source.url) - end + source = find_by_url(given_source.url) if given_source.url.present? source end @@ -135,30 +136,45 @@ def self.approval_required? TeSS::Config.feature['user_source_creation'] && !User.current_user&.is_admin? end + def passes_filter?(item) + passes = false + allow_all = true + + source_filters.each do |filter| + if filter.allow? + allow_all = false + passes ||= filter.match(item) + elsif filter.block? && filter.match(item) + return false + end + end + + passes || allow_all + end + private def set_approval_status - if self.class.approval_required? - self.approval_status = :not_approved - else - self.approval_status = :approved - end + self.approval_status = if self.class.approval_required? + :not_approved + else + :approved + end end def reset_approval_status - if self.class.approval_required? - if method_changed? || url_changed? - self.approval_status = :not_approved - end - end + return unless self.class.approval_required? + return unless method_changed? || url_changed? + + self.approval_status = :not_approved end def log_approval_status_change - if approval_status_changed? - old = (APPROVAL_STATUS[approval_status_before_last_save.to_i] || APPROVAL_STATUS[0]).to_s - new = approval_status.to_s - create_activity(:approval_status_changed, owner: User.current_user, parameters: { old: old, new: new }) - end + return unless approval_status_changed? + + old = (APPROVAL_STATUS[approval_status_before_last_save.to_i] || APPROVAL_STATUS[0]).to_s + new = approval_status.to_s + create_activity(:approval_status_changed, owner: User.current_user, parameters: { old:, new: }) end def loggable_changes diff --git a/app/models/source_filter.rb b/app/models/source_filter.rb new file mode 100644 index 000000000..1518c86d5 --- /dev/null +++ b/app/models/source_filter.rb @@ -0,0 +1,68 @@ +class SourceFilter < ApplicationRecord + belongs_to :source + + auto_strip_attributes :filter_value + validates :filter_mode, :filter_by, presence: true + + enum :filter_by, { + target_audience: 'target_audience', + keyword: 'keyword', + title: 'title', + description: 'description', + description_contains: 'description_contains', + url: 'url', + url_prefix: 'url_prefix', + doi: 'doi', + license: 'license', + difficulty_level: 'difficulty_level', + resource_type: 'resource_type', + prerequisites_contains: 'prerequisites_contains', + learning_objectives_contains: 'learning_objectives_contains', + subtitle: 'subtitle', + subtitle_contains: 'subtitle_contains', + city: 'city', + country: 'country', + event_type: 'event_type', + timezone: 'timezone' + } + + enum :filter_mode, { + allow: 'allow', + block: 'block' + } + + def match(item) + return false unless item.respond_to?(filter_property) + + val = item.send(filter_property) + + # string match + if %w[title url doi description license difficulty_level subtitle city country timezone].include?(filter_by) + val.to_s.casecmp?(filter_value) + # prefix string match + elsif %w[url_prefix].include?(filter_by) + val.to_s.downcase.start_with?(filter_value.downcase) + # contains string match + elsif %w[description_contains prerequisites_contains learning_objectives_contains subtitle_contains].include?(filter_by) + val.to_s.downcase.include?(filter_value.downcase) + # array string match + elsif %w[target_audience keyword resource_type event_type].include?(filter_by) + val.any? { |i| i.to_s.casecmp?(filter_value) } + else + false + end + end + + def filter_property + { + 'event_type' => 'event_types', + 'keyword' => 'keywords', + 'url_prefix' => 'url', + 'description_contains' => 'description', + 'prerequisites_contains' => 'prerequisites', + 'learning_objectives_contains' => 'learning_objectives', + 'subtitle_contains' => 'subtitle', + 'license' => 'licence' + }.fetch(filter_by, filter_by) + end +end diff --git a/app/views/sources/_form.html.erb b/app/views/sources/_form.html.erb index 6a791a0d8..360e8111f 100644 --- a/app/views/sources/_form.html.erb +++ b/app/views/sources/_form.html.erb @@ -36,10 +36,53 @@ include_blank: false %> <% end %> +

<%= t('sources.headings.filters') %>

+ +

<%= t('sources.headings.allow_list') %>

+ <%= t('sources.hints.allow_list') %> +
+
+ <% f.object.source_filters.allow.each do |filter| %> + <%= f.simple_fields_for :source_filters, filter, child_index: filter.id do |ff| %> + <%= render partial: 'source_filter_form', locals: { f: ff } %> + <% end %> + <% end %> +
+ + + + + <%= t('sources.hints.add_filter') %> +
+ +

<%= t('sources.headings.block_list') %>

+ <%= t('sources.hints.block_list') %> +
+
+ <% f.object.source_filters.block.each do |filter| %> + <%= f.simple_fields_for :source_filters, filter, child_index: filter.id do |ff| %> + <%= render partial: 'source_filter_form', locals: { f: ff } %> + <% end %> + <% end %> +
+ + + + + <%= t('sources.hints.add_filter') %> +
+ +
<%= f.submit(class: 'btn btn-primary') %> <%= link_to t('.cancel', default: t("helpers.links.cancel")), sources_path, class: 'btn btn-default' %>
+ + <% end %> diff --git a/app/views/sources/_source_filter_form.html.erb b/app/views/sources/_source_filter_form.html.erb new file mode 100644 index 000000000..5c57549c1 --- /dev/null +++ b/app/views/sources/_source_filter_form.html.erb @@ -0,0 +1,11 @@ +
+ <%= f.input :filter_by, collection: SourceFilter.filter_bies.keys.map { |t| [t.humanize, t] }, include_blank: false, required: false %> + + <%= f.input :filter_value %> + + <%= f.input :filter_mode, collection: SourceFilter.filter_modes.keys.map { |m| [m.humanize, m] }, include_blank: false, as: :hidden %> + + <%= f.input :_destroy, as: :hidden %> + + +
diff --git a/app/workers/source_test_worker.rb b/app/workers/source_test_worker.rb index 153f6e1e8..ef5638613 100644 --- a/app/workers/source_test_worker.rb +++ b/app/workers/source_test_worker.rb @@ -9,6 +9,7 @@ class SourceTestWorker def perform(source_id) source = Source.find_by_id(source_id) return unless source + results = { events: [], materials: [], @@ -20,13 +21,14 @@ def perform(source_id) ingestor = Ingestors::IngestorFactory.get_ingestor(source.method) ingestor.token = source.token ingestor.read(source.url) + ingestor.filter(source) results = { events: ingestor.events, materials: ingestor.materials, - messages: ingestor.messages, + messages: ingestor.messages } rescue StandardError => e - results[:messages] << "Ingestor encountered an unexpected error" + results[:messages] << 'Ingestor encountered an unexpected error' exception = e end diff --git a/config/locales/en.yml b/config/locales/en.yml index d5650cfb0..6fba21e17 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -792,10 +792,17 @@ en: token: Some ingestion methods require an authentication token, leave blank if you aren't sure. enabled: Sources that are not enabled will not be ingested. default_language: 'Default language of ingested events/materials' + allow_list: Only things that match one of the conditions are allowed for import. Everything is allowed for an empty allow list. + block_list: Everything that matches a filter in the block list is not allowed for import even if it is allowed by the allow list. + add_filter: Add filter condition approval_status: not_approved: Not approved requested: Approval requested approved: Approved + headings: + filters: Filters + allow_list: Allow List + block_list: Block List prompts: default_language: 'Select a default language...' scraper: diff --git a/db/migrate/20251209112056_create_source_filters.rb b/db/migrate/20251209112056_create_source_filters.rb new file mode 100644 index 000000000..9b3ac7ebe --- /dev/null +++ b/db/migrate/20251209112056_create_source_filters.rb @@ -0,0 +1,12 @@ +class CreateSourceFilters < ActiveRecord::Migration[7.2] + def change + create_table :source_filters do |t| + t.references :source, null: false, foreign_key: true + t.string :filter_mode + t.string :filter_by + t.string :filter_value + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 769407996..d44b5e364 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_12_01_143501) do +ActiveRecord::Schema[7.2].define(version: 2025_12_09_112056) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -464,6 +464,16 @@ t.string "title" end + create_table "source_filters", force: :cascade do |t| + t.bigint "source_id", null: false + t.string "filter_mode" + t.string "filter_by" + t.string "filter_value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["source_id"], name: "index_source_filters_on_source_id" + end + create_table "sources", force: :cascade do |t| t.bigint "content_provider_id" t.bigint "user_id" @@ -668,6 +678,7 @@ add_foreign_key "materials", "users" add_foreign_key "node_links", "nodes" add_foreign_key "nodes", "users" + add_foreign_key "source_filters", "sources" add_foreign_key "sources", "content_providers" add_foreign_key "sources", "spaces" add_foreign_key "sources", "users" diff --git a/lib/ingestors/ingestor.rb b/lib/ingestors/ingestor.rb index 7f9106e9a..a68afa27d 100644 --- a/lib/ingestors/ingestor.rb +++ b/lib/ingestors/ingestor.rb @@ -33,10 +33,22 @@ def read(_url) raise NotImplementedError end + def filter(source) + material_count = @materials.length + event_count = @events.length + + @materials = @materials.select { |m| source.passes_filter? m } + @events = @events.select { |e| source.passes_filter? e } + + @messages << "#{@materials.length} of #{material_count} materials passed the filters" if @materials.length != material_count + @messages << "#{@events.length} of #{event_count} events passed the filters" if @events.length != event_count + end + def write(user, provider, source: nil) - write_resources(Event, @events, user, provider, source: source) + filter(source) if source + write_resources(Event, @events, user, provider, source:) @messages << stats_summary(:events) - write_resources(Material, @materials, user, provider, source: source) + write_resources(Material, @materials, user, provider, source:) @messages << stats_summary(:materials) end @@ -174,9 +186,7 @@ def write_resources(type, resources, user, provider, source: nil) type.new(resource.to_h) end - if resource.has_attribute?(:language) && resource.new_record? - resource.language ||= source&.default_language - end + resource.language ||= source&.default_language if resource.has_attribute?(:language) && resource.new_record? resource = set_resource_defaults(resource) if resource.valid? diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index bda4816df..1d05bff29 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -506,3 +506,34 @@ plant_space_event: end: 2126-12-12 12:00:00 timezone: UTC space: plants + +passing_import_filters_event: + title: MyString + url: MyString + user: regular_user + subtitle: does_match + city: does_match + country: does_match + event_types: + - other + - does_match + timezone: does_match + +passing_contains_import_filters_event: + title: MyString + url: MyString + user: regular_user + subtitle: prefix does_match suffix + +failing_import_filters_event: + title: MyString + url: MyString + user: regular_user + subtitle: does_not_match + city: does_not_match + country: does_not_match + event_types: + - other + - does_not_match + timezone: does_not_match + diff --git a/test/fixtures/materials.yml b/test/fixtures/materials.yml index 4d9a90b09..5c75d3335 100644 --- a/test/fixtures/materials.yml +++ b/test/fixtures/materials.yml @@ -190,3 +190,51 @@ plant_space_material: content_provider: goblet status: active space: plants + +passing_import_filters_material: + user: regular_user + title: does_match + url: does_match + doi: does_match + description: does_match + licence: does_match + difficulty_level: does_match + prerequisites: does_match + learning_objectives: does_match + target_audience: + - other + - does_match + keywords: + - other + - does_match + resource_type: + - other + - does_match + +passing_contains_import_filters_material: + user: regular_user + title: MyString + url: does_match suffix + description: prefix does_match suffix + prerequisites: prefix does_match suffix + learning_objectives: prefix does_match suffix + +failing_import_filters_material: + user: regular_user + title: does_not_match + url: does_not_match + doi: does_not_match + description: does_not_match + licence: does_not_match + difficulty_level: does_not_match + prerequisites: does_not_match + learning_objectives: does_not_match + target_audience: + - other + - does_not_match + keywords: + - other + - does_not_match + resource_type: + - other + - does_not_match diff --git a/test/fixtures/source_filters.yml b/test/fixtures/source_filters.yml new file mode 100644 index 000000000..b550fe46c --- /dev/null +++ b/test/fixtures/source_filters.yml @@ -0,0 +1,121 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +source_filter_target_audience: + source: filtered_source + filter_mode: allow + filter_by: target_audience + filter_value: does_match + +source_filter_keyword: + source: filtered_source + filter_mode: allow + filter_by: keyword + filter_value: does_match + +source_filter_keyword_block: + source: filtered_source + filter_mode: block + filter_by: keyword + filter_value: does_match + +source_filter_title: + source: filtered_source + filter_mode: allow + filter_by: title + filter_value: does_match + +source_filter_description: + source: filtered_source + filter_mode: allow + filter_by: description + filter_value: does_match + +source_filter_description_contains: + source: filtered_source + filter_mode: allow + filter_by: description_contains + filter_value: does_match + +source_filter_url: + source: filtered_source + filter_mode: allow + filter_by: url + filter_value: does_match + +source_filter_url_prefix: + source: filtered_source + filter_mode: allow + filter_by: url_prefix + filter_value: does_match + +source_filter_doi: + source: filtered_source + filter_mode: allow + filter_by: doi + filter_value: does_match + +source_filter_license: + source: filtered_source + filter_mode: allow + filter_by: license + filter_value: does_match + +source_filter_difficulty_level: + source: filtered_source + filter_mode: allow + filter_by: difficulty_level + filter_value: does_match + +source_filter_resource_type: + source: filtered_source + filter_mode: allow + filter_by: resource_type + filter_value: does_match + +source_filter_prerequisites_contains: + source: filtered_source + filter_mode: allow + filter_by: prerequisites_contains + filter_value: does_match + +source_filter_learning_objectives_contains: + source: filtered_source + filter_mode: allow + filter_by: learning_objectives_contains + filter_value: does_match + +source_filter_learning_subtitle: + source: filtered_source + filter_mode: allow + filter_by: subtitle + filter_value: does_match + +source_filter_subtitle_contains: + source: filtered_source + filter_mode: allow + filter_by: subtitle_contains + filter_value: does_match + +source_filter_city: + source: filtered_source + filter_mode: allow + filter_by: city + filter_value: does_match + +source_filter_country: + source: filtered_source + filter_mode: allow + filter_by: country + filter_value: does_match + +source_filter_event_type: + source: filtered_source + filter_mode: allow + filter_by: event_type + filter_value: does_match + +source_filter_timezone: + source: filtered_source + filter_mode: allow + filter_by: timezone + filter_value: does_match diff --git a/test/fixtures/sources.yml b/test/fixtures/sources.yml index 3ea73984f..59edce307 100644 --- a/test/fixtures/sources.yml +++ b/test/fixtures/sources.yml @@ -102,3 +102,12 @@ disabled_source: enabled: false user: regular_user approval_status: 2 + +filtered_source: + content_provider: portal_provider + url: 'https://website.org' + method: bioschemas + enabled: false + user: regular_user + approval_status: 2 + diff --git a/test/models/source_filter_test.rb b/test/models/source_filter_test.rb new file mode 100644 index 000000000..344f7da76 --- /dev/null +++ b/test/models/source_filter_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class SourceFilterTest < ActiveSupport::TestCase + # The source_filter model is tested extensively as part of the scraper_test ingestor unit test. + + # test "the truth" do + # assert true + # end +end diff --git a/test/schema_helper.rb b/test/schema_helper.rb index 9eabcf873..d456bd0d0 100644 --- a/test/schema_helper.rb +++ b/test/schema_helper.rb @@ -15,7 +15,7 @@ def path URI.parse(@request.original_fullpath).path.chomp('.json_api') end - alias_method :path_info, :path + alias path_info path def request_method @request.env['action_dispatch.original_request_method'] || @request.request_method @@ -33,7 +33,7 @@ def assert_valid_legacy_json_response(expected_status = 200) def assert_valid_json_response(validator, options, expected_status = nil) unless validator.link_exist? response = "`#{committee_request_object.request_method} #{committee_request_object.path_info}` undefined in schema (prefix: #{options[:prefix].inspect})." - raise Committee::InvalidResponse.new(response) + raise Committee::InvalidResponse, response end status, headers, body = committee_response_data @@ -42,7 +42,7 @@ def assert_valid_json_response(validator, options, expected_status = nil) Committee.warn_deprecated('Pass expected response status code to check it against the corresponding schema explicitly.') elsif expected_status != status response = "Expected `#{expected_status}` status code, but it was `#{status}`." - raise Committee::InvalidResponse.new(response) + raise Committee::InvalidResponse, response end valid = Committee::Middleware::ResponseValidation.validate?(status, options.fetch(:validate_success_only, false)) @@ -60,8 +60,8 @@ def committee_response_data def current_committee_options @current_committee_options ||= { - schema: Committee::Drivers::load_from_file('public/api/definitions/tess.yml', - parser_options: { strict_reference_validation: true }), + schema: Committee::Drivers.load_from_file('public/api/definitions/tess.yml', + parser_options: { strict_reference_validation: true }), query_hash_key: 'rack.request.query_hash', parse_response_by_content_type: false } @@ -69,8 +69,8 @@ def current_committee_options def legacy_committee_options @legacy_committee_options ||= { - schema: Committee::Drivers::load_from_file('public/api/definitions/tess_legacy.yml', - parser_options: { strict_reference_validation: true }), + schema: Committee::Drivers.load_from_file('public/api/definitions/tess_legacy.yml', + parser_options: { strict_reference_validation: true }), query_hash_key: 'rack.request.query_hash', parse_response_by_content_type: false } diff --git a/test/unit/ingestors/ingestor_test.rb b/test/unit/ingestors/ingestor_test.rb index 7b1bd13c3..3ed4a1317 100644 --- a/test/unit/ingestors/ingestor_test.rb +++ b/test/unit/ingestors/ingestor_test.rb @@ -8,7 +8,7 @@ class IngestorTest < ActiveSupport::TestCase expected = input assert_equal expected, ingestor.convert_description(input) - input = "

Title