forked from OpenNeo/impress
152 lines
4.9 KiB
Ruby
152 lines
4.9 KiB
Ruby
|
class Item
|
||
|
module Search
|
||
|
class Query
|
||
|
FIELD_CLASSES = {
|
||
|
:is_nc => Fields::Flag,
|
||
|
:is_pb => Fields::Flag,
|
||
|
:species_support_id => Fields::SetField,
|
||
|
:occupied_zone_id => Fields::SetField,
|
||
|
:restricted_zone_id => Fields::SetField,
|
||
|
:name => Fields::TextField
|
||
|
}
|
||
|
|
||
|
def initialize(filters, user)
|
||
|
@filters = filters
|
||
|
end
|
||
|
|
||
|
def fields
|
||
|
initial_fields.tap do |fields|
|
||
|
@filters.each { |filter| fields[filter.key] << filter }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def to_flex_params
|
||
|
fields.values.map(&:to_flex_params).inject(&:merge)
|
||
|
end
|
||
|
|
||
|
def paginate(options={})
|
||
|
begin
|
||
|
flex_params = self.to_flex_params
|
||
|
rescue Item::Search::Contradiction
|
||
|
# If we have a contradictory query, no need to raise a stink about
|
||
|
# it, but no need to actually run a search, either.
|
||
|
return []
|
||
|
end
|
||
|
|
||
|
final_flex_params = {
|
||
|
:page => (options[:page] || 1),
|
||
|
:size => (options[:per_page] || 30)
|
||
|
}.merge(flex_params)
|
||
|
|
||
|
locales = I18n.fallbacks[I18n.locale] &
|
||
|
I18n.locales_with_neopets_language_code
|
||
|
final_flex_params[:locale] = locales.first
|
||
|
|
||
|
if final_flex_params[:name] || final_flex_params[:negative_name]
|
||
|
locale_entries = locales.map do |locale|
|
||
|
boost = (locale == I18n.locale) ? 4 : 1
|
||
|
{:locale => locale, :boost => boost}
|
||
|
end
|
||
|
|
||
|
if final_flex_params[:name]
|
||
|
final_flex_params[:_name_locales] = locale_entries
|
||
|
end
|
||
|
|
||
|
if final_flex_params[:negative_name]
|
||
|
final_flex_params[:_negative_name_locales] = locale_entries
|
||
|
end
|
||
|
end
|
||
|
|
||
|
result = FlexSearch.item_search(final_flex_params)
|
||
|
result.scoped_loaded_collection(:scopes => {'Item' => Item.with_translations})
|
||
|
end
|
||
|
|
||
|
# Load the text query labels from I18n, so that when we see, say,
|
||
|
# the filter "species:acara", we know it means species_support_id.
|
||
|
TEXT_KEYS_BY_LABEL = {}
|
||
|
I18n.available_locales.each do |locale|
|
||
|
TEXT_KEYS_BY_LABEL[locale] = {}
|
||
|
FIELD_CLASSES.keys.each do |key|
|
||
|
# A locale can specify multiple labels for a key by separating by
|
||
|
# commas: "occupies,zone,type"
|
||
|
labels = I18n.translate("items.search.labels.#{key}",
|
||
|
:locale => locale).split(',')
|
||
|
labels.each { |label| TEXT_KEYS_BY_LABEL[locale][label] = key }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
TEXT_QUERY_RESOURCE_FINDERS = {
|
||
|
:species => lambda { |name|
|
||
|
species = Species.find_by_name(name)
|
||
|
unless species
|
||
|
Item::Search.error 'not_found.species', :species_name => name
|
||
|
end
|
||
|
species.id
|
||
|
},
|
||
|
:zone => lambda { |name|
|
||
|
zone_set = Zone.find_set(name)
|
||
|
unless zone_set
|
||
|
Item::Search.error 'not_found.zone', :zone_name => name
|
||
|
end
|
||
|
zone_set.map(&:id)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TEXT_QUERY_RESOURCE_TYPES_BY_KEY = {
|
||
|
:species_support_id => :species,
|
||
|
:occupied_zone_id => :zone,
|
||
|
:restricted_zone_id => :zone
|
||
|
}
|
||
|
|
||
|
TEXT_FILTER_EXPR = /([+-]?)(?:([a-z]+):)?(?:"([^"]+)"|(\S+))/
|
||
|
def self.from_text(text, user=nil)
|
||
|
filters = []
|
||
|
text.scan(TEXT_FILTER_EXPR) do |sign, label, quoted_value, unquoted_value|
|
||
|
label ||= 'name'
|
||
|
raw_value = quoted_value || unquoted_value
|
||
|
is_positive = (sign != '-')
|
||
|
|
||
|
if label == 'is'
|
||
|
# is-filters are weird. "-is:nc" is transposed to something more
|
||
|
# like "-nc:<nil>", then it's translated into a negative "is_nc"
|
||
|
# flag.
|
||
|
label = raw_value
|
||
|
raw_value = nil
|
||
|
end
|
||
|
|
||
|
key = TEXT_KEYS_BY_LABEL[I18n.locale][label]
|
||
|
if key.nil?
|
||
|
message = I18n.translate('items.search.errors.not_found.label',
|
||
|
:label => label)
|
||
|
raise Item::Search::Error, message
|
||
|
end
|
||
|
|
||
|
if TEXT_QUERY_RESOURCE_TYPES_BY_KEY.has_key?(key)
|
||
|
resource_type = TEXT_QUERY_RESOURCE_TYPES_BY_KEY[key]
|
||
|
finder = TEXT_QUERY_RESOURCE_FINDERS[resource_type]
|
||
|
value = finder.call(raw_value)
|
||
|
else
|
||
|
value = raw_value
|
||
|
end
|
||
|
|
||
|
filters << Filter.new(key, value, is_positive)
|
||
|
end
|
||
|
|
||
|
self.new(filters, user)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
# The fields start out empty, then have the filters inserted into 'em,
|
||
|
# so that the fields can validate and aggregate their requirements.
|
||
|
def initial_fields
|
||
|
{}.tap do |fields|
|
||
|
FIELD_CLASSES.map do |key, klass|
|
||
|
fields[key] = klass.new(key)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|