Add fits and not_fits back to item search

Some fun stuff here to figure out how to API this out well, but I'm pretty pleased with where it ended up!
This commit is contained in:
Matchu 2023-07-28 14:45:10 -07:00 committed by Matchu
parent 62f1d883af
commit e1b17e05be
5 changed files with 96 additions and 10 deletions

View file

@ -6,6 +6,11 @@ class Color < ActiveRecord::Base
scope :standard, -> { where(:standard => true) }
scope :nonstandard, -> { where(:standard => false) }
scope :funny, -> { order(:prank) unless pranks_funny? }
scope :matching_name, ->(name, locale = I18n.locale) {
ct = Color::Translation.arel_table
joins(:translations).where(ct[:locale].eq(locale)).
where(ct[:name].matches(sanitize_sql_like(name)))
}
validates :name, presence: true
@ -33,4 +38,10 @@ class Color < ActiveRecord::Base
now = Time.now.in_time_zone('Pacific Time (US & Canada)')
now.month == 4 && now.day == 1
end
# TODO: Copied from modern Rails source, can delete once we're there!
def self.sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
end
end

View file

@ -48,12 +48,12 @@ class Item < ActiveRecord::Base
scope :name_includes, ->(value, locale = I18n.locale) {
it = Item::Translation.arel_table
Item.joins(:translations).where(it[:locale].eq(locale)).
where(it[:name].matches('%' + Item.sanitize_sql_like(value) + '%'))
where(it[:name].matches('%' + sanitize_sql_like(value) + '%'))
}
scope :name_excludes, ->(value, locale = I18n.locale) {
it = Item::Translation.arel_table
Item.joins(:translations).where(it[:locale].eq(locale)).
where(it[:name].matches('%' + Item.sanitize_sql_like(value) + '%').not)
where(it[:name].matches('%' + sanitize_sql_like(value) + '%').not)
}
scope :is_nc, -> {
i = Item.arel_table
@ -67,23 +67,20 @@ class Item < ActiveRecord::Base
it = Item::Translation.arel_table
joins(:translations).where(it[:locale].eq('en')).
where('description LIKE ?',
'%' + Item.sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
}
scope :is_not_pb, -> {
it = Item::Translation.arel_table
joins(:translations).where(it[:locale].eq('en')).
where('description NOT LIKE ?',
'%' + Item.sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
}
scope :occupies, ->(zone_label, locale = I18n.locale) {
zone_ids = Zone.matching_label(zone_label, locale).map(&:id)
i = Item.arel_table
sa = SwfAsset.arel_table
joins(:swf_assets).where(sa[:zone_id].in(zone_ids)).distinct
}
scope :not_occupies, ->(zone_label, locale = I18n.locale) {
# TODO: This is pretty slow! But I imagine it's uncommon so that's probably
# fine in practice? Querying for "has NO records matching X" is hard!
zone_ids = Zone.matching_label(zone_label, locale).map(&:id)
i = Item.arel_table
sa = SwfAsset.arel_table
@ -107,6 +104,30 @@ class Item < ActiveRecord::Base
condition = zone_ids.map { '(SUBSTR(zones_restrict, ?, 1) = "1")' }.join(' OR ')
where("NOT (#{condition})", *zone_ids)
}
scope :fits, ->(body_id) {
sa = SwfAsset.arel_table
joins(:swf_assets).where(sa[:body_id].eq(body_id)).distinct
}
scope :not_fits, ->(body_id) {
i = Item.arel_table
sa = SwfAsset.arel_table
# Querying for "has NO swf_assets matching these body IDs" is trickier than
# the positive case! To do it, we GROUP_CONCAT the body_ids together for
# each item, then use FIND_IN_SET to search the result for the body ID,
# and assert that it must not find a match. (This is uhh, not exactly fast,
# so it helps to have other tighter conditions applied first!)
#
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
#
# NOTE: The `fits` and `not_fits` counts don't perfectly add up to the
# total number of items, 5 items aren't accounted for? I'm not going to
# bother looking into this, but one thing I notice is items with no assets
# somehow would not match either scope in this impl (but LEFT JOIN would!)
joins(:swf_assets).group(i[:id]).
having('FIND_IN_SET(?, GROUP_CONCAT(body_id)) = 0', body_id).
distinct
}
def closeted?
@owned || @wanted

View file

@ -46,6 +46,18 @@ class Item
filters << (is_positive ?
Filter.restricts(value, locale) :
Filter.not_restricts(value, locale))
when 'fits'
color_name, species_name = value.split('-')
begin
pet_type = PetType.matching_name(color_name, species_name, locale).first!
rescue ActiveRecord::RecordNotFound
message = I18n.translate('items.search.errors.not_found.pet_type',
name1: color_name.capitalize, name2: species_name.capitalize)
raise Item::Search::Error, message
end
filters << (is_positive ?
Filter.fits(pet_type.body_id, color_name, species_name) :
Filter.not_fits(pet_type.body_id, color_name, species_name))
when 'is'
case value
when 'nc'
@ -82,9 +94,9 @@ class Item
# A Filter is basically a wrapper for an Item scope, with extra info about
# how to convert it into a search query string.
class Filter
def initialize(query, text)
def initialize(query, text_fn)
@query = query
@text = text
@text_fn = text_fn
end
def to_query
@ -92,7 +104,7 @@ class Item
end
def to_s
@text
@text_fn.call
end
def inspect
@ -125,6 +137,20 @@ class Item
self.new Item.not_restricts(value, locale), "-restricts:#{value}"
end
def self.fits(body_id, color_name, species_name)
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
value = "#{color_name.downcase}-#{species_name.downcase}"
value = '"' + value + '"' if value.include? ' '
self.new Item.fits(body_id), "fits:#{value}"
end
def self.not_fits(body_id, color_name, species_name)
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
value = "#{color_name.downcase}-#{species_name.downcase}"
value = '"' + value + '"' if value.include? ' '
self.new Item.not_fits(body_id), "-fits:#{value}"
end
def self.is_nc
self.new Item.is_nc, 'is:nc'
end
@ -148,6 +174,16 @@ class Item
def self.is_not_pb
self.new Item.is_not_pb, '-is:pb'
end
private
def self.build_fits_filter_text(color_name, species_name)
# NOTE: Colors like "Polka Dot" must be written as
# `fits:"polka dot-aisha"`.
value = "#{color_name.downcase}-#{species_name.downcase}"
value = '"' + value + '"' if value.include? ' '
"fits:#{value}"
end
end
end
end

View file

@ -22,6 +22,12 @@ class PetType < ActiveRecord::Base
scope :includes_child_translations,
-> { includes({:color => :translations, :species => :translations}) }
scope :matching_name, ->(color_name, species_name, locale = I18n.locale) {
color = Color.matching_name(color_name, locale).first!
species = Species.matching_name(species_name, locale).first!
where(color_id: color.id, species_id: species.id)
}
def self.special_color_or_basic(special_color)
color_ids = special_color ? [special_color.id] : Color.basic.select([:id]).map(&:id)
where(color_id: color_ids)

View file

@ -3,6 +3,12 @@ class Species < ActiveRecord::Base
scope :alphabetical, -> { with_translations(I18n.locale).order(Species::Translation.arel_table[:name]) }
scope :matching_name, ->(name, locale = I18n.locale) {
st = Species::Translation.arel_table
joins(:translations).where(st[:locale].eq(locale)).
where(st[:name].matches(sanitize_sql_like(name)))
}
def as_json(options={})
{:id => id, :name => human_name}
end
@ -14,4 +20,10 @@ class Species < ActiveRecord::Base
I18n.translate('species.default_human_name')
end
end
# TODO: Copied from modern Rails source, can delete once we're there!
def self.sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
end
end