module ActsAsTaggableOn::Taggable module Collection def self.included(base) base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods base.initialize_acts_as_taggable_on_collection end module ClassMethods def initialize_acts_as_taggable_on_collection tag_types.map(&:to_s).each do |tag_type| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.#{tag_type.singularize}_counts(options={}) tag_counts_on('#{tag_type}', options) end def #{tag_type.singularize}_counts(options = {}) tag_counts_on('#{tag_type}', options) end def top_#{tag_type}(limit = 10) tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i) end def self.top_#{tag_type}(limit = 10) tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i) end RUBY end end def acts_as_taggable_on(*args) super(*args) initialize_acts_as_taggable_on_collection end def tag_counts_on(context, options = {}) all_tag_counts(options.merge({on: context.to_s})) end def tags_on(context, options = {}) all_tags(options.merge({on: context.to_s})) end ## # Calculate the tag names. # To be used when you don't need tag counts and want to avoid the taggable joins. # # @param [Hash] options Options: # * :start_at - Restrict the tags to those created after a certain time # * :end_at - Restrict the tags to those created before a certain time # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons. # * :limit - The maximum number of tags to return # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' # * :on - Scope the find to only include a certain context def all_tags(options = {}) options = options.dup options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on ## Generate conditions: options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] ## Generate scope: tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id") tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit]) # Joins and conditions tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } tag_scope = tag_scope.where(options[:conditions]) group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore: tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns) tag_scope_joins(tag_scope, tagging_scope) end ## # Calculate the tag counts for all tags. # # @param [Hash] options Options: # * :start_at - Restrict the tags to those created after a certain time # * :end_at - Restrict the tags to those created before a certain time # * :conditions - A piece of SQL conditions to add to the query # * :limit - The maximum number of tags to return # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' # * :at_least - Exclude tags with a frequency less than the given value # * :at_most - Exclude tags with a frequency greater than the given value # * :on - Scope the find to only include a certain context def all_tag_counts(options = {}) options = options.dup options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id ## Generate conditions: options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] ## Generate joins: taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id" taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition ## Generate scope: tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count") tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit]) # Joins and conditions tagging_scope = tagging_scope.joins(taggable_join) tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } tag_scope = tag_scope.where(options[:conditions]) # GROUP BY and HAVING clauses: having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"] having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least] having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most] having = having.compact.join(' AND ') group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" unless options[:id] # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore: tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) end tagging_scope = tagging_scope.group(group_columns).having(having) tag_scope_joins(tag_scope, tagging_scope) end def safe_to_sql(relation) connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql end private def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) table_name_pkey = "#{table_name}.#{primary_key}" if ActsAsTaggableOn::Utils.using_mysql? # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details scoped_ids = select(table_name_pkey).map(&:id) tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids) else tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})") end tagging_scope end def tagging_conditions(options) tagging_conditions = [] tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at] tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at] taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name]) taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on] taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id] tagging_conditions.push taggable_conditions tagging_conditions end def tag_scope_joins(tag_scope, tagging_scope) tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id") tag_scope.extending(CalculationMethods) end end def tag_counts_on(context, options={}) self.class.tag_counts_on(context, options.merge(id: id)) end module CalculationMethods def count(column_name=:all) # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2 super end end end end