forked from OpenNeo/impress
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:
parent
4ac1304c74
commit
070599e2ca
5 changed files with 96 additions and 10 deletions
|
@ -6,6 +6,11 @@ class Color < ActiveRecord::Base
|
||||||
scope :standard, -> { where(:standard => true) }
|
scope :standard, -> { where(:standard => true) }
|
||||||
scope :nonstandard, -> { where(:standard => false) }
|
scope :nonstandard, -> { where(:standard => false) }
|
||||||
scope :funny, -> { order(:prank) unless pranks_funny? }
|
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
|
validates :name, presence: true
|
||||||
|
|
||||||
|
@ -33,4 +38,10 @@ class Color < ActiveRecord::Base
|
||||||
now = Time.now.in_time_zone('Pacific Time (US & Canada)')
|
now = Time.now.in_time_zone('Pacific Time (US & Canada)')
|
||||||
now.month == 4 && now.day == 1
|
now.month == 4 && now.day == 1
|
||||||
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
|
end
|
||||||
|
|
|
@ -48,12 +48,12 @@ class Item < ActiveRecord::Base
|
||||||
scope :name_includes, ->(value, locale = I18n.locale) {
|
scope :name_includes, ->(value, locale = I18n.locale) {
|
||||||
it = Item::Translation.arel_table
|
it = Item::Translation.arel_table
|
||||||
Item.joins(:translations).where(it[:locale].eq(locale)).
|
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) {
|
scope :name_excludes, ->(value, locale = I18n.locale) {
|
||||||
it = Item::Translation.arel_table
|
it = Item::Translation.arel_table
|
||||||
Item.joins(:translations).where(it[:locale].eq(locale)).
|
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, -> {
|
scope :is_nc, -> {
|
||||||
i = Item.arel_table
|
i = Item.arel_table
|
||||||
|
@ -67,23 +67,20 @@ class Item < ActiveRecord::Base
|
||||||
it = Item::Translation.arel_table
|
it = Item::Translation.arel_table
|
||||||
joins(:translations).where(it[:locale].eq('en')).
|
joins(:translations).where(it[:locale].eq('en')).
|
||||||
where('description LIKE ?',
|
where('description LIKE ?',
|
||||||
'%' + Item.sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
||||||
}
|
}
|
||||||
scope :is_not_pb, -> {
|
scope :is_not_pb, -> {
|
||||||
it = Item::Translation.arel_table
|
it = Item::Translation.arel_table
|
||||||
joins(:translations).where(it[:locale].eq('en')).
|
joins(:translations).where(it[:locale].eq('en')).
|
||||||
where('description NOT LIKE ?',
|
where('description NOT LIKE ?',
|
||||||
'%' + Item.sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
||||||
}
|
}
|
||||||
scope :occupies, ->(zone_label, locale = I18n.locale) {
|
scope :occupies, ->(zone_label, locale = I18n.locale) {
|
||||||
zone_ids = Zone.matching_label(zone_label, locale).map(&:id)
|
zone_ids = Zone.matching_label(zone_label, locale).map(&:id)
|
||||||
i = Item.arel_table
|
|
||||||
sa = SwfAsset.arel_table
|
sa = SwfAsset.arel_table
|
||||||
joins(:swf_assets).where(sa[:zone_id].in(zone_ids)).distinct
|
joins(:swf_assets).where(sa[:zone_id].in(zone_ids)).distinct
|
||||||
}
|
}
|
||||||
scope :not_occupies, ->(zone_label, locale = I18n.locale) {
|
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)
|
zone_ids = Zone.matching_label(zone_label, locale).map(&:id)
|
||||||
i = Item.arel_table
|
i = Item.arel_table
|
||||||
sa = SwfAsset.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 ')
|
condition = zone_ids.map { '(SUBSTR(zones_restrict, ?, 1) = "1")' }.join(' OR ')
|
||||||
where("NOT (#{condition})", *zone_ids)
|
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?
|
def closeted?
|
||||||
@owned || @wanted
|
@owned || @wanted
|
||||||
|
|
|
@ -46,6 +46,18 @@ class Item
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.restricts(value, locale) :
|
Filter.restricts(value, locale) :
|
||||||
Filter.not_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'
|
when 'is'
|
||||||
case value
|
case value
|
||||||
when 'nc'
|
when 'nc'
|
||||||
|
@ -82,9 +94,9 @@ class Item
|
||||||
# A Filter is basically a wrapper for an Item scope, with extra info about
|
# A Filter is basically a wrapper for an Item scope, with extra info about
|
||||||
# how to convert it into a search query string.
|
# how to convert it into a search query string.
|
||||||
class Filter
|
class Filter
|
||||||
def initialize(query, text)
|
def initialize(query, text_fn)
|
||||||
@query = query
|
@query = query
|
||||||
@text = text
|
@text_fn = text_fn
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_query
|
def to_query
|
||||||
|
@ -92,7 +104,7 @@ class Item
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
@text
|
@text_fn.call
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
|
@ -125,6 +137,20 @@ class Item
|
||||||
self.new Item.not_restricts(value, locale), "-restricts:#{value}"
|
self.new Item.not_restricts(value, locale), "-restricts:#{value}"
|
||||||
end
|
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
|
def self.is_nc
|
||||||
self.new Item.is_nc, 'is:nc'
|
self.new Item.is_nc, 'is:nc'
|
||||||
end
|
end
|
||||||
|
@ -148,6 +174,16 @@ class Item
|
||||||
def self.is_not_pb
|
def self.is_not_pb
|
||||||
self.new Item.is_not_pb, '-is:pb'
|
self.new Item.is_not_pb, '-is:pb'
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,12 @@ class PetType < ActiveRecord::Base
|
||||||
|
|
||||||
scope :includes_child_translations,
|
scope :includes_child_translations,
|
||||||
-> { includes({:color => :translations, :species => :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)
|
def self.special_color_or_basic(special_color)
|
||||||
color_ids = special_color ? [special_color.id] : Color.basic.select([:id]).map(&:id)
|
color_ids = special_color ? [special_color.id] : Color.basic.select([:id]).map(&:id)
|
||||||
|
|
|
@ -2,6 +2,12 @@ class Species < ActiveRecord::Base
|
||||||
translates :name
|
translates :name
|
||||||
|
|
||||||
scope :alphabetical, -> { with_translations(I18n.locale).order(Species::Translation.arel_table[:name]) }
|
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={})
|
def as_json(options={})
|
||||||
{:id => id, :name => human_name}
|
{:id => id, :name => human_name}
|
||||||
|
@ -14,4 +20,10 @@ class Species < ActiveRecord::Base
|
||||||
I18n.translate('species.default_human_name')
|
I18n.translate('species.default_human_name')
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue