module ActsAsTaggable #:nodoc: module ActiveRecordExtension extend ActiveSupport::Concern included do self.extend(ClassMethods) end module ClassMethods def acts_as_taggable has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag has_many :tags, :through => :taggings before_save :save_cached_tag_list after_save :save_tags #include ActsAsTaggable::ActiveRecordExtension::InstanceMethods extend ActsAsTaggable::ActiveRecordExtension::SingletonMethods alias_method_chain :reload, :tag_list end def cached_tag_list_column_name "cached_tag_list" end def set_cached_tag_list_column_name(value = nil, &block) define_attr_method :cached_tag_list_column_name, value, &block end end module SingletonMethods # Pass either a tag, string, or an array of strings or tags. # # Options: # - +:match_any+ - match any of the given tags (default). # - +:match_all+ - match all of the given tags. # def tagged_with(tags, options = {}) tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags) return [] if tags.empty? records = select("DISTINCT #{quoted_table_name}.*") if options[:match_all] records.search_all_tags(tags) else records.search_any_tags(tags) end end # Matches records that have none of the given tags. def not_tagged_with(tags) tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags) sub = Tagging.select("#{Tagging.table_name}.taggable_id").joins(:tag). where(:taggable_type => base_class.name, "#{Tag.table_name}.name" => tags) where("#{quoted_table_name}.#{primary_key} NOT IN (" + sub.to_sql + ")") end # Returns an array of related tags. Related tags are all the other tags # that are found on the models tagged with the provided tags. def related_tags(tags) search_related_tags(tags) end # Counts the number of occurences of all tags. # See Tag.counts for options. def tag_counts(options = {}) tags = Tag.joins(:taggings). where("#{Tagging.table_name}.taggable_type" => base_class.name) if options[:tags] tags = tags.where("#{Tag.table_name}.name" => options.delete(:tags)) end unless descends_from_active_record? tags = tags.joins("INNER JOIN #{quoted_table_name} ON " + "#{quoted_table_name}.#{primary_key} = #{Tagging.quoted_table_name}.taggable_id") tags = tags.where(type_condition) end if scoped != unscoped sub = scoped.except(:select).select("#{quoted_table_name}.#{primary_key}") tags = tags.where("#{Tagging.quoted_table_name}.taggable_id IN (#{sub.to_sql})") end tags.counts(options) end # Returns an array of related tags. # Related tags are all the other tags that are found on the models # tagged with the provided tags. # # Pass either a tag, string, or an array of strings or tags. # # Options: # - +:order+ - SQL Order how to order the tags. Defaults to "count_all DESC, tags.name". # - +:include+ # # DEPRECATED: use #related_tags instead. def find_related_tags(tags, options = {}) ActiveSupport::Deprecation.warn "#find_related_tags() is deprecated and will be removed in the next release. Use #related_tags", caller rs = related_tags(tags).order(options[:order] || "count DESC, #{Tag.quoted_table_name}.name") rs = rs.includes(options[:include]) if options[:include] rs end # Pass either a tag, string, or an array of strings or tags. # # Options: # - +:exclude+ - Find models that are not tagged with the given tags # - +:match_all+ - Find models that match all of the given tags, not just one # - +:conditions+ - A piece of SQL conditions to add to the query # - +:include+ # # DEPRECATED: use #tagged_with and #not_tagged_with instead. def find_tagged_with(*args) ActiveSupport::Deprecation.warn "#find_tagged_with() is deprecated and will be removed in the next release. Use #tagged_with and #not_tagged_with instead", caller options = args.extract_options! tags = args.first records = self records = records.where(options[:conditions]) if options[:conditions] records = records.includes(options[:include]) if options[:include] records = records.order(options[:order]) if options[:order] if options[:exclude] records.not_tagged_with(tags) else records.tagged_with(tags, options) end end def caching_tag_list? column_names.include?(cached_tag_list_column_name) end #protected def joins_tags(options = {}) # :nodoc: options[:suffix] = "_#{options[:suffix]}" if options[:suffix] taggings_alias = connection.quote_table_name(Tagging.table_name + options[:suffix].to_s) tags_alias = connection.quote_table_name(Tag.table_name + options[:suffix].to_s) taggings = "INNER JOIN #{Tagging.quoted_table_name} AS #{taggings_alias} " + "ON #{taggings_alias}.taggable_id = #{quoted_table_name}.#{primary_key} " + "AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" tags = "INNER JOIN #{Tag.quoted_table_name} AS #{tags_alias} " + "ON #{tags_alias}.id = #{taggings_alias}.tag_id " tags += "AND #{tags_alias}.name LIKE #{quote_value(options[:tag_name])}" if options[:tag_name] joins([taggings, tags]) end def search_all_tags(tags) records = self tags.dup.each_with_index do |tag_name, index| records = records.joins_tags(:suffix => index, :tag_name => tag_name) end records end def search_any_tags(tags) joins(:tags).where(Tag.arel_table[:name].matches_any(tags.dup)) end def search_related_tags(tags) tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags) return where('1=0') if tags.empty? sub = select("#{quoted_table_name}.#{primary_key}").search_any_tags(tags) _tags = tags.map { |tag| tag.downcase } Tag.select("#{Tag.quoted_table_name}.*, COUNT(#{Tag.quoted_table_name}.id) AS count"). joins(:taggings). where("#{Tagging.table_name}.taggable_type" => base_class.name). where("#{Tagging.quoted_table_name}.taggable_id IN (" + sub.to_sql + ")"). group("#{Tag.quoted_table_name}.name"). having(Tag.arel_table[:name].does_not_match_all(_tags)) end end def tag_list return @tag_list if @tag_list if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil? @tag_list = TagList.from(cached_value) else @tag_list = TagList.new(*tags.map(&:name)) end end def tag_list=(value) @tag_list = TagList.from(value) end def save_cached_tag_list if self.class.caching_tag_list? self[self.class.cached_tag_list_column_name] = tag_list.to_s end end def save_tags return unless @tag_list new_tag_names = @tag_list - tags.map(&:name) old_tags = tags.reject { |tag| @tag_list.include?(tag.name) } self.class.transaction do if old_tags.any? taggings.where(:tag_id => old_tags.map(&:id)).each(&:destroy) taggings.reset end new_tag_names.each do |new_tag_name| tags << Tag.find_or_create_with_like_by_name(new_tag_name) end end true end # Calculate the tag counts for the tags used by this model. # See Tag.counts for available options. def tag_counts(options = {}) return [] if tag_list.blank? self.class.tag_counts(options.merge(:tags => tag_list)) end def reload_with_tag_list(*args) #:nodoc: @tag_list = nil reload_without_tag_list(*args) end end end