impress/app/models/item/search/query.rb
Matchu 6581597d7c Add user:owns/wants back to item search
Not so bad, using a condition on `has_many` `through` was a cute trick!!
2023-10-23 19:05:03 -07:00

213 lines
6.3 KiB
Ruby

# encoding=utf-8
# ^ to put the regex in utf-8 mode
class Item
module Search
class Query
def initialize(filters, user, text=nil)
@filters = filters
@user = user
@text = text
end
def results
@filters.map(&:to_query).inject(Item.all, &:merge).
alphabetize_by_translations(Query.locale)
end
def to_s
@text || @filters.map(&:to_s).join(' ')
end
def self.locale
(I18n.fallbacks[I18n.locale] &
I18n.locales_with_neopets_language_code).first
end
TEXT_FILTER_EXPR = /([+-]?)(?:(\p{Word}+):)?(?:"([^"]+)"|(\S+))/
def self.from_text(text, user=nil)
filters = []
text.scan(TEXT_FILTER_EXPR) do |sign, key, quoted_value, unquoted_value|
key = 'name' if key.blank?
value = quoted_value || unquoted_value
is_positive = (sign != '-')
case key
when 'name'
filters << (is_positive ?
Filter.name_includes(value, locale) :
Filter.name_excludes(value, locale))
when 'occupies'
filters << (is_positive ?
Filter.occupies(value, locale) :
Filter.not_occupies(value, locale))
when 'restricts'
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 'user'
if user.nil?
message = I18n.translate('items.search.errors.not_logged_in')
raise Item::Search::Error, message
end
case value
when 'owns'
filters << (is_positive ?
Filter.user_owns(user) :
Filter.user_wants(user))
when 'wants'
else
message = I18n.translate('items.search.errors.not_found.ownership',
keyword: value)
raise Item::Search::Error, message
end
when 'is'
case value
when 'nc'
filters << (is_positive ? Filter.is_nc : Filter.is_not_nc)
when 'np'
filters << (is_positive ? Filter.is_np : Filter.is_not_np)
when 'pb'
filters << (is_positive ? Filter.is_pb : Filter.is_not_pb)
else
message = I18n.translate('items.search.errors.not_found.label',
:label => "is:#{value}")
raise Item::Search::Error, message
end
else
message = I18n.translate('items.search.errors.not_found.label',
:label => key)
raise Item::Search::Error, message
end
end
self.new(filters, user, text)
end
def self.from_params(params, user=nil)
raise NotImplementedError, "TODO: Reimplemented Advanced Search"
end
end
class Error < Exception
end
private
# 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_fn)
@query = query
@text_fn = text_fn
end
def to_query
@query
end
def to_s
@text_fn.call
end
def inspect
"#<#{self.class.name} #{@text.inspect}>"
end
def self.name_includes(value, locale)
text = /\s/.match(value) ? '"' + value + '"' : value
self.new Item.name_includes(value, locale), text
end
def self.name_excludes(value, locale)
text = '-' + (/\s/.match(value) ? '"' + value + '"' : value)
self.new Item.name_excludes(value, locale), text
end
def self.occupies(value, locale)
self.new Item.occupies(value, locale), "occupies:#{value}"
end
def self.not_occupies(value, locale)
self.new Item.not_occupies(value, locale), "-occupies:#{value}"
end
def self.restricts(value, locale)
self.new Item.restricts(value, locale), "restricts:#{value}"
end
def self.not_restricts(value, locale)
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.user_owns(user)
self.new user.owned_items, 'user:owns'
end
def self.user_wants(user)
self.new user.wanted_items, 'user:wants'
end
def self.is_nc
self.new Item.is_nc, 'is:nc'
end
def self.is_not_nc
self.new Item.is_np, '-is:nc'
end
def self.is_np
self.new Item.is_np, 'is:np'
end
def self.is_not_np
self.new Item.is_nc, '-is:np'
end
def self.is_pb
self.new Item.is_pb, 'is:pb'
end
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