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.
@@ -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