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') %>
+
+
+ <%= t('sources.headings.block_list') %>
+ <%= t('sources.hints.block_list') %>
+
+
+
<%= f.submit(class: 'btn btn-primary') %>
<%= link_to t('.cancel', default: t("helpers.links.cancel")),
sources_path, class: 'btn btn-default' %>
+
+ <%= f.simple_fields_for :source_filters, SourceFilter.new(filter_mode: 'allow'), child_index: "REPLACE_ME" do |ff| %>
+ <%= render partial: 'source_filter_form', locals: { f: ff } %>
+ <% end %>
+
+
<% 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
- Item 1
- Item 2
"
+ input = 'Title
- Item 1
- Item 2
'
expected = "# Title\n\n- Item 1\n- Item 2"
assert_equal expected, ingestor.convert_description(input)
end
@@ -30,7 +30,7 @@ class IngestorTest < ActiveSupport::TestCase
[OpenStruct.new(url: 'https://some-course.ca',
title: 'Some course',
start: '2021-01-31 13:00:00',
- end:'2021-01-31 14:00:00')])
+ end: '2021-01-31 14:00:00')])
assert_difference('provider.events.count', 1) do
ingestor.write(user, provider, source: @source)
end
@@ -55,7 +55,7 @@ class IngestorTest < ActiveSupport::TestCase
[OpenStruct.new(url: 'https://some-course.de',
title: 'Some german course',
start: '2021-01-31 13:00:00',
- end:'2021-01-31 14:00:00',
+ end: '2021-01-31 14:00:00',
language: 'de')])
assert_difference('provider.events.count', 1) do
ingestor.write(user, provider, source: @source)
@@ -80,7 +80,7 @@ class IngestorTest < ActiveSupport::TestCase
[OpenStruct.new(url: 'https://some-course.org',
title: 'Some other course',
start: '2021-01-31 13:00:00',
- end:'2021-01-31 14:00:00',
+ end: '2021-01-31 14:00:00',
language: 'de')])
assert_difference('provider.events.count', 1) do
ingestor.write(user, provider, source: @source)
@@ -105,7 +105,7 @@ class IngestorTest < ActiveSupport::TestCase
[OpenStruct.new(url: 'https://some-course.net',
title: 'Yet another course',
start: '2021-01-31 13:00:00',
- end:'2021-01-31 14:00:00')])
+ end: '2021-01-31 14:00:00')])
assert_difference('provider.events.count', 1) do
ingestor.write(user, provider, source: @source)
end
@@ -113,6 +113,80 @@ class IngestorTest < ActiveSupport::TestCase
assert_nil(event.language)
end
+ def run_filter(source_filter)
+ source = Source.create!(url: 'https://somewhere.com/stuff', method: 'bioschemas',
+ enabled: true, approval_status: 'approved',
+ content_provider: content_providers(:portal_provider), user: users(:admin),
+ source_filters: [source_filter])
+ ingestor = Ingestors::Ingestor.new
+ ingestor.instance_variable_set(:@events,
+ [events(:passing_import_filters_event), events(:passing_contains_import_filters_event), events(:failing_import_filters_event)])
+ ingestor.instance_variable_set(:@materials,
+ [materials(:passing_import_filters_material), materials(:passing_contains_import_filters_material), materials(:failing_import_filters_material)])
+ ingestor.filter(source)
+ ingestor
+ end
+
+ test 'does respect material filter conditions' do
+ [
+ source_filters(:source_filter_target_audience),
+ source_filters(:source_filter_keyword),
+ source_filters(:source_filter_title),
+ source_filters(:source_filter_description),
+ source_filters(:source_filter_description_contains),
+ source_filters(:source_filter_url),
+ source_filters(:source_filter_url_prefix),
+ source_filters(:source_filter_doi),
+ source_filters(:source_filter_license),
+ source_filters(:source_filter_difficulty_level),
+ source_filters(:source_filter_resource_type),
+ source_filters(:source_filter_prerequisites_contains),
+ source_filters(:source_filter_learning_objectives_contains)
+ ].each do |filter|
+ filtered_ingestor = run_filter(filter)
+ assert_includes(filtered_ingestor.instance_variable_get(:@materials), materials(:passing_import_filters_material), "Filter_by: #{filter.filter_by}")
+ refute_includes(filtered_ingestor.instance_variable_get(:@materials), materials(:failing_import_filters_material), "Filter_by: #{filter.filter_by}")
+ end
+ end
+
+ test 'does respect event only filter conditions' do
+ [
+ source_filters(:source_filter_subtitle_contains),
+ source_filters(:source_filter_city),
+ source_filters(:source_filter_country),
+ source_filters(:source_filter_event_type),
+ source_filters(:source_filter_timezone)
+ ].each do |filter|
+ filtered_ingestor = run_filter(filter)
+ assert_includes(filtered_ingestor.instance_variable_get(:@events), events(:passing_import_filters_event), "Filter_by: #{filter.filter_by}")
+ refute_includes(filtered_ingestor.instance_variable_get(:@events), events(:failing_import_filters_event), "Filter_by: #{filter.filter_by}")
+ end
+ end
+
+ test 'does respect contains filter conditions' do
+ [
+ source_filters(:source_filter_url_prefix),
+ source_filters(:source_filter_description_contains),
+ source_filters(:source_filter_prerequisites_contains),
+ source_filters(:source_filter_learning_objectives_contains)
+ ].each do |filter|
+ filtered_ingestor = run_filter(filter)
+ assert_includes(filtered_ingestor.instance_variable_get(:@materials), materials(:passing_contains_import_filters_material), "Filter_by: #{filter.filter_by}")
+ end
+
+ [
+ source_filters(:source_filter_subtitle_contains)
+ ].each do |filter|
+ filtered_ingestor = run_filter(filter)
+ assert_includes(filtered_ingestor.instance_variable_get(:@events), events(:passing_contains_import_filters_event), "Filter_by: #{filter.filter_by}")
+ end
+ end
+
+ test 'does respect block list filter' do
+ filtered_ingestor = run_filter(source_filters(:source_filter_keyword_block))
+ refute_includes(filtered_ingestor.instance_variable_get(:@materials), materials(:passing_import_filters_material))
+ assert_includes(filtered_ingestor.instance_variable_get(:@materials), materials(:failing_import_filters_material))
+ end
test 'open_url returns content when URL is valid' do
ingestor = DummyIngestor.new
stub_request(:get, 'https://example.com').to_return(body: 'ok', status: 200)
diff --git a/test/unit/ingestors/scraper_test.rb b/test/unit/ingestors/scraper_test.rb
index 401f6c143..ad37c3fb1 100644
--- a/test/unit/ingestors/scraper_test.rb
+++ b/test/unit/ingestors/scraper_test.rb
@@ -5,7 +5,7 @@
class ScraperTest < ActiveSupport::TestCase
setup do
mock_ingestions
- Source.delete_all
+ Source.destroy_all # use destroy_all instead of delete_all to delete related objects correctly
end
def run
@@ -337,14 +337,14 @@ def set_up_event_check
freeze_time(2019) do
set_up_event_check
with_settings({ scraper_event_check: { enabled: true, stale_threshold: 0.3 } }) do
- assert_not @scraper.scraper_event_check({config: [@source]})
+ assert_not @scraper.scraper_event_check({ config: [@source] })
@source.content_provider.events.each do |event|
event.last_scraped = 10.days.freeze.ago
event.timezone = 'Amsterdam'
event.save!
event.reload
end
- assert @scraper.scraper_event_check({config: [@source]})
+ assert @scraper.scraper_event_check({ config: [@source] })
end
end
end
@@ -352,12 +352,12 @@ def set_up_event_check
test 'event_check_rejected' do
set_up_event_check
with_settings({ scraper_event_check: { enabled: true, rejected_threshold: 0.3 } }) do
- assert_not @scraper.scraper_event_check({config: [@source]})
+ assert_not @scraper.scraper_event_check({ config: [@source] })
@source.records_written = 10
@source.resources_rejected = 90
@source.save!
@source.reload
- assert @scraper.scraper_event_check({config: [@source]})
+ assert @scraper.scraper_event_check({ config: [@source] })
end
end