module ActsAsTaggableOn::Taggable::Core::ClassMethods

Public Instance Methods

adjust_taggings_alias(taggings_alias) click to toggle source
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 219
def adjust_taggings_alias(taggings_alias)
  if taggings_alias.size > 75
    taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
  end
  taggings_alias
end
grouped_column_names_for(object) click to toggle source

all column names are necessary for PostgreSQL group clause

# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 62
def grouped_column_names_for(object)
  object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ')
end
initialize_acts_as_taggable_on_core() click to toggle source
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 15
      def initialize_acts_as_taggable_on_core
        include taggable_mixin
        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
          taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])

          class_eval do
            # when preserving tag order, include order option so that for a 'tags' context
            # the associations tag_taggings & tags are always returned in created order
            has_many_with_taggable_compatibility context_taggings, as: :taggable,
                                                 dependent: :destroy,
                                                 class_name: 'ActsAsTaggableOn::Tagging',
                                                 order: taggings_order,
                                                 conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
                                                 include: :tag

            has_many_with_taggable_compatibility context_tags, through: context_taggings,
                                                 source: :tag,
                                                 class_name: 'ActsAsTaggableOn::Tag',
                                                 order: taggings_order

          end

          taggable_mixin.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
", __FILE__, __LINE__ + 1
        end
      end
is_taggable?() click to toggle source
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 215
def is_taggable?
  true
end
taggable_mixin() click to toggle source
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 226
def taggable_mixin
  @taggable_mixin ||= Module.new
end
taggable_on(preserve_tag_order, *tag_types) click to toggle source
Calls superclass method
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 56
def taggable_on(preserve_tag_order, *tag_types)
  super(preserve_tag_order, *tag_types)
  initialize_acts_as_taggable_on_core
end
tagged_with(tags, options = {}) click to toggle source

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:

* <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
* <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
* <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
* <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
* <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner

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", :any => true, :order_by_matching_tag_count => true)  # Sort by users who match the most tags, descending
User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
# File lib/acts_as_taggable_on/acts_as_taggable_on/core.rb, line 84
def tagged_with(tags, options = {})
  tag_list = ActsAsTaggableOn::TagList.from(tags)
  options = options.dup
  empty_result = where('1 = 0')

  return empty_result if tag_list.empty?

  joins = []
  conditions = []
  having = []
  select_clause = []
  order_by = []

  context = options.delete(:on)
  owned_by = options.delete(:owned_by)
  alias_base_name = undecorated_table_name.gsub('.', '_')
  quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''

  if options.delete(:exclude)
    if options.delete(:wild)
      tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(t)}%"]) }.join(' OR ')
    else
      tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
    end

    conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"

    if owned_by
      joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}"                  "  ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}"                  " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}"                  " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}"                  " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
    end

  elsif options.delete(:any)
    # get tags, drop out if nothing returned (we need at least one)
    tags = if options.delete(:wild)
             ActsAsTaggableOn::Tag.named_like_any(tag_list)
           else
             ActsAsTaggableOn::Tag.named_any(tag_list)
           end

    return empty_result unless tags.length > 0

    # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
    # avoid ambiguous column name
    taggings_context = context ? "_#{context}" : ''

    taggings_alias = adjust_taggings_alias(
        "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
    )

    tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}"                "  ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}"                " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
    tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context

    # don't need to sanitize sql, map all ids and join with OR logic
    conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(' OR ')
    select_clause = " #{table_name}.*" unless context and tag_types.one?

    if owned_by
      tagging_join << ' AND ' +
          sanitize_sql([
                           "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
                           owned_by.id,
                           owned_by.class.base_class.to_s
                       ])
    end

    joins << tagging_join
    group = "#{table_name}.#{primary_key}"
  else
    tags = ActsAsTaggableOn::Tag.named_any(tag_list)

    return empty_result unless tags.length == tag_list.length

    tags.each do |tag|
      taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
      tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}"                  "  ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
          " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
          " AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"

      tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context

      if owned_by
        tagging_join << ' AND ' +
            sanitize_sql([
                             "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
                             owned_by.id,
                             owned_by.class.base_class.to_s
                         ])
      end

      joins << tagging_join
    end
  end

  group ||= [] # Rails interprets this as a no-op in the group() call below
  if options.delete(:order_by_matching_tag_count)
    select_clause = "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
    group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
    group = group_columns
    order_by << "#{taggings_alias}_count DESC"

  elsif options.delete(:match_all)
    taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
    joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}"                "  ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}"                " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"

    joins << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context

    group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
    group = group_columns
    having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
  end

  order_by << options[:order] if options[:order].present?

  select(select_clause)
  .joins(joins.join(' '))
  .where(conditions.join(' AND '))
  .group(group)
  .having(having)
  .order(order_by.join(', '))
  .readonly(false)
end