rotuka-taggable 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +167 -0
- data/MIT-LICENSE +20 -0
- data/README +146 -0
- data/Rakefile +22 -0
- data/generators/tags/USAGE +1 -0
- data/generators/tags/tags_generator.rb +14 -0
- data/generators/tags/templates/migration.rb +26 -0
- data/generators/tags/templates/tag.rb +26 -0
- data/generators/tags/templates/tagging.rb +12 -0
- data/generators/tags/templates/tags_helper.rb +13 -0
- data/init.rb +1 -0
- data/lib/tag_list.rb +91 -0
- data/lib/taggable.rb +215 -0
- data/test/abstract_unit.rb +97 -0
- data/test/acts_as_taggable_test.rb +347 -0
- data/test/database.yml +10 -0
- data/test/fixtures/magazine.rb +3 -0
- data/test/fixtures/magazines.yml +7 -0
- data/test/fixtures/photo.rb +8 -0
- data/test/fixtures/photos.yml +24 -0
- data/test/fixtures/post.rb +7 -0
- data/test/fixtures/posts.yml +34 -0
- data/test/fixtures/special_post.rb +2 -0
- data/test/fixtures/subscription.rb +4 -0
- data/test/fixtures/subscriptions.yml +3 -0
- data/test/fixtures/taggings.yml +149 -0
- data/test/fixtures/tags.yml +19 -0
- data/test/fixtures/user.rb +7 -0
- data/test/fixtures/users.yml +7 -0
- data/test/schema.rb +37 -0
- data/test/tag_list_test.rb +106 -0
- data/test/tag_test.rb +34 -0
- data/test/tagging_test.rb +13 -0
- data/test/tags_helper_test.rb +28 -0
- metadata +87 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module TagsHelper
|
2
|
+
# See the README for an example using tag_cloud.
|
3
|
+
def tag_cloud(tags, classes)
|
4
|
+
return if tags.empty?
|
5
|
+
|
6
|
+
max_count = tags.sort_by(&:count).last.count.to_f
|
7
|
+
|
8
|
+
tags.each do |tag|
|
9
|
+
index = ((tag.count / max_count) * (classes.size - 1)).round
|
10
|
+
yield tag, classes[index]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/lib/taggable'
|
data/lib/tag_list.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
class TagList < Array
|
2
|
+
cattr_accessor :delimiter
|
3
|
+
self.delimiter = ','
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
add(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
10
|
+
#
|
11
|
+
# tag_list.add("Fun", "Happy")
|
12
|
+
#
|
13
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
14
|
+
#
|
15
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
16
|
+
def add(*names)
|
17
|
+
extract_and_apply_options!(names)
|
18
|
+
concat(names)
|
19
|
+
clean!
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Remove specific tags from the tag_list.
|
24
|
+
#
|
25
|
+
# tag_list.remove("Sad", "Lonely")
|
26
|
+
#
|
27
|
+
# Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
|
28
|
+
#
|
29
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
30
|
+
def remove(*names)
|
31
|
+
extract_and_apply_options!(names)
|
32
|
+
delete_if { |name| names.include?(name) }
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Transform the tag_list into a tag string suitable for edting in a form.
|
37
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
38
|
+
#
|
39
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
40
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
41
|
+
def to_s
|
42
|
+
clean!
|
43
|
+
|
44
|
+
list = map do |name|
|
45
|
+
if delimiter.is_a?(Regexp)
|
46
|
+
name.match(delimiter) ? "\"#{name}\"" : name
|
47
|
+
else
|
48
|
+
name.include?(delimiter) ? "\"#{name}\"" : name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
list.join( delimiter.is_a?(Regexp) ? "#{delimiter.source.match(/[^\\\[\]\*\?\{\}\.\|]/)[0]} " : (delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ") )
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
# Remove whitespace, duplicates, and blanks.
|
57
|
+
def clean!
|
58
|
+
reject!(&:blank?)
|
59
|
+
map!(&:strip)
|
60
|
+
uniq!
|
61
|
+
end
|
62
|
+
|
63
|
+
def extract_and_apply_options!(args)
|
64
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
65
|
+
options.assert_valid_keys :parse
|
66
|
+
|
67
|
+
if options[:parse]
|
68
|
+
args.map! { |a| self.class.from(a) }
|
69
|
+
end
|
70
|
+
|
71
|
+
args.flatten!
|
72
|
+
end
|
73
|
+
|
74
|
+
class << self
|
75
|
+
# Returns a new TagList using the given tag string.
|
76
|
+
#
|
77
|
+
# tag_list = TagList.from("One , Two, Three")
|
78
|
+
# tag_list # ["One", "Two", "Three"]
|
79
|
+
def from(string)
|
80
|
+
returning new do |tag_list|
|
81
|
+
string = string.to_s.gsub('.', '').dup
|
82
|
+
|
83
|
+
# Parse the quoted tags
|
84
|
+
string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
85
|
+
string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
86
|
+
|
87
|
+
tag_list.add(string.split(delimiter))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/taggable.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module Taggable #:nodoc:
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Macro that adds tags and taggings for object
|
10
|
+
def acts_as_taggable
|
11
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
12
|
+
has_many :tags, :through => :taggings
|
13
|
+
|
14
|
+
before_save :save_cached_tag_list
|
15
|
+
after_save :save_tags
|
16
|
+
|
17
|
+
include ActiveRecord::Acts::Taggable::InstanceMethods
|
18
|
+
extend ActiveRecord::Acts::Taggable::SingletonMethods
|
19
|
+
|
20
|
+
alias_method_chain :reload, :tag_list
|
21
|
+
end
|
22
|
+
|
23
|
+
def cached_tag_list_column_name
|
24
|
+
"cached_tag_list"
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_cached_tag_list_column_name(value = nil, &block)
|
28
|
+
define_attr_method :cached_tag_list_column_name, value, &block
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module SingletonMethods
|
33
|
+
# Pass either a tag string, or an array of strings or tags
|
34
|
+
#
|
35
|
+
# Options:
|
36
|
+
# :exclude - Find models that are not tagged with the given tags
|
37
|
+
# :match_all - Find models that match all of the given tags, not just one
|
38
|
+
# :conditions - A piece of SQL conditions to add to the query
|
39
|
+
def find_tagged_with(*args)
|
40
|
+
options = find_options_for_find_tagged_with(*args)
|
41
|
+
options.blank? ? [] : find(:all, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
# will_paginate's method_missing function wants to hit
|
45
|
+
# find_all_tagged_with if you call paginate_tagged_with, which is
|
46
|
+
# obviously suboptimal
|
47
|
+
def find_all_tagged_with(*args)
|
48
|
+
find_tagged_with(*args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_options_for_find_tagged_with(tags, options = {})
|
52
|
+
tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
|
53
|
+
options = options.dup
|
54
|
+
|
55
|
+
return {} if tags.empty?
|
56
|
+
|
57
|
+
conditions = []
|
58
|
+
conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
|
59
|
+
|
60
|
+
taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
|
61
|
+
|
62
|
+
if options.delete(:exclude)
|
63
|
+
conditions << <<-END
|
64
|
+
#{table_name}.id NOT IN
|
65
|
+
(SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name}
|
66
|
+
INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id
|
67
|
+
WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})
|
68
|
+
END
|
69
|
+
else
|
70
|
+
if options.delete(:match_all)
|
71
|
+
conditions << <<-END
|
72
|
+
(SELECT COUNT(*) FROM #{Tagging.table_name}
|
73
|
+
INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id
|
74
|
+
WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)} AND
|
75
|
+
taggable_id = #{table_name}.id AND
|
76
|
+
#{tags_condition(tags)}) = #{tags.size}
|
77
|
+
END
|
78
|
+
else
|
79
|
+
conditions << tags_condition(tags, tags_alias)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
{ :select => "DISTINCT #{table_name}.*",
|
84
|
+
:joins => "INNER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
|
85
|
+
"INNER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
|
86
|
+
:conditions => conditions.join(" AND ")
|
87
|
+
}.reverse_merge!(options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Calculate the tag counts for all tags.
|
91
|
+
#
|
92
|
+
# Options:
|
93
|
+
# :start_at - Restrict the tags to those created after a certain time
|
94
|
+
# :end_at - Restrict the tags to those created before a certain time
|
95
|
+
# :conditions - A piece of SQL conditions to add to the query
|
96
|
+
# :limit - The maximum number of tags to return
|
97
|
+
# :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
98
|
+
# :at_least - Exclude tags with a frequency less than the given value
|
99
|
+
# :at_most - Exclude tags with a frequency greater than the given value
|
100
|
+
def tag_counts(options = {})
|
101
|
+
Tag.find(:all, find_options_for_tag_counts(options))
|
102
|
+
end
|
103
|
+
|
104
|
+
# Find how many objects are tagged with a certain tag.
|
105
|
+
def count_by_tag(tag_name)
|
106
|
+
counts = tag_counts(:conditions => "tags.name = #{quote_value(tag_name)}")
|
107
|
+
counts[0].respond_to?(:count) ? counts[0].count : 0
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_options_for_tag_counts(options = {})
|
111
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit
|
112
|
+
options = options.dup
|
113
|
+
|
114
|
+
scope = scope(:find)
|
115
|
+
start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
116
|
+
end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
117
|
+
|
118
|
+
conditions = [
|
119
|
+
"#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}",
|
120
|
+
sanitize_sql(options.delete(:conditions)),
|
121
|
+
scope && scope[:conditions],
|
122
|
+
start_at,
|
123
|
+
end_at
|
124
|
+
]
|
125
|
+
|
126
|
+
conditions << type_condition unless descends_from_active_record?
|
127
|
+
conditions.compact!
|
128
|
+
conditions = conditions.join(' AND ')
|
129
|
+
|
130
|
+
joins = ["INNER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
131
|
+
joins << "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
132
|
+
joins << scope[:joins] if scope && scope[:joins]
|
133
|
+
|
134
|
+
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
135
|
+
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
136
|
+
having = [at_least, at_most].compact.join(' AND ')
|
137
|
+
group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
|
138
|
+
group_by << " AND #{having}" unless having.blank?
|
139
|
+
|
140
|
+
{ :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
|
141
|
+
:joins => joins.join(" "),
|
142
|
+
:conditions => conditions,
|
143
|
+
:group => group_by
|
144
|
+
}.reverse_merge!(options)
|
145
|
+
end
|
146
|
+
|
147
|
+
def caching_tag_list?
|
148
|
+
column_names.include?(cached_tag_list_column_name)
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
def tags_condition(tags, table_name = Tag.table_name)
|
153
|
+
condition = tags.map { |t| sanitize_sql(["#{table_name}.name LIKE ?", t]) }.join(" OR ")
|
154
|
+
"(" + condition + ")"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
module InstanceMethods
|
159
|
+
def tag_list
|
160
|
+
return @tag_list if @tag_list
|
161
|
+
|
162
|
+
if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
|
163
|
+
@tag_list = TagList.from(cached_value)
|
164
|
+
else
|
165
|
+
@tag_list = TagList.new(*tags.map(&:name))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def tag_list=(value)
|
170
|
+
@tag_list = TagList.from(value)
|
171
|
+
end
|
172
|
+
|
173
|
+
def save_cached_tag_list
|
174
|
+
if self.class.caching_tag_list?
|
175
|
+
self[self.class.cached_tag_list_column_name] = tag_list.to_s
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def save_tags
|
180
|
+
return unless @tag_list
|
181
|
+
|
182
|
+
new_tag_names = @tag_list - tags.map(&:name)
|
183
|
+
old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
|
184
|
+
|
185
|
+
self.class.transaction do
|
186
|
+
if old_tags.any?
|
187
|
+
taggings.find(:all, :conditions => ["tag_id IN (?)", old_tags.map(&:id)]).each(&:destroy)
|
188
|
+
taggings.reset
|
189
|
+
end
|
190
|
+
|
191
|
+
new_tag_names.each do |new_tag_name|
|
192
|
+
tags << Tag.find_or_create_with_like_by_name(new_tag_name)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
true
|
197
|
+
end
|
198
|
+
|
199
|
+
# Calculate the tag counts for the tags used by this model.
|
200
|
+
#
|
201
|
+
# The possible options are the same as the tag_counts class method, excluding :conditions.
|
202
|
+
def tag_counts(options = {})
|
203
|
+
self.class.tag_counts({ :conditions => self.class.send(:tags_condition, tag_list) }.reverse_merge!(options))
|
204
|
+
end
|
205
|
+
|
206
|
+
def reload_with_tag_list(*args) #:nodoc:
|
207
|
+
@tag_list = nil
|
208
|
+
reload_without_tag_list(*args)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require File.dirname(__FILE__) + '/../../../../config/environment'
|
5
|
+
rescue LoadError
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'activerecord'
|
8
|
+
gem 'actionpack'
|
9
|
+
require 'active_record'
|
10
|
+
require 'action_controller'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Search for fixtures first
|
14
|
+
fixture_path = File.dirname(__FILE__) + '/fixtures/'
|
15
|
+
Dependencies.load_paths.insert(0, fixture_path)
|
16
|
+
|
17
|
+
require 'active_record/fixtures'
|
18
|
+
|
19
|
+
require File.dirname(__FILE__) + '/../lib/acts_as_taggable'
|
20
|
+
require_dependency File.dirname(__FILE__) + '/../lib/tag_list'
|
21
|
+
require_dependency File.dirname(__FILE__) + '/../lib/tags_helper'
|
22
|
+
|
23
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
|
24
|
+
ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
25
|
+
ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql')
|
26
|
+
|
27
|
+
load(File.dirname(__FILE__) + '/schema.rb')
|
28
|
+
|
29
|
+
Test::Unit::TestCase.fixture_path = fixture_path
|
30
|
+
|
31
|
+
class Test::Unit::TestCase #:nodoc:
|
32
|
+
self.use_transactional_fixtures = true
|
33
|
+
self.use_instantiated_fixtures = false
|
34
|
+
|
35
|
+
def assert_equivalent(expected, actual, message = nil)
|
36
|
+
if expected.first.is_a?(ActiveRecord::Base)
|
37
|
+
assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message
|
38
|
+
else
|
39
|
+
assert_equal expected.sort, actual.sort, message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def assert_tag_counts(tags, expected_values)
|
44
|
+
# Map the tag fixture names to real tag names
|
45
|
+
expected_values = expected_values.inject({}) do |hash, (tag, count)|
|
46
|
+
hash[tags(tag).name] = count
|
47
|
+
hash
|
48
|
+
end
|
49
|
+
|
50
|
+
tags.each do |tag|
|
51
|
+
value = expected_values.delete(tag.name)
|
52
|
+
|
53
|
+
assert_not_nil value, "Expected count for #{tag.name} was not provided"
|
54
|
+
assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}"
|
55
|
+
end
|
56
|
+
|
57
|
+
unless expected_values.empty?
|
58
|
+
assert false, "The following tag counts were not present: #{expected_values.inspect}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def assert_queries(num = 1)
|
63
|
+
$query_count = 0
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
|
67
|
+
end
|
68
|
+
|
69
|
+
def assert_no_queries(&block)
|
70
|
+
assert_queries(0, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# From Rails trunk
|
74
|
+
def assert_difference(expressions, difference = 1, message = nil, &block)
|
75
|
+
expression_evaluations = [expressions].flatten.collect{|expression| lambda { eval(expression, block.binding) } }
|
76
|
+
|
77
|
+
original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression.call }
|
78
|
+
yield
|
79
|
+
expression_evaluations.each_with_index do |expression, i|
|
80
|
+
assert_equal original_values[i] + difference, expression.call, message
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def assert_no_difference(expressions, message = nil, &block)
|
85
|
+
assert_difference expressions, 0, message, &block
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
ActiveRecord::Base.connection.class.class_eval do
|
90
|
+
def execute_with_counting(sql, name = nil, &block)
|
91
|
+
$query_count ||= 0
|
92
|
+
$query_count += 1
|
93
|
+
execute_without_counting(sql, name, &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
alias_method_chain :execute, :counting
|
97
|
+
end
|
@@ -0,0 +1,347 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/abstract_unit'
|
2
|
+
|
3
|
+
class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase
|
4
|
+
fixtures :tags, :taggings, :posts, :users, :photos, :subscriptions, :magazines
|
5
|
+
|
6
|
+
def test_find_tagged_with
|
7
|
+
assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"')
|
8
|
+
assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good'])
|
9
|
+
assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)])
|
10
|
+
|
11
|
+
assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature')
|
12
|
+
assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature'])
|
13
|
+
assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)])
|
14
|
+
|
15
|
+
assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad')
|
16
|
+
assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad'])
|
17
|
+
assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)])
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_find_tagged_with_nothing
|
21
|
+
assert_equal [], Post.find_tagged_with("")
|
22
|
+
assert_equal [], Post.find_tagged_with([])
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_find_tagged_with_nonexistant_tags
|
26
|
+
assert_equal [], Post.find_tagged_with('ABCDEFG')
|
27
|
+
assert_equal [], Photo.find_tagged_with(['HIJKLM'])
|
28
|
+
assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')])
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_find_tagged_with_match_all
|
32
|
+
assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_find_tagged_with_match_all_and_include
|
36
|
+
assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true, :include => :tags)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_find_tagged_with_conditions
|
40
|
+
assert_equal [], Post.find_tagged_with('"Very good", Nature', :conditions => '1=0')
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_find_tagged_with_duplicates_options_hash
|
44
|
+
options = { :conditions => '1=1' }.freeze
|
45
|
+
assert_nothing_raised { Post.find_tagged_with("Nature", options) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_find_tagged_with_exclusions
|
49
|
+
assert_equivalent [photos(:jonathan_questioning_dog), photos(:jonathan_bad_cat)], Photo.find_tagged_with("Nature", :exclude => true)
|
50
|
+
assert_equivalent [posts(:jonathan_grass), posts(:jonathan_rain), posts(:jonathan_cloudy), posts(:jonathan_still_cloudy)], Post.find_tagged_with("'Very good', Bad", :exclude => true)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_find_options_for_find_tagged_with_no_tags_returns_empty_hash
|
54
|
+
assert_equal Hash.new, Post.find_options_for_find_tagged_with("")
|
55
|
+
assert_equal Hash.new, Post.find_options_for_find_tagged_with([nil])
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_find_options_for_find_tagged_with_leaves_arguments_unchanged
|
59
|
+
original_tags = photos(:jonathan_questioning_dog).tags.dup
|
60
|
+
Photo.find_options_for_find_tagged_with(photos(:jonathan_questioning_dog).tags)
|
61
|
+
assert_equal original_tags, photos(:jonathan_questioning_dog).tags
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_find_options_for_find_tagged_with_respects_custom_table_name
|
65
|
+
Tagging.table_name = "categorisations"
|
66
|
+
Tag.table_name = "categories"
|
67
|
+
|
68
|
+
options = Photo.find_options_for_find_tagged_with("Hello")
|
69
|
+
|
70
|
+
assert_no_match(/ taggings /, options[:joins])
|
71
|
+
assert_no_match(/ tags /, options[:joins])
|
72
|
+
|
73
|
+
assert_match(/ categorisations /, options[:joins])
|
74
|
+
assert_match(/ categories /, options[:joins])
|
75
|
+
ensure
|
76
|
+
Tagging.table_name = "taggings"
|
77
|
+
Tag.table_name = "tags"
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_include_tags_on_find_tagged_with
|
81
|
+
assert_nothing_raised do
|
82
|
+
Photo.find_tagged_with('Nature', :include => :tags)
|
83
|
+
Photo.find_tagged_with("Nature", :include => { :taggings => :tag })
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_basic_tag_counts_on_class
|
88
|
+
assert_tag_counts Post.tag_counts, :good => 2, :nature => 7, :question => 1, :bad => 1
|
89
|
+
assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_tag_counts_on_class_with_date_conditions
|
93
|
+
assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 5, :question => 1, :bad => 1
|
94
|
+
assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1
|
95
|
+
assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 10)), :good => 1, :nature => 4, :bad => 1
|
96
|
+
|
97
|
+
assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 19)), :good => 1, :nature => 2, :bad => 1, :question => 1, :animal => 3
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_tag_counts_on_class_with_frequencies
|
101
|
+
assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3
|
102
|
+
assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_tag_counts_on_class_with_frequencies_and_conditions
|
106
|
+
assert_tag_counts Photo.tag_counts(:at_least => 2, :conditions => '1=1'), :nature => 3, :animal => 3
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_tag_counts_duplicates_options_hash
|
110
|
+
options = { :at_least => 2, :conditions => '1=1' }.freeze
|
111
|
+
assert_nothing_raised { Photo.tag_counts(options) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_tag_counts_with_limit
|
115
|
+
assert_equal 2, Photo.tag_counts(:limit => 2).size
|
116
|
+
assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_tag_counts_with_limit_and_order
|
120
|
+
assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2)
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_tag_counts_on_association
|
124
|
+
assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 5, :question => 1
|
125
|
+
assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1
|
126
|
+
|
127
|
+
assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1
|
128
|
+
assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_tag_counts_on_association_with_options
|
132
|
+
assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0')
|
133
|
+
assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_tag_counts_on_has_many_through
|
137
|
+
assert_tag_counts users(:jonathan).magazines.tag_counts, :good => 1
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_tag_counts_respects_custom_table_names
|
141
|
+
Tagging.table_name = "categorisations"
|
142
|
+
Tag.table_name = "categories"
|
143
|
+
|
144
|
+
options = Photo.find_options_for_tag_counts(:start_at => 2.weeks.ago, :end_at => Date.today)
|
145
|
+
sql = options.values.join(' ')
|
146
|
+
|
147
|
+
assert_no_match /taggings/, sql
|
148
|
+
assert_no_match /tags/, sql
|
149
|
+
|
150
|
+
assert_match /categorisations/, sql
|
151
|
+
assert_match /categories/, sql
|
152
|
+
ensure
|
153
|
+
Tagging.table_name = "taggings"
|
154
|
+
Tag.table_name = "tags"
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_tag_list_reader
|
158
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
159
|
+
assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_reassign_tag_list
|
163
|
+
assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list
|
164
|
+
posts(:jonathan_rain).taggings.reload
|
165
|
+
|
166
|
+
# Only an update of the posts table should be executed
|
167
|
+
assert_queries 1 do
|
168
|
+
posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s)
|
169
|
+
end
|
170
|
+
|
171
|
+
assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_new_tags
|
175
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
176
|
+
posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two")
|
177
|
+
assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_remove_tag
|
181
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
182
|
+
posts(:jonathan_sky).update_attributes!(:tag_list => "Nature")
|
183
|
+
assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list
|
184
|
+
end
|
185
|
+
|
186
|
+
def test_change_case_of_tags
|
187
|
+
original_tag_names = photos(:jonathan_questioning_dog).tag_list
|
188
|
+
photos(:jonathan_questioning_dog).update_attributes!(:tag_list => photos(:jonathan_questioning_dog).tag_list.to_s.upcase)
|
189
|
+
|
190
|
+
# The new tag list is not uppercase becuase the AR finders are not case-sensitive
|
191
|
+
# and find the old tags when re-tagging with the uppercase tags.
|
192
|
+
assert_equivalent original_tag_names, photos(:jonathan_questioning_dog).reload.tag_list
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_remove_and_add_tag
|
196
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
197
|
+
posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful")
|
198
|
+
assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_tags_not_saved_if_validation_fails
|
202
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
203
|
+
assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "")
|
204
|
+
assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_tag_list_accessors_on_new_record
|
208
|
+
p = Post.new(:text => 'Test')
|
209
|
+
|
210
|
+
assert p.tag_list.blank?
|
211
|
+
p.tag_list = "One, Two"
|
212
|
+
assert_equal "One, Two", p.tag_list.to_s
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_clear_tag_list_with_nil
|
216
|
+
p = photos(:jonathan_questioning_dog)
|
217
|
+
|
218
|
+
assert !p.tag_list.blank?
|
219
|
+
assert p.update_attributes(:tag_list => nil)
|
220
|
+
assert p.tag_list.blank?
|
221
|
+
|
222
|
+
assert p.reload.tag_list.blank?
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_clear_tag_list_with_string
|
226
|
+
p = photos(:jonathan_questioning_dog)
|
227
|
+
|
228
|
+
assert !p.tag_list.blank?
|
229
|
+
assert p.update_attributes(:tag_list => ' ')
|
230
|
+
assert p.tag_list.blank?
|
231
|
+
|
232
|
+
assert p.reload.tag_list.blank?
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_tag_list_reset_on_reload
|
236
|
+
p = photos(:jonathan_questioning_dog)
|
237
|
+
assert !p.tag_list.blank?
|
238
|
+
p.tag_list = nil
|
239
|
+
assert p.tag_list.blank?
|
240
|
+
assert !p.reload.tag_list.blank?
|
241
|
+
end
|
242
|
+
|
243
|
+
def test_instance_tag_counts
|
244
|
+
assert_tag_counts posts(:jonathan_sky).tag_counts, :good => 2, :nature => 7
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_tag_list_populated_when_cache_nil
|
248
|
+
assert_nil posts(:jonathan_sky).cached_tag_list
|
249
|
+
posts(:jonathan_sky).save!
|
250
|
+
assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list
|
251
|
+
end
|
252
|
+
|
253
|
+
def test_cached_tag_list_used
|
254
|
+
posts(:jonathan_sky).save!
|
255
|
+
posts(:jonathan_sky).reload
|
256
|
+
|
257
|
+
assert_no_queries do
|
258
|
+
assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def test_cached_tag_list_not_used
|
263
|
+
# Load fixture and column information
|
264
|
+
posts(:jonathan_sky).taggings(:reload)
|
265
|
+
|
266
|
+
assert_queries 1 do
|
267
|
+
# Tags association will be loaded
|
268
|
+
posts(:jonathan_sky).tag_list
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_cached_tag_list_updated
|
273
|
+
assert_nil posts(:jonathan_sky).cached_tag_list
|
274
|
+
posts(:jonathan_sky).save!
|
275
|
+
assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list)
|
276
|
+
posts(:jonathan_sky).update_attributes!(:tag_list => "None")
|
277
|
+
|
278
|
+
assert_equal 'None', posts(:jonathan_sky).cached_tag_list
|
279
|
+
assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_clearing_cached_tag_list
|
283
|
+
# Generate the cached tag list
|
284
|
+
posts(:jonathan_sky).save!
|
285
|
+
|
286
|
+
posts(:jonathan_sky).update_attributes!(:tag_list => "")
|
287
|
+
assert_equal "", posts(:jonathan_sky).cached_tag_list
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_find_tagged_with_using_sti
|
291
|
+
special_post = SpecialPost.create!(:text => "Test", :tag_list => "Random")
|
292
|
+
|
293
|
+
assert_equal [special_post], SpecialPost.find_tagged_with("Random")
|
294
|
+
assert Post.find_tagged_with("Random").include?(special_post)
|
295
|
+
end
|
296
|
+
|
297
|
+
def test_tag_counts_using_sti
|
298
|
+
SpecialPost.create!(:text => "Test", :tag_list => "Nature")
|
299
|
+
|
300
|
+
assert_tag_counts SpecialPost.tag_counts, :nature => 1
|
301
|
+
end
|
302
|
+
|
303
|
+
def test_case_insensitivity
|
304
|
+
assert_difference "Tag.count", 1 do
|
305
|
+
Post.create!(:text => "Test", :tag_list => "one")
|
306
|
+
Post.create!(:text => "Test", :tag_list => "One")
|
307
|
+
end
|
308
|
+
|
309
|
+
assert_equal Post.find_tagged_with("Nature"), Post.find_tagged_with("nature")
|
310
|
+
end
|
311
|
+
|
312
|
+
def test_tag_not_destroyed_when_unused
|
313
|
+
posts(:jonathan_sky).tag_list.add("Random")
|
314
|
+
posts(:jonathan_sky).save!
|
315
|
+
|
316
|
+
assert_no_difference 'Tag.count' do
|
317
|
+
posts(:jonathan_sky).tag_list.remove("Random")
|
318
|
+
posts(:jonathan_sky).save!
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def test_tag_destroyed_when_unused
|
323
|
+
Tag.destroy_unused = true
|
324
|
+
|
325
|
+
posts(:jonathan_sky).tag_list.add("Random")
|
326
|
+
posts(:jonathan_sky).save!
|
327
|
+
|
328
|
+
assert_difference 'Tag.count', -1 do
|
329
|
+
posts(:jonathan_sky).tag_list.remove("Random")
|
330
|
+
posts(:jonathan_sky).save!
|
331
|
+
end
|
332
|
+
ensure
|
333
|
+
Tag.destroy_unused = false
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase
|
338
|
+
fixtures :tags, :taggings, :posts, :users, :photos
|
339
|
+
|
340
|
+
include ActionView::Helpers::FormHelper
|
341
|
+
|
342
|
+
def test_tag_list_contents
|
343
|
+
fields_for :post, posts(:jonathan_sky) do |f|
|
344
|
+
assert_match /Very good, Nature/, f.text_field(:tag_list)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|