Emi Matchu
52e81557c2
`is:np` now means "is not NC and is not PB". Note that it might be good to make NC and PB explicitly mutually exclusive too? It would complicate queries though, and not matter in most cases… the Burlap Usul Bow is the only item that we currently return for `is:pb is:nc`, which is probably because of a rarity issue?
289 lines
8.7 KiB
Ruby
289 lines
8.7 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).order(:name)
|
|
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) :
|
|
Filter.not_occupies(value))
|
|
when 'restricts'
|
|
filters << (is_positive ?
|
|
Filter.restricts(value) :
|
|
Filter.not_restricts(value))
|
|
when 'fits'
|
|
pet_type = load_pet_type_by_name(value)
|
|
filters << (is_positive ?
|
|
Filter.fits(pet_type.body_id, value.downcase) :
|
|
Filter.not_fits(pet_type.body_id, value.downcase))
|
|
when 'species'
|
|
begin
|
|
species = Species.find_by_name!(value)
|
|
color = Color.find_by_name!('blue')
|
|
pet_type = PetType.where(color_id: color.id, species_id: species.id).first!
|
|
rescue ActiveRecord::RecordNotFound
|
|
message = I18n.translate('items.search.errors.not_found.species',
|
|
species_name: value.capitalize)
|
|
raise Item::Search::Error, message
|
|
end
|
|
filters << (is_positive ?
|
|
Filter.fits_species(pet_type.body_id, value) :
|
|
Filter.not_fits_species(pet_type.body_id, value))
|
|
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.owned_by(user) :
|
|
Filter.not_owned_by(user))
|
|
when 'wants'
|
|
filters << (is_positive ?
|
|
Filter.wanted_by(user) :
|
|
Filter.not_wanted_by(user))
|
|
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)
|
|
filters = []
|
|
|
|
params.values.each do |filter_params|
|
|
key = filter_params[:key]
|
|
value = filter_params[:value]
|
|
is_positive = filter_params[:is_positive] != 'false'
|
|
|
|
case filter_params[:key]
|
|
when 'name'
|
|
filters << (is_positive ?
|
|
Filter.name_includes(value, locale) :
|
|
Filter.name_excludes(value, locale))
|
|
when 'is_nc'
|
|
filters << (is_positive ? Filter.is_nc : Filter.is_not_nc)
|
|
when 'is_pb'
|
|
filters << (is_positive ? Filter.is_pb : Filter.is_not_pb)
|
|
when 'is_np'
|
|
filters << (is_positive ? Filter.is_np : Filter.is_not_np)
|
|
when 'occupied_zone_set_name'
|
|
filters << (is_positive ?
|
|
Filter.occupies(value, locale) :
|
|
Filter.not_occupies(value, locale))
|
|
when 'restricted_zone_set_name'
|
|
filters << (is_positive ?
|
|
Filter.restricts(value, locale) :
|
|
Filter.not_restricts(value, locale))
|
|
when 'fits_pet_type'
|
|
pet_type = PetType.find(value)
|
|
color_name = pet_type.color.name
|
|
species_name = pet_type.species.name
|
|
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
|
|
value = "#{color_name}-#{species_name}"
|
|
filters << (is_positive ?
|
|
Filter.fits(pet_type.body_id, value) :
|
|
Filter.not_fits(pet_type.body_id, value))
|
|
when 'user_closet_hanger_ownership'
|
|
case value
|
|
when 'true'
|
|
filters << (is_positive ?
|
|
Filter.owned_by(user) :
|
|
Filter.not_owned_by(user))
|
|
when 'false'
|
|
filters << (is_positive ?
|
|
Filter.wanted_by(user) :
|
|
Filter.not_wanted_by(user))
|
|
end
|
|
else
|
|
Rails.logger.warn "Ignoring unexpected search filter key: #{key}"
|
|
end
|
|
end
|
|
|
|
self.new(filters, user)
|
|
end
|
|
|
|
def self.load_pet_type_by_name(pet_type_string)
|
|
color_name, species_name = pet_type_string.split("-")
|
|
|
|
begin
|
|
PetType.matching_name(color_name, species_name).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
|
|
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)
|
|
@query = query
|
|
@text = text
|
|
end
|
|
|
|
def to_query
|
|
@query
|
|
end
|
|
|
|
def to_s
|
|
@text
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class.name} #{@text.inspect}>"
|
|
end
|
|
|
|
def self.name_includes(value, locale)
|
|
self.new Item.name_includes(value, locale), "#{q value}"
|
|
end
|
|
|
|
def self.name_excludes(value, locale)
|
|
self.new Item.name_excludes(value, locale), "-#{q value}"
|
|
end
|
|
|
|
def self.occupies(value)
|
|
self.new Item.occupies(value), "occupies:#{q value}"
|
|
end
|
|
|
|
def self.not_occupies(value)
|
|
self.new Item.not_occupies(value), "-occupies:#{q value}"
|
|
end
|
|
|
|
def self.restricts(value)
|
|
self.new Item.restricts(value), "restricts:#{q value}"
|
|
end
|
|
|
|
def self.not_restricts(value)
|
|
self.new Item.not_restricts(value), "-restricts:#{q value}"
|
|
end
|
|
|
|
def self.fits(body_id, value)
|
|
self.new Item.fits(body_id), "fits:#{q value}"
|
|
end
|
|
|
|
def self.not_fits(body_id, value)
|
|
self.new Item.not_fits(body_id), "-fits:#{q value}"
|
|
end
|
|
|
|
def self.fits_species(body_id, species_name)
|
|
self.new Item.fits(body_id), "species:#{q species_name}"
|
|
end
|
|
|
|
def self.not_fits_species(body_id, species_name)
|
|
self.new Item.not_fits(body_id), "-species:#{q species_name}"
|
|
end
|
|
|
|
def self.owned_by(user)
|
|
self.new user.owned_items, 'user:owns'
|
|
end
|
|
|
|
def self.not_owned_by(user)
|
|
self.new user.unowned_items, 'user:owns'
|
|
end
|
|
|
|
def self.wanted_by(user)
|
|
self.new user.wanted_items, 'user:wants'
|
|
end
|
|
|
|
def self.not_wanted_by(user)
|
|
self.new user.unwanted_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_not_nc, '-is:nc'
|
|
end
|
|
|
|
def self.is_np
|
|
self.new Item.is_np, 'is:np'
|
|
end
|
|
|
|
def self.is_not_np
|
|
self.new Item.is_not_np, '-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
|
|
|
|
# Add quotes around the value, if needed.
|
|
def self.q(value)
|
|
/\s/.match(value) ? '"' + value + '"' : value
|
|
end
|
|
end
|
|
end
|
|
end
|