refactor filterable controller code into a filter service object

This commit is contained in:
James Smith committed 2025-07-29 11:53:01 +01:00
1 parent 6f4ffbcfad
commit ca48853d46
2 files changed
+237

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
class Search::FilterService
attr_reader :collection
attr_reader :creator
# Get list filters from URL
def initialize(params)
@filters = params.permit(
:library,
:collection,
:q,
:creator,
:link,
:missingtag,
tag: []
).compact || {}
@collection = Collection.find_param(parameter(:collection)) if parameter(:collection).present?
@creator = Creator.find_param(parameter(:creator)) if parameter(:creator).present?
end
def any?
!@filters.empty?
end
def filtering_by?(key)
parameter(key).present?
end
def parameter(key)
@filters[key]
end
def to_params(except: nil)
ActionController::Parameters.new(@filters.except(except))
end
def tags
end
def models(scope)
scope = scope.all
scope = filter_by_library(scope)
scope = filter_by_missing_tag(scope)
scope = filter_by_tag(scope)
scope = filter_by_collection(scope)
scope = filter_by_creator(scope)
scope = filter_by_url(scope)
filter_by_search(scope)
end
private
# Filter by library
def filter_by_library(scope)
filtering_by?(:library) ? scope.where(library: Library.find_param(parameter(:library))) : scope
end
# Filter by collection
def filter_by_collection(scope)
case parameter(:collection)
when nil
scope # No collection, move along
when ""
scope.where(collection_id: nil)
else
scope.where(collection: Collection.tree_down(@collection.id))
end
end
# Filter by creator
def filter_by_creator(scope)
case parameter(:creator)
when nil
scope # No creator specified, nothing to do
when ""
scope.where(creator_id: nil)
else
scope.where(creator: creator)
end
end
# Filter by tag
def filter_by_tag(scope)
case parameter(:tag)
when nil
scope # No tags, move along
when [""]
scope.where("(select count(*) from taggings where taggings.taggable_id=models.id and taggings.context='tags')<1")
else
# Build query directly rather than using tagged_with, which parses the tag list again using default separators
::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(scope, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, parameter(:tag), {})
end
end
# Filter by url
def filter_by_url(scope)
case parameter(:link)
when nil
scope # no filter
when ""
scope.where("(select count(*) from links where linkable_id=models.id and linkable_type='Model')<1")
else
scope.where("(select count(*) from links where linkable_id=models.id and linkable_type='Model' and url like ?)>0", "%#{parameter(:link)}%")
end
end
# Filter by search query
def filter_by_search(scope)
if parameter(:q)
Search::ModelSearchService.new(scope).search(parameter(:q))
else
scope
end
end
def filter_by_missing_tag(scope)
# Missing tags (If specific tag is not specified, require library to be set)
if filtering_by?(:missingtag) || (filtering_by?(:missingtag) && parameter(:library))
tag_regex_build = []
regexes = ((parameter(:missingtag) != "") ? [parameter(:missingtag)] : Library.find_param(parameter(:library)).tag_regex)
# Regexp match syntax - postgres is different from MySQL and SQLite
regact = (ApplicationRecord.connection.adapter_name == "PostgreSQL") ? "~" : "REGEXP"
regexes.each do |reg|
qreg = ActiveRecord::Base.connection.quote(reg)
tag_regex_build.push "(select count(*) from tags join taggings on tags.id=taggings.tag_id where tags.name #{regact} #{qreg} and taggings.taggable_id=models.id and taggings.taggable_type='Model')<1"
end
qreg = ActiveRecord::Base.connection.quote(parameter(:missingtag))
tag_regex_build.push "(select count(*) from tags join taggings on tags.id=taggings.tag_id where tags.name #{regact} #{qreg} and taggings.taggable_id=models.id and taggings.taggable_type='Model')<1"
scope.where("(" + tag_regex_build.join(" OR ") + ")")
else
scope
end
end
end
+104
View File
@@ -0,0 +1,104 @@
require "rails_helper"
RSpec.describe Search::FilterService do
context "with a range of models" do
let(:creator) { create(:creator, name: "Dr Seuss") }
let(:collection) { create(:collection, name: "Chiroptera") }
before do
create(:model, name: "cat in the hat", tag_list: ["dog", "log", "frog", "cat"], creator: creator, notes: "lorem ipsum", caption: nil)
create(:model, name: "hat on the cat", tag_list: [], creator: creator, notes: nil, caption: "dolor sit amet")
create(:model, name: "bat on a mat", tag_list: ["log"], collection: collection, notes: nil, caption: nil, links_attributes: [{url: "https://thingiverse.com/thing:1234"}])
create(:model, name: "bat on a hat", tag_list: ["frog"], collection: collection, notes: nil, caption: nil)
end
it "returns all models with no filters" do
filter = described_class.new(ActionController::Parameters.new)
expect(filter.models(Model).count).to eq 4
end
it "filters on a simple search query" do
service = described_class.new(ActionController::Parameters.new(q: "cat"))
expect(service.models(Model.all).pluck(:name)).to eq [
"cat in the hat",
"hat on the cat"
]
end
it "filters by collection" do
service = described_class.new(ActionController::Parameters.new(collection: collection.to_param))
expect(service.models(Model.all).pluck(:name)).to eq [
"bat on a mat",
"bat on a hat"
]
end
it "filters by missing collection" do
service = described_class.new(ActionController::Parameters.new(collection: ""))
expect(service.models(Model.all).pluck(:name)).to eq [
"cat in the hat",
"hat on the cat"
]
end
it "filters by creator" do
service = described_class.new(ActionController::Parameters.new(creator: creator.to_param))
expect(service.models(Model.all).pluck(:name)).to eq [
"cat in the hat",
"hat on the cat"
]
end
it "filters by missing creator" do
service = described_class.new(ActionController::Parameters.new(creator: ""))
expect(service.models(Model.all).pluck(:name)).to eq [
"bat on a mat",
"bat on a hat"
]
end
it "filters by single tag" do
service = described_class.new(ActionController::Parameters.new(tag: ["log"]))
expect(service.models(Model.all).pluck(:name)).to eq [
"cat in the hat",
"bat on a mat"
]
end
it "filters by no tags" do
service = described_class.new(ActionController::Parameters.new(tag: [""]))
expect(service.models(Model.all).pluck(:name)).to eq [
"hat on the cat"
]
end
it "filters by multiple tags" do
service = described_class.new(ActionController::Parameters.new(tag: ["log", "frog"]))
expect(service.models(Model.all).pluck(:name)).to eq [
"cat in the hat"
]
end
it "filters by missing tag" do
service = described_class.new(ActionController::Parameters.new(missingtag: "log"))
expect(service.models(Model.all).pluck(:name)).to eq [
"hat on the cat",
"bat on a hat"
]
end
it "filters by library" do
service = described_class.new(ActionController::Parameters.new(library: Library.last.to_param))
expect(service.models(Model.all).pluck(:name)).to eq [
"bat on a hat"
]
end
it "filters by link" do
service = described_class.new(ActionController::Parameters.new(link: "thingiverse.com"))
expect(service.models(Model.all).pluck(:name)).to eq [
"bat on a mat"
]
end
end
end