diff --git a/app/models/item.rb b/app/models/item.rb index 05b27187..3df39b1f 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -194,38 +194,6 @@ class Item < ActiveRecord::Base Species.find(species_ids) end - def self.search(query, user, locale) - raise SearchError, "Please provide a search query" unless query - query = query.strip - raise SearchError, "Search queries should be at least 3 characters" if query.length < 3 - query_conditions = [Condition.new] - in_phrase = false - query.each_char do |c| - if c == ' ' && !in_phrase - query_conditions << Condition.new - elsif c == '"' - in_phrase = !in_phrase - elsif c == ':' && !in_phrase - query_conditions.last.to_filter! - elsif c == '-' && !in_phrase && query_conditions.last.empty? - query_conditions.last.negate! - else - query_conditions.last << c - end - end - limited_filters_used = [] - query_conditions.inject(self.with_translations(locale)) do |scope, condition| - if condition.filter? && LimitedSearchFilters.include?(condition.filter) - if limited_filters_used.include?(condition.filter) - raise SearchError, "The #{condition.filter} filter is complex; please only use one per search. Thanks!" - else - limited_filters_used << condition.filter - end - end - condition.narrow(scope, user) - end - end - def as_json(options = {}) { :description => description, @@ -709,214 +677,4 @@ class Item < ActiveRecord::Base class SpiderHTTPError < SpiderError;end class SpiderJSONError < SpiderError;end end - - private - - SearchFilterScopes = [] - LimitedSearchFilters = [] - - def self.search_filter(name, options={}, &block) - assume_complement = options.delete(:assume_complement) || true - name = name.to_s - SearchFilterScopes << name - LimitedSearchFilters << name if options[:limit] - - (class << self; self; end).instance_eval do - if options[:full] - define_method "search_filter_#{name}", &options[:full] - else - if assume_complement - define_method "search_filter_not_#{name}", &Item.search_filter_block(options, false, &block) - end - define_method "search_filter_#{name}", &Item.search_filter_block(options, true, &block) - end - end - end - - def self.single_search_filter(name, options={}, &block) - options[:assume_complement] = false - search_filter name, options, &block - end - - def self.search_filter_block(options, positive, &block) - Proc.new { |str, user, scope| - condition = block.arity == 1 ? block.call(str) : block.call(str, user) - unless positive - condition = condition.to_sql if condition.respond_to?(:to_sql) - condition = "!(#{condition})" - end - scope = scope.send(options[:scope]) if options[:scope] - scope.where(condition) - } - end - - search_filter :name do |name| - Item::Translation.arel_table[:name].matches("%#{name}%") - end - - search_filter :description do |description| - Item::Translation.arel_table[:description].matches("%#{description}%") - end - - def self.adjective_filters - @adjective_filters ||= { - 'nc' => arel_table[:rarity_index].in(NCRarities), - 'pb' => arel_table[:description].eq(PAINTBRUSH_SET_DESCRIPTION) - } - end - - search_filter :is do |adjective| - filter = adjective_filters[adjective] - unless filter - raise SearchError, - "We don't know how an item can be \"#{adjective}\". " + - "Did you mean is:nc or is:pb?" - end - filter - end - - USER_ADJECTIVES = { - 'own' => true, - 'owns' => true, - 'owned' => true, - 'want' => false, - 'wants' => false, - 'wanted' => false, - 'all' => nil, - 'items' => nil - } - def self.parse_user_adjective(adjective, user) - unless USER_ADJECTIVES.has_key?(adjective) - raise SearchError, "We don't understand user:#{adjective}. " + - "Find items you own with user:owns, items you want with user:wants, or " + - "both with user:all" - end - - unless user - raise SearchError, "It looks like you're not logged in, so you don't own any items." - end - - USER_ADJECTIVES[adjective] - end - - search_filter :user do |adjective, user| - # Though joins may seem more efficient here for the positive case, we need - # to be able to handle cases like "user:owns user:wants", which breaks on - # the JOIN approach. Just have to look up the IDs in advance. - - owned_value = parse_user_adjective(adjective, user) - hangers = ClosetHanger.arel_table - items = user.closeted_items - items = items.where(ClosetHanger.arel_table[:owned].eq(owned_value)) unless owned_value.nil? - item_ids = items.map(&:id) - # Though it's best to do arel_table[:id].in(item_ids), it breaks in this - # version of Arel, and other conditions will overwrite this one. Since IDs - # are guaranteed to be integers, let's just build our own string condition - # and be done with it. - - if item_ids.empty? - raise SearchError, "You don't #{ClosetHanger.verb :you, owned_value} " + - "any items yet. Head to Your Items to add some!" - end - - arel_table[:id].in(item_ids) - end - - search_filter :only do |species_name| - begin - id = Species.require_by_name(species_name).id - rescue Species::NotFound => e - raise SearchError, e.message - end - arel_table[:species_support_ids].eq(id.to_s) - end - - search_filter :species do |species_name| - begin - id = Species.require_by_name(species_name).id - rescue Species::NotFound => e - raise SearchError, e.message - end - ids = arel_table[:species_support_ids] - ids.eq('').or(ids.matches_any([ - id, - "#{id},%", - "%,#{id},%", - "%,#{id}" - ])) - end - - single_search_filter :type, {:limit => true, :scope => :join_swf_assets} do |zone_set_name| - zone_set = Zone.find_set(zone_set_name) - raise SearchError, "Type \"#{zone_set_name}\" does not exist" unless zone_set - SwfAsset.arel_table[:zone_id].in(zone_set.map(&:id)) - end - - single_search_filter :not_type, :full => lambda { |zone_set_name, user, scope| - zone_set = Zone::ItemZoneSets[zone_set_name] - raise SearchError, "Type \"#{zone_set_name}\" does not exist" unless zone_set - psa = ParentSwfAssetRelationship.arel_table.alias - sa = SwfAsset.arel_table.alias - # Join to SWF assets, including the zone condition in the join so that - # SWFs that don't match end up being NULL rows. Then we take the max SWF - # asset ID, which is NULL if and only if there are no rows that matched - # the zone requirement. If that max was NULL, return the object. - item_ids = select(arel_table[:id]).joins( - "LEFT JOIN #{ParentSwfAssetRelationship.table_name} #{psa.name} ON " + - psa[:parent_type].eq(self.name). - and(psa[:parent_id].eq(arel_table[:id])). - to_sql - ). - joins( - "LEFT JOIN #{SwfAsset.table_name} #{sa.name} ON " + - sa[:type].eq(SwfAssetType). - and(sa[:id].eq(psa[:swf_asset_id])). - and(sa[:zone_id].in(zone_set.map(&:id))). - to_sql - ). - group("#{table_name}.id"). - having("MAX(#{sa.name}.id) IS NULL"). # SwfAsset.arel_table[:id].maximum has no #eq - map(&:id) - scope.where(arel_table[:id].in(item_ids)) - } - - class Condition < String - attr_accessor :filter - - def initialize - @positive = true - end - - def filter? - !@filter.nil? - end - - def to_filter! - @filter = self.clone - self.replace '' - end - - def negate! - @positive = !@positive - end - - def narrow(scope, user) - if SearchFilterScopes.include?(filter) - polarized_filter = @positive ? filter : "not_#{filter}" - Item.send("search_filter_#{polarized_filter}", self, user, scope) - else - raise SearchError, "Filter #{filter} does not exist" - end - end - - def filter - @filter || 'name' - end - - def inspect - @filter ? "#{@filter}:#{super}" : super - end - end - - class SearchError < ArgumentError;end end