diff --git a/lib/tagging_extensions.rb b/lib/tagging_extensions.rb index ab47bf1f..51ad2e08 100644 --- a/lib/tagging_extensions.rb +++ b/lib/tagging_extensions.rb @@ -1,4 +1,3 @@ - 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. @@ -13,7 +12,7 @@ class ActiveRecord::Base #:nodoc: 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 + tags << tag rescue ActiveRecord::StatementInvalid => e raise unless e.to_s =~ /duplicate/i end @@ -24,7 +23,6 @@ class ActiveRecord::Base #:nodoc: def _remove_tags outgoing taggable?(true) outgoing = tag_cast_to_string(outgoing) - tags.delete(*(tags.select do |tag| outgoing.include? tag.name end)) @@ -36,7 +34,7 @@ class ActiveRecord::Base #:nodoc: 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 + def tag_with list #:stopdoc: taggable?(true) list = tag_cast_to_string(list) @@ -60,7 +58,11 @@ class ActiveRecord::Base #:nodoc: tags.to_s #:startdoc: end - + + def tag_list=(value) + tag_with(value) + end + private def tag_cast_to_string obj #:nodoc: @@ -68,8 +70,7 @@ class ActiveRecord::Base #:nodoc: when Array obj.map! do |item| case item - # removed next line: its prevents adding numbers as tags - # when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot. + 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 @@ -78,7 +79,7 @@ class ActiveRecord::Base #:nodoc: end when String obj = obj.split(Tag::DELIMITER).map do |tag_name| - tag_name.strip.squeeze(" ") + tag_name.strip.squeeze(" ") end else raise "Invalid object of class #{obj.class} as tagging method parameter" @@ -96,13 +97,14 @@ class ActiveRecord::Base #:nodoc: 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) # + # XXX This query strategy is not performant, and needs to be rewritten as an inverted join or a series of unions + # def tagged_with(*tag_list) options = tag_list.last.is_a?(Hash) ? tag_list.pop : {} tag_list = parse_tags(tag_list) @@ -114,17 +116,22 @@ class ActiveRecord::Base #:nodoc: sql = "SELECT #{(scope && scope[:select]) || options[:select]} " sql << "FROM #{(scope && scope[:from]) || options[:from]} " - add_joins!(sql, options, scope) + add_joins!(sql, options[:joins], 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| "'#{t}'"}.join(", ") + tag_list_condition = tag_list.map {|name| "'#{name}'"}.join(", ") sql << "AND (tags.name IN (#{sanitize_sql(tag_list_condition)})) " sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions] - sql << "GROUP BY #{table_name}.id " + + columns = column_names.map do |column| + "#{table_name}.#{column}" + end.join(", ") + + sql << "GROUP BY #{columns} " sql << "HAVING COUNT(taggings.tag_id) = #{tag_list.size}" add_order!(sql, options[:order], scope) @@ -133,6 +140,47 @@ class ActiveRecord::Base #:nodoc: find_by_sql(sql) end + + def self.tagged_with_any(*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}, meta_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.meta_tag_id = meta_tags.id " + + sql << "AND (" + or_options = [] + tag_list.each do |name| + or_options << "meta_tags.name = '#{name}'" + end + or_options_joined = or_options.join(" OR ") + sql << "#{or_options_joined}) " + + + sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions] + + columns = column_names.map do |column| + "#{table_name}.#{column}" + end.join(", ") + + sql << "GROUP BY #{columns} " + + 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) return [] if tags.blank?