module ActsAsTaggableOn::Taggable module Core def self.included(base) base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods base.class_eval do attr_writer :custom_contexts after_save :save_tags end base.initialize_acts_as_taggable_on_core end module ClassMethods def initialize_acts_as_taggable_on_core tag_types.map(&:to_s).each do |tags_type| tag_type = tags_type.to_s.singularize context_taggings = "#{tag_type}_taggings".to_sym context_tags = tags_type.to_sym class_eval do has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagging", :conditions => ['#{Tagging.table_name}.tagger_id IS NULL AND #{Tagging.table_name}.context = ?', tags_type] has_many context_tags, :through => context_taggings, :source => :tag end class_eval %( def #{tag_type}_list tag_list_on('#{tags_type}') end def #{tag_type}_list=(new_tags) set_tag_list_on('#{tags_type}', new_tags) end def all_#{tags_type}_list all_tags_list_on('#{tags_type}') end ) end end def acts_as_taggable_on(*args) super(*args) initialize_acts_as_taggable_on_core end # all column names are necessary for PostgreSQL group clause def grouped_column_names_for(object) object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ") end ## # Return a scope of objects that are tagged with the specified tags. # # @param tags The tags that we want to query for # @param [Hash] options A hash of options to alter you query: # * :exclude - if set to true, return objects that are *NOT* tagged with the specified tags # * :any - if set to true, return objects that are tagged with *ANY* of the specified tags # * :match_all - if set to true, return objects that are *ONLY* tagged with the specified tags # # Example: # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool def tagged_with(tags, options = {}) tag_list = TagList.from(tags) return {} if tag_list.empty? joins = [] conditions = [] context = options.delete(:on) if options.delete(:exclude) tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ") conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})" elsif options.delete(:any) tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ") conditions << "#{table_name}.#{primary_key} IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})" else tags = Tag.named_any(tag_list) return scoped(:conditions => "1 = 0") unless tags.length == tag_list.length tags.each do |tag| safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '') prefix = "#{safe_tag}_#{rand(1024)}" taggings_alias = "#{table_name}_taggings_#{prefix}" tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" + " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" + " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" + " AND #{taggings_alias}.tag_id = #{tag.id}" tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context joins << tagging_join end end taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group" if options.delete(:match_all) joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" + " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" + " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}" end scoped(:joins => joins.join(" "), :group => group, :conditions => conditions.join(" AND "), :readonly => false) end def is_taggable? true end end module InstanceMethods # all column names are necessary for PostgreSQL group clause def grouped_column_names_for(object) self.class.grouped_column_names_for(object) end def custom_contexts @custom_contexts ||= [] end def is_taggable? self.class.is_taggable? end def add_custom_context(value) custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s) end def cached_tag_list_on(context) self["cached_#{context.to_s.singularize}_list"] end def tag_list_cache_set_on(context) variable_name = "@#{context.to_s.singularize}_list" !instance_variable_get(variable_name).nil? end def tag_list_cache_on(context) variable_name = "@#{context.to_s.singularize}_list" instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name))) end def tag_list_on(context) add_custom_context(context) tag_list_cache_on(context) end def all_tags_list_on(context) variable_name = "@all_#{context.to_s.singularize}_list" return instance_variable_get(variable_name) if instance_variable_get(variable_name) instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze) end ## # Returns all tags of a given context def all_tags_on(context) opts = ["#{Tagging.table_name}.context = ?", context.to_s] base_tags.where(opts).order("#{Tagging.table_name}.created_at").group("#{Tagging.table_name}.tag_id").all end ## # Returns all tags that are not owned of a given context def tags_on(context) base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all end def set_tag_list_on(context, new_list) add_custom_context(context) variable_name = "@#{context.to_s.singularize}_list" instance_variable_set(variable_name, TagList.from(new_list)) end def tagging_contexts custom_contexts + self.class.tag_types.map(&:to_s) end def reload self.class.tag_types.each do |context| instance_variable_set("@#{context.to_s.singularize}_list", nil) instance_variable_set("@all_#{context.to_s.singularize}_list", nil) end super end def save_tags tagging_contexts.each do |context| next unless tag_list_cache_set_on(context) tag_list = tag_list_cache_on(context).uniq # Find existing tags or create non-existing tags: tag_list = Tag.find_or_create_all_with_like_by_name(tag_list) current_tags = tags_on(context) old_tags = current_tags - tag_list new_tags = tag_list - current_tags # Find taggings to remove: old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil, :context => context.to_s, :tag_id => old_tags).all if old_taggings.present? # Destroy old taggings: Tagging.destroy_all :id => old_taggings.map(&:id) end # Create new taggings: new_tags.each do |tag| taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self) end end true end end end end