2008-12-21 23:15:55 +01:00
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
# Add tags to <tt>self</tt>. 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.
2008-12-22 20:55:32 +01:00
def _add_tags incoming
2008-12-21 23:15:55 +01:00
taggable? ( true )
tag_cast_to_string ( incoming ) . each do | tag_name |
2011-02-08 22:24:06 +01:00
# added following check to prevent empty tags from being saved (which will fail)
unless tag_name . blank?
begin
tag = Tag . find_or_create_by_name ( tag_name )
raise Tag :: Error , " tag could not be saved: #{ tag_name } " if tag . new_record?
tags << tag
rescue ActiveRecord :: StatementInvalid = > e
raise unless e . to_s =~ / duplicate /i
end
2008-12-21 23:15:55 +01:00
end
end
end
# Removes tags from <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
2008-12-22 20:55:32 +01:00
def _remove_tags outgoing
2008-12-21 23:15:55 +01:00
taggable? ( true )
outgoing = tag_cast_to_string ( outgoing )
tags . delete ( * ( tags . select do | tag |
2011-02-08 22:24:06 +01:00
outgoing . include? tag . name
end ) )
2008-12-22 23:26:38 +01:00
end
2008-12-21 23:15:55 +01:00
2011-02-08 22:24:06 +01:00
# Returns the tags on <tt>self</tt> as a string.
2008-12-21 23:15:55 +01:00
def tag_list
# Redefined later to avoid an RDoc parse error.
end
# Replace the existing tags on <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
2011-02-08 22:04:05 +01:00
def tag_with list
2008-12-21 23:15:55 +01:00
#: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 )
2008-12-22 20:55:32 +01:00
_add_tags ( list - current )
_remove_tags ( current - list )
2008-12-21 23:15:55 +01:00
end
self
#:startdoc:
end
2011-02-08 22:24:06 +01:00
# Returns the tags on <tt>self</tt> as a string.
2008-12-21 23:15:55 +01:00
def tag_list #:nodoc:
#:stopdoc:
taggable? ( true )
tags . reload
tags . to_s
#:startdoc:
end
2011-02-08 22:04:05 +01:00
def tag_list = ( value )
2011-02-08 22:24:06 +01:00
tag_with ( value )
2011-02-08 22:04:05 +01:00
end
2008-12-21 23:15:55 +01:00
private
def tag_cast_to_string obj #:nodoc:
case obj
2011-02-08 22:24:06 +01:00
when Array
obj . map! do | item |
case item
# removed next line as it prevents using numbers as tags
# 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 "
2008-12-21 23:15:55 +01:00
end
2011-02-08 22:24:06 +01:00
end
when String
obj = obj . split ( Tag :: DELIMITER ) . map do | tag_name |
tag_name . strip . squeeze ( " " )
end
else
raise " Invalid object of class #{ obj . class } as tagging method parameter "
2008-12-21 23:15:55 +01:00
end . flatten . compact . map ( & :downcase ) . uniq
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)
#
2011-02-08 22:04:05 +01:00
# XXX This query strategy is not performant, and needs to be rewritten as an inverted join or a series of unions
#
2008-12-21 23:15:55 +01:00
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 ] } "
2011-02-08 22:04:05 +01:00
add_joins! ( sql , options [ :joins ] , scope )
2008-12-21 23:15:55 +01:00
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 "
2011-02-08 22:04:05 +01:00
tag_list_condition = tag_list . map { | name | " ' #{ name } ' " } . join ( " , " )
2008-12-21 23:15:55 +01:00
sql << " AND (tags.name IN ( #{ sanitize_sql ( tag_list_condition ) } )) "
sql << " AND #{ sanitize_sql ( options [ :conditions ] ) } " if options [ :conditions ]
2011-02-08 22:04:05 +01:00
columns = column_names . map do | column |
" #{ table_name } . #{ column } "
end . join ( " , " )
sql << " GROUP BY #{ columns } "
2008-12-21 23:15:55 +01:00
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
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
def self . tagged_with_any ( * tag_list )
options = tag_list . last . is_a? ( Hash ) ? tag_list . pop : { }
tag_list = parse_tags ( tag_list )
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
scope = scope ( :find )
options [ :select ] || = " #{ table_name } .* "
options [ :from ] || = " #{ table_name } , meta_tags, taggings "
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
sql = " SELECT #{ ( scope && scope [ :select ] ) || options [ :select ] } "
sql << " FROM #{ ( scope && scope [ :from ] ) || options [ :from ] } "
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
add_joins! ( sql , options , scope )
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
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 "
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
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 } ) "
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
sql << " AND #{ sanitize_sql ( options [ :conditions ] ) } " if options [ :conditions ]
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
columns = column_names . map do | column |
" #{ table_name } . #{ column } "
end . join ( " , " )
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
sql << " GROUP BY #{ columns } "
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
add_order! ( sql , options [ :order ] , scope )
add_limit! ( sql , options , scope )
add_lock! ( sql , options , scope )
2011-02-08 22:04:05 +01:00
2011-02-08 22:24:06 +01:00
find_by_sql ( sql )
end
2008-12-21 23:15:55 +01:00
def parse_tags ( tags )
return [ ] if tags . blank?
tags = Array ( tags ) . first
tags = tags . respond_to? ( :flatten ) ? tags . flatten : tags . split ( Tag :: DELIMITER )
tags . map { | tag | tag . strip . squeeze ( " " ) } . flatten . compact . map ( & :downcase ) . uniq
end
end
include TaggingExtensions
extend TaggingFinders
end