module ActiveRecord module Acts #:nodoc: module Taggable #:nodoc: def self.included(base) base.extend(ClassMethods) end module ClassMethods def acts_as_taggable(options = {}) write_inheritable_attribute(:acts_as_taggable_options, { :taggable_type => ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s, :from => options[:from] }) class_inheritable_reader :acts_as_taggable_options has_many :taggings, :as => :taggable, :dependent => true has_many :tags, :through => :taggings include ActiveRecord::Acts::Taggable::InstanceMethods extend ActiveRecord::Acts::Taggable::SingletonMethods end end module SingletonMethods def find_tagged_with(list, options = {}) local_options = { :limit => 1000, :offset => 0 }.merge(options) find_by_sql([ "SELECT DISTINCT #{table_name}.* FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]})" if local_options[:conditions]} LIMIT ? OFFSET ?", acts_as_taggable_options[:taggable_type], list, local_options[:limit], local_options[:offset] ]) end def count_tagged_with(list, options = {}) local_options = {}.merge(options) find_by_sql([ "SELECT COUNT(DISTINCT #{table_name}.#{primary_key}) AS cnt FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]}) " if local_options[:conditions]}", acts_as_taggable_options[:taggable_type], list ]).first.cnt.to_i end def find_tagged_with_intersecting(list, options = {}) local_options = { :limit => 1000, :offset => 0 }.merge(options) find_by_sql([ "SELECT DISTINCT #{table_name}.* FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]}) " if local_options[:conditions]} GROUP BY #{table_name}.id HAVING COUNT(#{table_name}.id) = #{list.size} LIMIT ? OFFSET ?", acts_as_taggable_options[:taggable_type], list, local_options[:limit], local_options[:offset] ]) end def count_tagged_with_intersecting(list, options = {}) local_options = {}.merge(options) find_by_sql([ "SELECT COUNT(*) AS cnt FROM (SELECT #{table_name}.#{primary_key} AS cnt FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?) " + "#{"AND (#{local_options[:conditions]})" if local_options[:conditions]} " + "GROUP BY taggings.taggable_id HAVING COUNT(taggings.taggable_id) = #{list.size}) AS x", acts_as_taggable_options[:taggable_type], list ]).first.cnt.to_i end end module InstanceMethods def tag_with(list) Tag.transaction do taggings.destroy_all Tag.parse(list).each do |name| if acts_as_taggable_options[:from] send(acts_as_taggable_options[:from]).tags.find_or_create_by_name(name).on(self) else Tag.find_or_create_by_name(name).on(self) end end end end def tag_list tags.collect { |tag| tag.name.include?(" ") ? "'#{tag.name}'" : tag.name }.join(" ") end end end end end
These are the sorts of tests I'm then doing:
assert_equal 2, Post.find_tagged_with_intersecting(%w{TagOne TagTwo}).size assert_equal 2, Post.count_tagged_with_intersecting(%w{TagOne TagTwo}) assert_equal 1, Post.find_tagged_with_intersecting(%w{TagOne TagTwo}, :conditions => 'status = 1').size assert_equal 1, Post.count_tagged_with_intersecting(%w{TagOne TagTwo}, :conditions => 'status = 1') p = Post.find_tagged_with("TagOne", :conditions => 'status = 1')
- Robert Dempsey