# Includes calagator-specific modifications to the default has_many_polymorphs generated tagging_extensions.rb # These are denoted with #calagator in the source. class ActiveRecord::Base #:nodoc: # These extensions make models taggable. This file is automatically generated and required by your app if you run the tagging generator included with has_many_polymorphs. module TaggingExtensions def self.included(base) base.after_create do |record| record.apply_cached_tags end end def apply_cached_tags tag_with @cached_tags if taggable? && !@cached_tags.blank? end # Add tags to self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. # # We need to avoid name conflicts with the built-in ActiveRecord association methods, thus the underscores. def _add_tags incoming taggable?(true) tag_cast_to_string(incoming).each do |tag_name| begin tag = Tag.find_or_create_by_name(tag_name) raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record? tag.taggables << self rescue ActiveRecord::StatementInvalid => e raise unless e.to_s =~ /duplicate/i end end end # Removes tags from self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. def _remove_tags outgoing taggable?(true) outgoing = tag_cast_to_string(outgoing) tags.delete(*(tags.select do |tag| outgoing.include? tag.name end)) end # Returns the tags on self as a string. def tag_list # Redefined later to avoid an RDoc parse error. end # Replace the existing tags on self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. def tag_with list #:stopdoc: taggable?(true) list = tag_cast_to_string(list) # Transactions may not be ideal for you here; be aware. Tag.transaction do current = tags.map(&:name) _add_tags(list - current) _remove_tags(current - list) end self #:startdoc: end # Returns the tags on self as a string. def tag_list #:nodoc: return @cached_tags || "" if new_record? #:stopdoc: taggable?(true) tags.reload tags.to_s #:startdoc: end def tag_list=(value) tag_with value unless new_record? @cached_tags = value if new_record? end private # TODO Why is this called "to_string" when it returns an Array? def tag_cast_to_string obj #:nodoc: tags = case obj when Array obj.map! do |item| case item when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot. when Tag then item.name when String then item else raise "Invalid type" end end when String obj else raise "Invalid object of class #{obj.class} as tagging method parameter" end return(self.class.parse_tags([tags])) end # Check if a model is in the :taggables target list. The alternative to this check is to explicitly include a TaggingMethods module (which you would create) in each target model. def taggable?(should_raise = false) #:nodoc: unless flag = respond_to?(:tags) raise "#{self.class} is not a taggable model" if should_raise end flag end end module TaggingFinders # # Find all the objects tagged with the supplied list of tags # # Usage : Model.tagged_with("ruby") # Model.tagged_with("hello", "world") # Model.tagged_with("hello", "world", :limit => 10) # def tagged_with(*tag_list) options = tag_list.last.is_a?(Hash) ? tag_list.pop : {} tag_list = parse_tags(tag_list) scope = scope(:find) options[:select] ||= "#{table_name}.*" options[:from] ||= "#{table_name}, tags, taggings" sql = "SELECT #{(scope && scope[:select]) || options[:select]} " sql << "FROM #{(scope && scope[:from]) || options[:from]} " add_joins!(sql, options, scope) sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' " sql << "AND taggings.tag_id = tags.id " tag_list_condition = tag_list.map {|t| connection.quote(t)}.join("#{Tag::DELIMITER} ") sql << "AND (tags.name IN (#{tag_list_condition})) " sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions] sql << "GROUP BY #{table_name}.id " sql << "HAVING COUNT(taggings.tag_id) = #{tag_list.size}" add_order!(sql, options[:order], scope) add_limit!(sql, options, scope) add_lock!(sql, options, scope) find_by_sql(sql) end def parse_tags(tags) if tags.blank? [] else [tags] \ .flatten \ .map{|tag| tag.split(Tag::DELIMITER)} \ .flatten \ .compact \ .map{|tag| tag.strip.squeeze(" ")} \ .reject{|tag| tag.blank?} \ .map(&:downcase).uniq end end end include TaggingExtensions extend TaggingFinders end