adding acts_as_taggable plugin

git-svn-id: http://svn.barleysodas.com/barleysodas/trunk@32 0f7b21a7-9e3a-4941-bbeb-ce5c7c368fa7
master
andrew 2007-11-17 05:54:02 +00:00
parent 1e88dc6d10
commit f3940be586
9 changed files with 1040 additions and 0 deletions

70
vendor/plugins/acts_as_taggable/README vendored Normal file
View File

@ -0,0 +1,70 @@
= The Acts As Taggable Mixin
== Installation
To install or update the gem, simply execute:
gem install acts_as_taggable
To use the 'acts_as_taggable' library in your Rails application after installing
the gem, add this line at the end of your 'config/environment.rb' file:
require_gem 'acts_as_taggable'
== Usage Instructions
To use the acts_as_taggable mixin with your ActiveRecord objects, you must use
a normalized database schema for tagging (also know as folksnomies).
This means that you must have a table solely for holding tag names. Usually,
this table is named 'tags' having at least 2 columns: primary key (usually
an autoincrement integer called 'id' - the AR standard for PKs) and a 'name'
columns, usually a varchar. You must also have a defined ActiveRecord model
class that relates to this table, by default called 'Tag'.
For associating tags to your objects you also must have join tables that are
composed of at least 2 columns: the tags table foreign key (by default 'tag_id')
and your taggable object table foreign key.
If you´re using the simple has_and_belongs_to_many model, you must NOT have a
primary key (usually an 'id' column) defined on the join table. If you´re using
a full join model, you must add a primary key column to the join table. Please
see the RDoc documentation on acts_as_taggable macro and the :join_class_name
option for the differences between these two approaches.
For example, suppose you are tagging photos and you hold your photo data thru
the Photo model and on the 'photos' table. Your database schema would look
something like this (example suited for MySQL):
CREATE TABLE `tags` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) default NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `tags_photos` (
`tag_id` int(11) NOT NULL,
`photo_id` int(11) NOT NULL
)
CREATE TABLE `photos` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(255) default NULL,
`author_name` varchar(255) default NULL,
`image_path` varchar(255) default NULL,
PRIMARY KEY (`id`)
)
You would normally define 2 models to relate to these tables:
class Tag < ActiveRecord::Base
end
class Photo < ActiveRecord::Base
acts_as_taggable
end
Now you can easily apply and search for tags on photos in your Rails application.
This assumes you´re using only default naming conventions. For using the mix-in
with non-standard naming conventions, please see the proper RDoc documentation.

View File

@ -0,0 +1 @@
require 'taggable'

View File

@ -0,0 +1,493 @@
require 'active_support'
require 'active_record'
module ActiveRecord
module Acts #:nodoc:
module Taggable #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
def self.split_tag_names(tags, separator)
tag_names = []
if tags.is_a?(Array)
tag_names << tags
elsif tags.is_a?(String)
tag_names << (separator.is_a?(Proc) ? separator.call(tags) : tags.split(separator))
end
tag_names = tag_names.flatten.map { |name| name.strip }.uniq.compact #straight 'em up
end
# This mixin provides an easy way for addind tagging capabilities (also
# known as folksnomy) to your active record objects. It allows you to add
# tags to your objects as well as search for tagged objects.
#
# It assumes you are using a fully-normalized tagging database schema. For
# that, you need a table (by default, named +tags+) to hold all tags in your
# application and this table must have a primary key (normally a +id+ int
# autonumber column) and a +name+ varchar column. You must also define a model class
# related to this table (by default, named +Tag+).
#
# All tag names will be stored in this tags table. Taggable objects should reside
# in their own tables, like any other object. Tagging objects is perfomed by
# the +acts_as_taggable+ mixin using a +has_and_belong_to_many+ relationship that is
# automatically created on the taggable class, and as so, a join table must exist
# between the tags table and the taggable object table.
#
# The name of the join table, by default, always follow the form
# '[tags_table_name]_[taggable_object_table_name]' even if the taggable object
# table name precedes the tags table name alphabetically (for example, tags_photos).
# This is different from the regular +has_and_belongs_to_many+ convention and
# allows all your join tables to share a common prefix (which is the tags table name).
#
# The join table must be composed of the foreign keys from the tags table and the
# taggable object table, so for instance, if we have a tags table named +tags+ (related
# to a +Tag+ model) and a taggable +photos+ table (related to a +Photo+ model),
# there should be a join table +tags_photos+ with int FK columns +photo_id+ and +tag_id+.
# If you don<6F>t use a explicit full model related to the join table (thru the
# +:join_class_name+ option), you must not add a primary key to the join table.
#
# The +acts_as_taggable+ adds the instance methods +tag+, +tag_names+,
# +tag_names= +, +tag_names<< +, +tagged_with? + for adding tags to the object
# and also the class method +find_tagged_with+ method for search tagged objects.
#
# Examples:
#
# class Photo < ActiveRecord::Base
# # this creates a 'tags' collection, thru a has_and_belongs_to_many
# # relationship that utilizes the join table 'tags_photos'.
# acts_as_taggable
# end
#
# photo = Photo.new
#
# # splits and adds to the tags collection
# photo.tag "wine beer alcohol"
#
# # don't need to split since it's an array, but replaces the tags collection
# # trailing and leading spaces are properly removed
# photo.tag [ 'wine ', ' vodka'], :clear => true
#
# photo.tag_names # => [ 'wine', 'vodka' ]
#
# # appends new tags with a different separator
# # the 'wine' tag won<6F>t be duplicated
# photo.tag_names << 'wine, beer, alcohol', :separator => ','
#
# # The difference between +tag_names+ and +tags+ is that +tag_names+
# # holds an array of String objects, mapped from +tags+, while +tags+
# # holds the actual +has_and_belongs_to_many+ collection, and so, is
# # composed of +Tag+ objects.
# photo.tag_names.size # => 4
# photo.tags.size # => 4
#
# # Find photos with 'wine' OR 'whisky'
# Photo.find_tagged_with :any => [ 'wine', 'whisky' ]
#
# # Finds photos with 'wine' AND 'whisky' using a different separator.
# # This is also known as tag combos.
# Photo.find_tagged_with(:all => 'wine+whisky', :separator => '+'
#
# # Gets the top 10 tags for all photos
# Photo.tags_count :limit => 10 # => { 'beer' => 68, 'wine' => 37, 'vodka' => '22', ... }
#
# # Gets the tags count that are greater than 30
# Photo.tags_count :count => '> 30' # => { 'beer' => 68, 'wine' => 37 }
#
# You can also use full join models if you want to take advantage of
# ActiveRecord<72>s callbacks, timestamping, inheritance and other features
# on the join records as well. For that, you use the +:join_class_name+ option.
# In this case, the join table must have a primary key.
#
# class Person
# # This defines a class +TagPerson+ automagically.
# acts_as_taggable :join_class_name => 'TagPerson'
# end
#
# # We can open the +TagPerson+ class and add features to it.
# class TagPerson
# acts_as_list :scope => :person
# belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
# before_save :do_some_validation
# after_save :do_some_stats
# end
#
# # We can do some interesting things with it now
# person = Person.new
# person.tag "wine beer alcohol", :attributes => { :created_by_id => 1 }
# Person.find_tagged_with(:any => 'wine', :condition => "tags_people.created_by_id = 1 AND tags_people.position = 1")
module ClassMethods
# This method defines a +has_and_belongs_to_many+ relationship between
# the target class and the tag model class. It also adds several instance methods
# for tagging objects of the target class, as well as a class method for searching
# objects that contains specific tags.
#
# The options are:
#
# The +:collection+ parameter receives a symbol defining
# the name of the tag collection method and it defaults to +:tags+.
#
# The +:tag_class_name+ parameter receives the tag model class name and
# it defaults to +'Tag'+.
#
# THe +:join_class_name+ parameter receives the model class name that joins
# the tag model and the taggable model. This automagically defines the join model
# class that can be opened and extended.
#
# The remaining options are passed on to the +has_and_belongs_to_many+ declaration.
# The +:join_table+ parameter is defined by default using the form
# of '[tags_table_name]_[target_class_table_name]', example: +tags_photos+,
# which differs from the standard +has_and_belongs_to_many+ behavior.
def acts_as_taggable(options = {})
options = { :collection => :tags, :tag_class_name => 'Tag' }.merge(options)
collection_name = options[:collection]
tag_model = options[:tag_class_name].constantize
default_join_table = "#{tag_model.table_name}_#{self.table_name}"
options[:join_table] ||= default_join_table
options[:foreign_key] ||= self.name.to_s.foreign_key
options[:association_foreign_key] ||= tag_model.to_s.foreign_key
# not using a simple has_and_belongs_to_many but a full model
# for joining the tags table and the taggable object table
if join_class_name = options[:join_class_name]
Object.class_eval "class #{join_class_name} < ActiveRecord::Base; set_table_name '#{options[:join_table]}' end" unless Object.const_defined?(join_class_name)
join_model = join_class_name.constantize
tagged = self
join_model.class_eval do
belongs_to :tag, :class_name => tag_model.to_s, :foreign_key => Inflector.foreign_key(tag_model.name)
belongs_to :tagged, :class_name => tagged.name.to_s, :foreign_key => Inflector.foreign_key(tagged.name)
define_method(:name) { self['name'] ||= tag.name }
end
options[:class_name] ||= join_model.to_s
tag_pk, tag_fk = tag_model.primary_key, options[:association_foreign_key]
t, jt = tag_model.table_name, join_model.table_name
options[:finder_sql] ||= "SELECT #{jt}.*, #{t}.name AS name FROM #{jt}, #{t} WHERE #{jt}.#{tag_fk} = #{t}.#{tag_pk} AND #{jt}.#{options[:foreign_key]} = \#{quoted_id}"
else
join_model = nil
end
# set some class-wide attributes needed in class and instance methods
write_inheritable_attribute(:tag_foreign_key, options[:association_foreign_key])
write_inheritable_attribute(:taggable_foreign_key, options[:foreign_key])
write_inheritable_attribute(:tag_collection_name, collection_name)
write_inheritable_attribute(:tag_model, tag_model)
write_inheritable_attribute(:tags_join_model, join_model)
write_inheritable_attribute(:tags_join_table, options[:join_table])
write_inheritable_attribute(:tag_options, options)
[ :collection, :tag_class_name, :join_class_name ].each { |key| options.delete(key) } # remove these, we don't need it anymore
[ :join_table, :association_foreign_key ].each { |key| options.delete(key) } if join_model # don<6F>t need this for has_many
# now, finally add the proper relationships
class_eval do
include ActiveRecord::Acts::Taggable::InstanceMethods
extend ActiveRecord::Acts::Taggable::SingletonMethods
class_inheritable_reader :tag_collection_name, :tag_model, :tags_join_model,
:tags_options, :tags_join_table,
:tag_foreign_key, :taggable_foreign_key
if join_model
has_many collection_name, options
else
has_and_belongs_to_many collection_name, options
end
end
end
end
module SingletonMethods
# This method searches for objects of the taggable class and subclasses that
# contains specific tags associated to them. The tags to be searched for can
# be passed to the +:any+ or +:all+ options, either as a String or an Array.
#
# The options are:
#
# +:any+: searches objects that are related to ANY of the given tags
#
# +:all+: searcher objects that are related to ALL of the the given tags
#
# +:separator+: a string, regex or Proc object that will be used to split the
# tags string passed to +:any+ or +:all+ using a regular +String#split+ method.
# If a Proc is passed, the proc should split the string in any way it wants
# and return an array of strings.
#
# +:conditions+: any additional conditions that should be appended to the
# WHERE clause of the finder SQL. Just like regular +ActiveRecord::Base#find+ methods.
#
# +:order+: the same as used in regular +ActiveRecord::Base#find+ methods.
#
# +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
def find_tagged_with(options = {})
options = { :separator => ' ' }.merge(options)
tag_names = ActiveRecord::Acts::Taggable.split_tag_names(options[:any] || options[:all], options[:separator])
raise "No tags were passed to :any or :all options" if tag_names.empty?
o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
sql = "SELECT DISTINCT #{o}.* FROM #{o}
WHERE (SELECT COUNT(#{jt}.#{o_fk}) FROM #{jt}, #{t}
WHERE #{jt}.#{t_fk} = #{t}.#{t_pk}
AND #{jt}.#{o_fk} = #{o}.#{o_pk}
AND (#{t}.name = #{tag_names.collect { |name| quote_value(name) }.join(" OR #{t}.name = ")}))"
sql << (options[:all] ? " = #{tag_names.length}" : " > 0")
sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options)
find_by_sql(sql)
end
# This method counts the number of times the tags have been applied to your objects
# and, by default, returns a hash in the form of { 'tag_name' => count, ... }
#
# The options are:
#
# +:raw+: If you just want to get the raw output of the SQL statement (Array of Hashes), instead of the regular tags count Hash, set this to +true+.
#
# +:conditions+: any additional conditions that should be appended to the
# WHERE clause of the SQL. Just like in regular +ActiveRecord::Base#find+ methods.
#
# +:order+: The same as used in +ActiveRecord::Base#find+ methods. By default, this is 'count DESC'.
#
# +:count+: Adds a HAVING clause to the SQL statement, where you can set conditions for the 'count' column. For example: '> 50'
#
# +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
def tags_count(options = {})
options = {:order => 'count DESC'}.merge(options)
o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
sql = "SELECT #{t}.#{t_pk} AS id, #{t}.name AS name, COUNT(*) AS count FROM #{jt}, #{o}, #{t} WHERE #{jt}.#{t_fk} = #{t}.#{t_pk}
AND #{jt}.#{o_fk} = #{o}.#{o_pk}"
sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
sql << " GROUP BY #{t}.name, #{t}.#{t_pk}"
sql << " HAVING COUNT(*) #{options[:count]} " if options[:count]
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options)
result = connection.select_all(sql)
count = result.inject({}) { |hsh, row| hsh[row['name']] = row['count'].to_i; hsh } unless options[:raw]
count || result
end
# Alias for +tags_count+
alias_method :tag_count, :tags_count
def count_tagged_with(options = {})
options = { :separator => ' ' }.merge(options)
tag_names = ActiveRecord::Acts::Taggable.split_tag_names(options[:any] || options[:all], options[:separator])
raise "No tags were passed to :any or :all options" if tag_names.empty?
o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
sql = "SELECT COUNT(*) count_all FROM #{jt}, #{o}, #{t} WHERE #{jt}.#{t_fk} = #{t}.#{t_pk}"
sql << " AND ("
sql << tag_names.collect {|tag| sanitize_sql( ["#{t}.name = ?",tag])}.join(" OR ")
sql << ")"
sql << " AND #{o}.#{o_pk} = #{jt}.#{o_fk}"
sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
result = connection.select_all(sql)
result[0]["count_all"].to_i
end
# Finds other records that share the most tags with the record passed
# as the +related+ parameter. Useful for constructing 'Related' or
# 'See Also' boxes and lists.
#
# The options are:
#
# +:limit+: defaults to 5, which means the method will return the top 5 records
# that share the greatest number of tags with the passed one.
def find_related_tagged(related, options = {})
related_id = related.is_a?(self) ? related.id : related
options = { :limit => 5 }.merge(options)
o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
sql = "SELECT o.*,
(SELECT COUNT(jt2.#{o_fk}) FROM #{jt} jt, #{t} t, #{jt} jt2
WHERE jt.#{o_fk} = #{related_id} AND t.#{t_pk} = jt.#{t_fk}
AND jt2.#{o_fk} != jt.#{o_fk}
AND jt2.#{t_fk} = jt.#{t_fk} AND o.#{o_pk} = jt2.#{o_fk}) AS count
FROM #{o} o
ORDER BY count DESC, o.#{o_pk} ASC"
add_limit!(sql, options)
find_by_sql(sql)
end
# Finds other tags that are related to the tags passed thru the +tags+
# parameter, by finding common records that share similar sets of tags.
# Useful for constructing 'Related tags' lists.
#
# The options are:
#
# +:separator+ => defines the separator (String or Regex) used to split
# the tags parameter and defaults to ' ' (space and line breaks).
#
# +:raw+: If you just want to get the raw output of the SQL statement (Array of Hashes), instead of the regular tags count Hash, set this to +true+.
#
# +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
def find_related_tags(tags, options = {})
tag_names = ActiveRecord::Acts::Taggable.split_tag_names(tags, options[:separator])
o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
sql = "SELECT jt.#{o_fk} AS o_id FROM #{jt} jt, #{t} t
WHERE jt.#{t_fk} = t.#{t_pk}"
sql << " AND (t.name IN ( "
sql << quote_bound_value(tag_names)
sql << ")) "
sql << "GROUP BY jt.#{o_fk}
HAVING COUNT(jt.#{o_fk}) = #{tag_names.length}"
o_ids = connection.select_all(sql).map { |row| row['o_id'] }
return options[:raw] ? [] : {} if o_ids.length < 1
sql = "SELECT t.#{t_pk} AS id, t.name AS name, COUNT(jt.#{o_fk}) AS count FROM #{jt} jt, #{t} t
WHERE jt.#{o_fk} IN (#{o_ids.join(",")})
AND t.#{t_pk} = jt.#{t_fk}
GROUP BY t.#{t_pk}, t.name, jt.#{t_fk}
ORDER BY count DESC"
add_limit!(sql, options)
result = connection.select_all(sql).delete_if { |row| tag_names.include?(row['name']) }
count = result.inject({}) { |hsh, row| hsh[row['name']] = row['count'].to_i; hsh } unless options[:raw]
count || result
end
private
def set_locals_for_sql
[ table_name, primary_key, taggable_foreign_key,
tag_model.table_name, tag_model.primary_key, tag_foreign_key,
tags_join_model ? tags_join_model.table_name : tags_join_table ]
end
end
module InstanceMethods
# This method applies tags to the target object, by parsing the tags parameter
# into Tag object instances and adding them to the tag collection of the object.
# If the tag name already exists in the tags table, it just adds a relationship
# to the existing tag record. If it doesn't exist, it then creates a new
# Tag record for it.
#
# The +tags+ parameter can be a +String+, +Array+ or a +Proc+ object.
# If it's a +String+, it's splitted using the +:separator+ specified in
# the +options+ hash. If it's an +Array+ it is flattened and compacted.
# Duplicate entries will be removed as well. Tag names are also stripped
# of trailing and leading whitespaces. If a Proc is passed,
# the proc should split the string in any way it wants and return an array of strings.
#
# The +options+ hash has the following parameters:
#
# +:separator+ => defines the separator (String or Regex) used to split
# the tags parameter and defaults to ' ' (space and line breaks).
#
# +:clear+ => defines whether the existing tag collection will be cleared before
# applying the new +tags+ passed. Defaults to +false+.
def tag(tags, options = {})
options = { :separator => ' ', :clear => false }.merge(options)
attributes = options[:attributes] || {}
# parse the tags parameter
tag_names = ActiveRecord::Acts::Taggable.split_tag_names(tags, options[:separator])
# clear the collection if appropriate
tag_collection.clear if options[:clear]
# append the tag names to the collection
tag_names.each do |name|
# ensure that tag names don't get duplicated
tag_record = tag_model.find_by_name(name) || tag_model.new(:name => name)
if tags_join_model
tag_join_record = tags_join_model.new(attributes)
tag_join_record.tag = tag_record
tag_join_record.tagged = self
tag_collection << tag_join_record unless tagged_with?(name)
else
unless tagged_with?(name)
tag_record.attributes = attributes
tag_collection << tag_record
end
# tag_collection.push_with_attributes(tag_record, attributes) unless tagged_with?(name)
end
end
end
# Clears the current tags collection and sets the tag names for this object.
# Equivalent of calling #tag(..., :clear => true)
#
# Another way of appending tags to a existing tags collection is by using
# the +<<+ or +concat+ method on +tag_names+, which is equivalent of calling
# #tag(..., :clear => false).
def tag_names=(tags, options = {})
tag(tags, options.merge(:clear => true))
end
# Returns an array of strings containing the tags applied to this object.
# If +reload+ is +true+, the tags collection is reloaded.
def tag_names(reload = false)
ary = tag_collection(reload).map { |tag| tag.name }
ary.extend(TagNamesMixin)
ary.set_tag_container(self)
ary
end
# Checks to see if this object has been tagged with +tag_name+.
# If +reload+ is true, reloads the tag collection before doing the check.
def tagged_with?(tag_name, reload = false)
tag_names(reload).include?(tag_name)
end
# Calls +find_related_tagged+ passing +self+ as the +related+ parameter.
def tagged_related(options = {})
self.class.find_related_tagged(self.id, options)
end
private
def tag_model
self.class.tag_model
end
def tag_collection(reload = false)
send(self.class.tag_collection_name, reload)
end
def tags_join_model
self.class.tags_join_model
end
end
module TagNamesMixin #:nodoc:
def set_tag_container(tag_container)
@tag_container = tag_container
end
def <<(tags, options = {})
@tag_container.tag(tags, options.merge(:clear => false))
end
alias_method :concat, :<<
end
end
end
end
ActiveRecord::Base.class_eval do
include ActiveRecord::Acts::Taggable
end

View File

@ -0,0 +1,39 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_support/binding_of_caller'
require 'active_support/breakpoint'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'mysql'])
ActiveRecord::Base.connection.drop_table :topics rescue nil
ActiveRecord::Base.connection.drop_table :companies rescue nil
ActiveRecord::Base.connection.drop_table :posts rescue nil
ActiveRecord::Base.connection.create_table :topics do |t|
t.column :title, :string
end
ActiveRecord::Base.connection.create_table :companies do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :posts do |t|
t.column :title, :string
end
class Test::Unit::TestCase #:nodoc:
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
self.use_instantiated_fixtures = false
self.use_transactional_fixtures = (ENV['AR_NO_TX_FIXTURES'] != "yes")
def create_fixtures(*table_names, &block)
Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names, {}, &block)
end
end

View File

@ -0,0 +1,408 @@
# AR_PATH = 'c:/dev/rails/activerecord' || ARGS[0]
# $:.unshift("#{AR_PATH}/test")
# $:.unshift("#{AR_PATH}/test/connections/native_mysql")
# $:.unshift(File.dirname(__FILE__) + '/../lib')
require 'abstract_unit'
require 'taggable'
ActiveRecord::Base.connection.drop_table :tags rescue nil
ActiveRecord::Base.connection.drop_table :tags_topics rescue nil
ActiveRecord::Base.connection.drop_table :keywords rescue nil
ActiveRecord::Base.connection.drop_table :keywords_companies rescue nil
ActiveRecord::Base.connection.drop_table :tags_posts rescue nil
ActiveRecord::Base.connection.create_table :tags do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :tags_topics, :id => false do |t|
t.column :tag_id, :int
t.column :topic_id, :int
t.column :created_at, :time
end
ActiveRecord::Base.connection.create_table :keywords do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :keywords_companies, :id => false do |t|
t.column :keyword_id, :int
t.column :company_id, :int
end
ActiveRecord::Base.connection.create_table :tags_posts do |t|
t.column :tag_id, :int
t.column :post_id, :int
t.column :created_at, :time
t.column :created_by_id, :int
t.column :position, :int
end
class Tag < ActiveRecord::Base; end
class Topic < ActiveRecord::Base
acts_as_taggable
end
class Keyword < ActiveRecord::Base; end
class Company < ActiveRecord::Base
acts_as_taggable :collection => :keywords, :tag_class_name => 'Keyword'
def tag_model
Keyword
end
end
class Firm < Company; end
class Client < Company; end
class Post < ActiveRecord::Base
acts_as_taggable :join_class_name => 'TagPost'
end
class TagPost
acts_as_list :scope => :post
def before_save
self.created_by_id = rand(3) + 1
end
end
class Order < ActiveRecord::Base
end
class ActAsTaggableTest < Test::Unit::TestCase
# fixtures :topics, :companies, :posts
def setup
Tag.delete_all
Keyword.delete_all
ActiveRecord::Base.connection.execute 'delete from tags_topics'
ActiveRecord::Base.connection.execute 'delete from keywords_companies'
ActiveRecord::Base.connection.execute 'delete from tags_posts'
create_fixtures 'posts'
create_fixtures 'companies'
create_fixtures 'topics'
end
def test_singleton_methods
assert !Order.respond_to?(:find_tagged_with)
assert Firm.respond_to?(:find_tagged_with)
assert Post.respond_to?(:find_tagged_with)
assert Topic.respond_to?(:find_tagged_with)
assert Topic.respond_to?(:tag_count)
assert Topic.respond_to?(:tags_count)
end
def test_with_defaults
test_tagging(Topic.find(:first), Tag, :tags)
end
def test_with_non_defaults
test_tagging(Company.find(:first), Keyword, :keywords)
end
def test_tag_with_new_object
topic = Topic.new
topic.tag 'brazil rio beach'
topic.save
end
def test_tagging_with_join_model
Tag.delete_all
TagPost.delete_all
post = Post.find(:first)
tags = %w(brazil rio beach)
post.tag(tags)
tags.each { |tag| assert post.tagged_with?(tag) }
post.save
post.tags.reload
tags.each { |tag| assert post.tagged_with?(tag) }
posts = Post.find_tagged_with(:any => 'brazil sampa moutain')
assert_equal posts[0], post
posts = Post.find_tagged_with(:all => 'brazil beach')
assert_equal posts[0], post
posts = Post.find_tagged_with(:all => 'brazil rich')
assert_equal 0, posts.size
posts = Post.find_tagged_with(:all => 'brazil', :conditions => [ 'tags_posts.position = ?', 1])
assert_equal posts[0], post
posts = Post.find_tagged_with(:all => 'rio', :conditions => [ 'tags_posts.position = ?', 2])
assert_equal posts[0], post
posts = Post.find_tagged_with(:all => 'beach', :conditions => [ 'tags_posts.position = ?', 3])
assert_equal posts[0], post
end
def test_tags_count_with_join_model
p1 = Post.create(:title => 'test1')
p2 = Post.create(:title => 'test2')
p3 = Post.create(:title => 'test3')
p1.tag 'a b c d'
p2.tag 'a c e f'
p3.tag 'a c f g'
counts = Post.tags_count :count => '>= 2', :limit => 2
assert_equal counts.keys.size, 2
counts.each { |tag, count| assert count >= 2 }
assert counts.keys.include?('a')
assert counts.keys.include?('c')
end
def test_tags_count
t1 = Topic.create(:title => 'test1')
t2 = Topic.create(:title => 'test2')
t3 = Topic.create(:title => 'test3')
t1.tag 'a b c d'
t2.tag 'a c e f'
t3.tag 'a c f g'
count = Topic.tags_count
assert_equal 3, count['a']
assert_equal 1, count['b']
assert_equal 3, count['c']
assert_equal 1, count['d']
assert_equal 1, count['e']
assert_equal 2, count['f']
assert_equal 1, count['g']
assert_equal nil, count['h']
count = Topic.tags_count :count => '>= 2'
assert_equal 3, count['a']
assert_equal nil, count['b']
assert_equal 3, count['c']
assert_equal nil, count['d']
assert_equal nil, count['e']
assert_equal 2, count['f']
assert_equal nil, count['g']
assert_equal nil, count['h']
t4 = Topic.create(:title => 'test4')
t4.tag 'a f'
count = Topic.tags_count :limit => 3
assert_equal 4, count['a']
assert_equal nil, count['b']
assert_equal 3, count['c']
assert_equal nil, count['d']
assert_equal nil, count['e']
assert_equal 3, count['f']
assert_equal nil, count['g']
assert_equal nil, count['h']
raw = Topic.tags_count :raw => true
assert_equal 7, raw.size
assert_equal Array, raw.class
assert_equal 'a', raw.first['name']
assert_equal '4', raw.first['count']
assert_not_nil raw.first['id']
assert_equal 'g', raw.last['name']
assert_equal '1', raw.last['count']
assert_not_nil raw.last['id']
end
def test_find_related_tagged
t1, t2, t3, t4, t5, t6 = create_test_topics
assert_equal [ t4, t2, t3 ], t1.tagged_related(:limit => 3)
assert_equal [ t5, t1, t3 ], t2.tagged_related(:limit => 3)
assert_equal [ t1, t4, t6 ], t3.tagged_related(:limit => 3)
assert_equal [ t1, t3, t6 ], t4.tagged_related(:limit => 3)
assert_equal [ t2, t1, t3 ], t5.tagged_related(:limit => 3)
assert_equal [ t1, t3, t4 ], t6.tagged_related(:limit => 3)
end
def test_find_related_tags
t1, t2, t3, t4, t5, t6 = create_test_topics
tags = Topic.find_related_tags('rome walking')
assert_equal 1, tags['greatview']
assert_equal 2, tags['clean']
assert_equal 1, tags['mustsee']
# originaly was:
#
# assert_equal 2, tags['greatview']
# assert_equal 4, tags['clean']
# assert_equal 2, tags['mustsee']
end
def test_find_tagged_with_on_subclasses
firm = Firm.find(:first)
firm.tag 'law'
firms = Firm.find_tagged_with :any => 'law'
assert_equal firm, firms[0]
assert_equal 1, firms.size
end
def test_find_tagged_with_any
topic1 = Topic.create(:title => 'test1')
topic2 = Topic.create(:title => 'test2')
topic3 = Topic.create(:title => 'test3')
topic1.tag('a b c'); topic1.save
topic2.tag('a c e'); topic2.save
topic3.tag('c d e'); topic3.save
topics = Topic.find_tagged_with(:any => 'x y z')
assert_equal 0, topics.size
topics = Topic.find_tagged_with(:any => 'a b c d e x y z')
assert_equal 3, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
assert topics.include?(topic3)
topics = Topic.find_tagged_with(:any => 'a z')
assert_equal 2, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
topics = Topic.find_tagged_with(:any => 'b')
assert_equal 1, topics.size
assert topics.include?(topic1)
topics = Topic.find_tagged_with(:any => 'c')
assert_equal 3, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
assert topics.include?(topic3)
topics = Topic.find_tagged_with(:any => 'd')
assert_equal 1, topics.size
assert topics.include?(topic3)
topics = Topic.find_tagged_with(:any => 'e')
assert_equal 2, topics.size
assert topics.include?(topic2)
assert topics.include?(topic3)
end
def test_find_tagged_with_all
topic1 = Topic.create(:title => 'test1')
topic2 = Topic.create(:title => 'test2')
topic3 = Topic.create(:title => 'test3')
topic1.tag('a b c'); topic1.save
topic2.tag('a c e'); topic2.save
topic3.tag('c d e'); topic3.save
topics = Topic.find_tagged_with(:all => 'a b d')
assert_equal 0, topics.size
topics = Topic.find_tagged_with(:all => 'a c')
assert_equal 2, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
topics = Topic.find_tagged_with(:all => 'a+c', :separator => '+')
assert_equal 2, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
topics = Topic.find_tagged_with(:all => 'c e')
assert_equal 2, topics.size
assert topics.include?(topic2)
assert topics.include?(topic3)
topics = Topic.find_tagged_with(:all => 'c')
assert_equal 3, topics.size
assert topics.include?(topic1)
assert topics.include?(topic2)
assert topics.include?(topic3)
topics = Topic.find_tagged_with(:all => 'a b c')
assert_equal 1, topics.size
assert topics.include?(topic1)
topics = Topic.find_tagged_with(:all => 'a c e')
assert_equal 1, topics.size
assert topics.include?(topic2)
end
private
def test_tagging(tagged_object, tag_model, collection)
tag_model.delete_all
assert_equal 0, tag_model.count
tagged_object.tag_names << 'rio brazil'
tagged_object.save
assert_equal 2, tag_model.count
assert_equal 2, tagged_object.send(collection).size
tagged_object.tag_names = 'beach surf'
assert_equal 4, tag_model.count
assert_equal 2, tagged_object.send(collection).size
tagged_object.tag_names.concat 'soccer+pele', :separator => '+'
assert_equal 6, tag_model.count
assert_equal 4, tagged_object.send(collection).size
tag_model.delete_all
assert_equal 0, tag_model.count
tagged_object.send(collection).reload
tagged_object.tag_names = 'dhh'
assert_equal 1, tag_model.count
assert_equal 1, tagged_object.send(collection).size
tagged_object.tag 'dhh rails my', :clear => true
assert_equal 3, tag_model.count
assert_equal 3, tagged_object.send(collection).size
tagged_object.tag 'dhh dhh ruby tags', :clear => true
assert_equal 5, tag_model.count
assert_equal 3, tagged_object.send(collection).size
tagged_object.tag 'tagging, hello, ruby', :separator => ','
assert_equal 7, tag_model.count
assert_equal 5, tagged_object.send(collection).size
all_tags = %w( dhh rails my ruby tags tagging hello )
first_tags = %w( dhh ruby tags tagging hello )
tagged_object.send(collection).reload
assert_equal first_tags, tagged_object.tag_names
all_tags.each do |tag_name|
tag_record = tag_model.find_by_name(tag_name)
assert_not_nil tag_record
if first_tags.include?(tag_name)
assert tagged_object.send(collection).include?(tag_record)
assert tagged_object.tagged_with?(tag_name)
end
end
end
def create_test_topics
t1 = Topic.create(:title => 't1')
t2 = Topic.create(:title => 't2')
t3 = Topic.create(:title => 't3')
t4 = Topic.create(:title => 't4')
t5 = Topic.create(:title => 't5')
t6 = Topic.create(:title => 't6')
t1.tag('rome, luxury, clean, mustsee, greatview', :separator => ','); t1.save
t2.tag('rome, luxury, clean, italian, spicy, goodwine', :separator => ','); t2.save
t3.tag('rome, walking, clean, mustsee', :separator => ','); t3.save
t4.tag('rome, italy, clean, mustsee, greatview', :separator => ','); t4.save
t5.tag('rome, luxury, clean, italian, spicy, wine', :separator => ','); t5.save
t6.tag('rome, walking, clean, greatview', :separator => ','); t6.save
[ t1, t2, t3, t4, t5, t6 ]
end
end

View File

@ -0,0 +1,13 @@
mysql:
:adapter: mysql
:host: localhost
:username: root
:password:
:database: taggable
postgres:
:adapter: postgresql
:host: 192.168.0.24
:username: taggable
:password:
:database: taggable

View File

@ -0,0 +1,5 @@
#
#
company:
id: 1
name: blah

View File

@ -0,0 +1,5 @@
#
#
post:
id: 1
title: blah

View File

@ -0,0 +1,6 @@
#
#
topic:
id: 1
title: blah