bborn-acts_as_taggable_on_steroids 2.0.beta3 → 2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/README.markdown +157 -0
  2. data/{lib → app/helpers}/tags_helper.rb +0 -0
  3. data/{lib → app/models}/tag.rb +0 -0
  4. data/{lib → app/models}/tag_list.rb +0 -0
  5. data/{lib → app/models}/tagging.rb +0 -0
  6. data/lib/acts_as_taggable.rb +1 -245
  7. data/lib/acts_as_taggable/active_record_extension.rb +246 -0
  8. data/lib/acts_as_taggable/engine.rb +16 -0
  9. data/test/acts_as_taggable_test.rb +63 -74
  10. data/test/dummy/Rakefile +7 -0
  11. data/test/dummy/app/assets/javascripts/application.js +9 -0
  12. data/test/dummy/app/assets/stylesheets/application.css +7 -0
  13. data/test/dummy/app/controllers/application_controller.rb +3 -0
  14. data/test/dummy/app/helpers/application_helper.rb +2 -0
  15. data/test/{fixtures → dummy/app/models}/magazine.rb +0 -0
  16. data/test/{fixtures → dummy/app/models}/photo.rb +0 -0
  17. data/test/{fixtures → dummy/app/models}/post.rb +0 -0
  18. data/test/{fixtures → dummy/app/models}/special_post.rb +0 -0
  19. data/test/{fixtures → dummy/app/models}/subscription.rb +0 -0
  20. data/test/{fixtures → dummy/app/models}/user.rb +0 -0
  21. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  22. data/test/dummy/config.ru +4 -0
  23. data/test/dummy/config/application.rb +45 -0
  24. data/test/dummy/config/boot.rb +10 -0
  25. data/test/dummy/config/database.yml +25 -0
  26. data/test/dummy/config/environment.rb +5 -0
  27. data/test/dummy/config/environments/development.rb +30 -0
  28. data/test/dummy/config/environments/production.rb +60 -0
  29. data/test/dummy/config/environments/test.rb +42 -0
  30. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  31. data/test/dummy/config/initializers/inflections.rb +10 -0
  32. data/test/dummy/config/initializers/mime_types.rb +5 -0
  33. data/test/dummy/config/initializers/secret_token.rb +7 -0
  34. data/test/dummy/config/initializers/session_store.rb +8 -0
  35. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  36. data/test/dummy/config/locales/en.yml +5 -0
  37. data/test/dummy/config/routes.rb +58 -0
  38. data/test/dummy/db/development.sqlite3 +0 -0
  39. data/test/{schema.rb → dummy/db/schema.rb} +0 -0
  40. data/test/dummy/db/test.sqlite3 +0 -0
  41. data/test/dummy/log/test.log +440 -0
  42. data/test/dummy/public/404.html +26 -0
  43. data/test/dummy/public/422.html +26 -0
  44. data/test/dummy/public/500.html +26 -0
  45. data/test/dummy/public/favicon.ico +0 -0
  46. data/test/dummy/script/rails +6 -0
  47. data/test/support/query_counter.rb +29 -0
  48. data/test/tag_list_test.rb +1 -1
  49. data/test/tag_test.rb +63 -63
  50. data/test/tagging_test.rb +1 -1
  51. data/test/tags_helper_test.rb +1 -1
  52. data/test/test_helper.rb +61 -0
  53. metadata +117 -33
  54. data/CHANGELOG +0 -212
  55. data/README +0 -157
  56. data/acts_as_taggable_on_steroids.gemspec +0 -54
  57. data/init.rb +0 -1
  58. data/test/abstract_unit.rb +0 -106
@@ -0,0 +1,157 @@
1
+ ActsAsTaggableOnSteroids
2
+ ========================
3
+
4
+ [![Build Status](https://secure.travis-ci.org/bborn/acts_as_taggable_on_steroids.png)](http://travis-ci.org/bborn/acts_as_taggable_on_steroids)
5
+
6
+
7
+ Instructions
8
+ ------------
9
+
10
+ This plugin is based on acts_as_taggable by DHH but includes extras
11
+ such as tests, smarter tag assignment, and tag cloud calculations.
12
+
13
+ Installation
14
+ ------------
15
+
16
+ gem "bborn-acts_as_taggable_on_steroids"
17
+
18
+
19
+ Usage
20
+ =====
21
+
22
+ Prepare database
23
+ ----------------
24
+
25
+ Generate and apply the migration:
26
+
27
+ ruby script/generate acts_as_taggable_migration
28
+ rake db:migrate
29
+
30
+ Basic tagging
31
+ -------------
32
+
33
+ Let's suppose users have many posts and we want those posts to have tags.
34
+ The first step is to add `acts_as_taggable` to the Post class:
35
+
36
+ class Post < ActiveRecord::Base
37
+ acts_as_taggable
38
+
39
+ belongs_to :user
40
+ end
41
+
42
+ We can now use the tagging methods provided by acts_as_taggable, `#tag_list` and `#tag_list=`. Both these
43
+ methods work like regular attribute accessors.
44
+
45
+ p = Post.find(:first)
46
+ p.tag_list # []
47
+ p.tag_list = "Funny, Silly"
48
+ p.save
49
+ p.tag_list # ["Funny", "Silly"]
50
+
51
+ You can also add or remove arrays of tags.
52
+
53
+ p.tag_list.add("Great", "Awful")
54
+ p.tag_list.remove("Funny")
55
+
56
+ In your views you should use something like the following:
57
+
58
+ <%= f.label :tag_list %>
59
+ <%= f.text_field :tag_list, :size => 80 %>
60
+
61
+ Finding tagged objects
62
+ ----------------------
63
+
64
+ To retrieve objects tagged with a certain tag, use find_tagged_with.
65
+
66
+ Post.tagged_with('Funny, Silly')
67
+
68
+ By default, find_tagged_with will find objects that have any of the given tags. To
69
+ find only objects that are tagged with all the given tags, use match_all.
70
+
71
+ Post.tagged_with('Funny, Silly', :match_all => true)
72
+
73
+ See `ActsAsTaggable::ActiveRecordExtension::InstanceMethods` for more methods and options.
74
+
75
+ Tag cloud calculations
76
+ ----------------------
77
+
78
+ To construct tag clouds, the frequency of each tag needs to be calculated.
79
+ Because we specified `acts_as_taggable` on the `Post` class, we can
80
+ get a calculation of all the tag counts by using `Post.tag_counts`. But what if we wanted a tag count for
81
+ an single user's posts? To achieve this we call tag_counts on the association:
82
+
83
+ User.find(:first).posts.tag_counts
84
+
85
+ A helper is included to assist with generating tag clouds. Include it in your helper file:
86
+
87
+ module ApplicationHelper
88
+ include TagsHelper
89
+ end
90
+
91
+ You can also use the `counts` method on `Tag` to get the counts for all tags in the database.
92
+
93
+ Tag.counts
94
+
95
+ Here is an example that generates a tag cloud.
96
+
97
+ Controller:
98
+
99
+ class PostController < ApplicationController
100
+ def tag_cloud
101
+ @tags = Post.tag_counts
102
+ end
103
+ end
104
+
105
+ View:
106
+
107
+ <% tag_cloud @tags, %w(css1 css2 css3 css4) do |tag, css_class| %>
108
+ <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
109
+ <% end %>
110
+
111
+ CSS:
112
+
113
+ .css1 { font-size: 1.0em; }
114
+ .css2 { font-size: 1.2em; }
115
+ .css3 { font-size: 1.4em; }
116
+ .css4 { font-size: 1.6em; }
117
+
118
+ Caching
119
+ -------
120
+
121
+ It is useful to cache the list of tags to reduce the number of queries executed. To do this,
122
+ add a column named `cached_tag_list` to the model which is being tagged. The column should be long enough to hold
123
+ the full tag list and must have a default value of null, not an empty string.
124
+
125
+ class CachePostTagList < ActiveRecord::Migration
126
+ def self.up
127
+ add_column :posts, :cached_tag_list, :string
128
+ end
129
+ end
130
+
131
+ class Post < ActiveRecord::Base
132
+ acts_as_taggable
133
+
134
+ # The caching column defaults to cached_tag_list, but can be changed:
135
+ #
136
+ # set_cached_tag_list_column_name "my_caching_column_name"
137
+ end
138
+
139
+ The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would.
140
+ Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the
141
+ `tags` or `taggings` associations. To update the cached tag list you should call `save_cached_tag_list` manually.
142
+
143
+ Delimiter
144
+ ---------
145
+
146
+ If you want to change the delimiter used to parse and present tags, set TagList.delimiter.
147
+ For example, to use spaces instead of commas, add the following to your `application.rb`:
148
+
149
+ TagList.delimiter = " "
150
+
151
+ Unused tags
152
+ -----------
153
+
154
+ Set Tag.destroy_unused to remove tags when they are no longer being
155
+ used to tag any objects. Defaults to false.
156
+
157
+ Tag.destroy_unused = true
File without changes
File without changes
File without changes
File without changes
@@ -1,246 +1,2 @@
1
- require 'tag_list'
2
- require 'tag'
3
- require 'tagging'
4
- require 'tags_helper'
5
-
6
- module ActiveRecord #:nodoc:
7
- module Acts #:nodoc:
8
- module Taggable #:nodoc:
9
- def self.included(base)
10
- base.extend(ClassMethods)
11
- end
12
-
13
- module ClassMethods
14
- def acts_as_taggable
15
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
16
- has_many :tags, :through => :taggings
17
-
18
- before_save :save_cached_tag_list
19
- after_save :save_tags
20
-
21
- include ActiveRecord::Acts::Taggable::InstanceMethods
22
- extend ActiveRecord::Acts::Taggable::SingletonMethods
23
-
24
- alias_method_chain :reload, :tag_list
25
- end
26
-
27
- def cached_tag_list_column_name
28
- "cached_tag_list"
29
- end
30
-
31
- def set_cached_tag_list_column_name(value = nil, &block)
32
- define_attr_method :cached_tag_list_column_name, value, &block
33
- end
34
- end
35
-
36
- module SingletonMethods
37
- # Pass either a tag, string, or an array of strings or tags.
38
- #
39
- # Options:
40
- # - +:match_any+ - match any of the given tags (default).
41
- # - +:match_all+ - match all of the given tags.
42
- #
43
- def tagged_with(tags, options = {})
44
- tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
45
- return [] if tags.empty?
46
-
47
- records = select("DISTINCT #{quoted_table_name}.*")
48
-
49
- if options[:match_all]
50
- records.search_all_tags(tags)
51
- else
52
- records.search_any_tags(tags)
53
- end
54
- end
55
-
56
- # Matches records that have none of the given tags.
57
- def not_tagged_with(tags)
58
- tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
59
-
60
- sub = Tagging.select("#{Tagging.table_name}.taggable_id").joins(:tag).
61
- where(:taggable_type => base_class.name, "#{Tag.table_name}.name" => tags)
62
-
63
- where("#{quoted_table_name}.#{primary_key} NOT IN (" + sub.to_sql + ")")
64
- end
65
-
66
- # Returns an array of related tags. Related tags are all the other tags
67
- # that are found on the models tagged with the provided tags.
68
- def related_tags(tags)
69
- search_related_tags(tags)
70
- end
71
-
72
- # Counts the number of occurences of all tags.
73
- # See <tt>Tag.counts</tt> for options.
74
- def tag_counts(options = {})
75
- tags = Tag.joins(:taggings).
76
- where("#{Tagging.table_name}.taggable_type" => base_class.name)
77
-
78
- if options[:tags]
79
- tags = tags.where("#{Tag.table_name}.name" => options.delete(:tags))
80
- end
81
-
82
- unless descends_from_active_record?
83
- tags = tags.joins("INNER JOIN #{quoted_table_name} ON " +
84
- "#{quoted_table_name}.#{primary_key} = #{Tagging.quoted_table_name}.taggable_id")
85
- tags = tags.where(type_condition)
86
- end
87
-
88
- if scoped != unscoped
89
- sub = scoped.except(:select).select("#{quoted_table_name}.#{primary_key}")
90
- tags = tags.where("#{Tagging.quoted_table_name}.taggable_id IN (#{sub.to_sql})")
91
- end
92
-
93
- tags.counts(options)
94
- end
95
-
96
- # Returns an array of related tags.
97
- # Related tags are all the other tags that are found on the models
98
- # tagged with the provided tags.
99
- #
100
- # Pass either a tag, string, or an array of strings or tags.
101
- #
102
- # Options:
103
- # - +:order+ - SQL Order how to order the tags. Defaults to "count_all DESC, tags.name".
104
- # - +:include+
105
- #
106
- # DEPRECATED: use #related_tags instead.
107
- def find_related_tags(tags, options = {})
108
- rs = related_tags(tags).order(options[:order] || "count DESC, #{Tag.quoted_table_name}.name")
109
- rs = rs.includes(options[:include]) if options[:include]
110
- rs
111
- end
112
-
113
- # Pass either a tag, string, or an array of strings or tags.
114
- #
115
- # Options:
116
- # - +:exclude+ - Find models that are not tagged with the given tags
117
- # - +:match_all+ - Find models that match all of the given tags, not just one
118
- # - +:conditions+ - A piece of SQL conditions to add to the query
119
- # - +:include+
120
- #
121
- # DEPRECATED: use #tagged_with and #not_tagged_with instead.
122
- def find_tagged_with(*args)
123
- options = args.extract_options!
124
- tags = args.first
125
-
126
- records = self
127
- records = records.where(options[:conditions]) if options[:conditions]
128
- records = records.includes(options[:include]) if options[:include]
129
- records = records.order(options[:order]) if options[:order]
130
-
131
- if options[:exclude]
132
- records.not_tagged_with(tags)
133
- else
134
- records.tagged_with(tags, options)
135
- end
136
- end
137
-
138
- def caching_tag_list?
139
- column_names.include?(cached_tag_list_column_name)
140
- end
141
-
142
- protected
143
- def joins_tags(options = {}) # :nodoc:
144
- options[:suffix] = "_#{options[:suffix]}" if options[:suffix]
145
-
146
- taggings_alias = connection.quote_table_name(Tagging.table_name + options[:suffix].to_s)
147
- tags_alias = connection.quote_table_name(Tag.table_name + options[:suffix].to_s)
148
-
149
- taggings = "INNER JOIN #{Tagging.quoted_table_name} AS #{taggings_alias} " +
150
- "ON #{taggings_alias}.taggable_id = #{quoted_table_name}.#{primary_key} " +
151
- "AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
152
-
153
- tags = "INNER JOIN #{Tag.quoted_table_name} AS #{tags_alias} " +
154
- "ON #{tags_alias}.id = #{taggings_alias}.tag_id "
155
- tags += "AND #{tags_alias}.name LIKE #{quote_value(options[:tag_name])}" if options[:tag_name]
156
-
157
- joins([taggings, tags])
158
- end
159
-
160
- def search_all_tags(tags)
161
- records = self
162
-
163
- tags.dup.each_with_index do |tag_name, index|
164
- records = records.joins_tags(:suffix => index, :tag_name => tag_name)
165
- end
166
-
167
- records
168
- end
169
-
170
- def search_any_tags(tags)
171
- joins(:tags).where(Tag.arel_table[:name].matches_any(tags.dup))
172
- end
173
-
174
- def search_related_tags(tags)
175
- tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
176
- sub = select("#{quoted_table_name}.#{primary_key}").search_any_tags(tags)
177
- _tags = tags.map { |tag| tag.downcase }
178
-
179
- Tag.select("#{Tag.quoted_table_name}.*, COUNT(#{Tag.quoted_table_name}.id) AS count").
180
- joins(:taggings).
181
- where("#{Tagging.table_name}.taggable_type" => base_class.name).
182
- where("#{Tagging.quoted_table_name}.taggable_id IN (" + sub.to_sql + ")").
183
- group("#{Tag.quoted_table_name}.name").
184
- having(Tag.arel_table[:name].does_not_match_all(_tags))
185
- end
186
- end
187
-
188
- module InstanceMethods
189
- def tag_list
190
- return @tag_list if @tag_list
191
-
192
- if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
193
- @tag_list = TagList.from(cached_value)
194
- else
195
- @tag_list = TagList.new(*tags.map(&:name))
196
- end
197
- end
198
-
199
- def tag_list=(value)
200
- @tag_list = TagList.from(value)
201
- end
202
-
203
- def save_cached_tag_list
204
- if self.class.caching_tag_list?
205
- self[self.class.cached_tag_list_column_name] = tag_list.to_s
206
- end
207
- end
208
-
209
- def save_tags
210
- return unless @tag_list
211
-
212
- new_tag_names = @tag_list - tags.map(&:name)
213
- old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
214
-
215
- self.class.transaction do
216
- if old_tags.any?
217
- taggings.where(:tag_id => old_tags.map(&:id)).each(&:destroy)
218
- taggings.reset
219
- end
220
-
221
- new_tag_names.each do |new_tag_name|
222
- tags << Tag.find_or_create_with_like_by_name(new_tag_name)
223
- end
224
- end
225
-
226
- true
227
- end
228
-
229
- # Calculate the tag counts for the tags used by this model.
230
- # See <tt>Tag.counts</tt> for available options.
231
- def tag_counts(options = {})
232
- return [] if tag_list.blank?
233
- self.class.tag_counts(options.merge(:tags => tag_list))
234
- end
235
-
236
- def reload_with_tag_list(*args) #:nodoc:
237
- @tag_list = nil
238
- reload_without_tag_list(*args)
239
- end
240
- end
241
- end
242
- end
243
- end
244
-
245
- ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
246
1
 
2
+ require File.join(File.dirname(__FILE__), 'acts_as_taggable/engine')
@@ -0,0 +1,246 @@
1
+
2
+ module ActsAsTaggable #:nodoc:
3
+ module ActiveRecordExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_taggable
12
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
13
+ has_many :tags, :through => :taggings
14
+
15
+ before_save :save_cached_tag_list
16
+ after_save :save_tags
17
+
18
+ include ActsAsTaggable::ActiveRecordExtension::InstanceMethods
19
+ extend ActsAsTaggable::ActiveRecordExtension::SingletonMethods
20
+
21
+ alias_method_chain :reload, :tag_list
22
+ end
23
+
24
+ def cached_tag_list_column_name
25
+ "cached_tag_list"
26
+ end
27
+
28
+ def set_cached_tag_list_column_name(value = nil, &block)
29
+ define_attr_method :cached_tag_list_column_name, value, &block
30
+ end
31
+ end
32
+
33
+ module SingletonMethods
34
+ # Pass either a tag, string, or an array of strings or tags.
35
+ #
36
+ # Options:
37
+ # - +:match_any+ - match any of the given tags (default).
38
+ # - +:match_all+ - match all of the given tags.
39
+ #
40
+ def tagged_with(tags, options = {})
41
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
42
+ return [] if tags.empty?
43
+
44
+ records = select("DISTINCT #{quoted_table_name}.*")
45
+
46
+ if options[:match_all]
47
+ records.search_all_tags(tags)
48
+ else
49
+ records.search_any_tags(tags)
50
+ end
51
+ end
52
+
53
+ # Matches records that have none of the given tags.
54
+ def not_tagged_with(tags)
55
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
56
+
57
+ sub = Tagging.select("#{Tagging.table_name}.taggable_id").joins(:tag).
58
+ where(:taggable_type => base_class.name, "#{Tag.table_name}.name" => tags)
59
+
60
+ where("#{quoted_table_name}.#{primary_key} NOT IN (" + sub.to_sql + ")")
61
+ end
62
+
63
+ # Returns an array of related tags. Related tags are all the other tags
64
+ # that are found on the models tagged with the provided tags.
65
+ def related_tags(tags)
66
+ search_related_tags(tags)
67
+ end
68
+
69
+ # Counts the number of occurences of all tags.
70
+ # See <tt>Tag.counts</tt> for options.
71
+ def tag_counts(options = {})
72
+ tags = Tag.joins(:taggings).
73
+ where("#{Tagging.table_name}.taggable_type" => base_class.name)
74
+
75
+ if options[:tags]
76
+ tags = tags.where("#{Tag.table_name}.name" => options.delete(:tags))
77
+ end
78
+
79
+ unless descends_from_active_record?
80
+ tags = tags.joins("INNER JOIN #{quoted_table_name} ON " +
81
+ "#{quoted_table_name}.#{primary_key} = #{Tagging.quoted_table_name}.taggable_id")
82
+ tags = tags.where(type_condition)
83
+ end
84
+
85
+ if scoped != unscoped
86
+ sub = scoped.except(:select).select("#{quoted_table_name}.#{primary_key}")
87
+ tags = tags.where("#{Tagging.quoted_table_name}.taggable_id IN (#{sub.to_sql})")
88
+ end
89
+
90
+ tags.counts(options)
91
+ end
92
+
93
+ # Returns an array of related tags.
94
+ # Related tags are all the other tags that are found on the models
95
+ # tagged with the provided tags.
96
+ #
97
+ # Pass either a tag, string, or an array of strings or tags.
98
+ #
99
+ # Options:
100
+ # - +:order+ - SQL Order how to order the tags. Defaults to "count_all DESC, tags.name".
101
+ # - +:include+
102
+ #
103
+ # DEPRECATED: use #related_tags instead.
104
+ def find_related_tags(tags, options = {})
105
+ ActiveSupport::Deprecation.warn "#find_related_tags() is deprecated and will be removed in the next release. Use #related_tags", caller
106
+
107
+ rs = related_tags(tags).order(options[:order] || "count DESC, #{Tag.quoted_table_name}.name")
108
+ rs = rs.includes(options[:include]) if options[:include]
109
+ rs
110
+ end
111
+
112
+ # Pass either a tag, string, or an array of strings or tags.
113
+ #
114
+ # Options:
115
+ # - +:exclude+ - Find models that are not tagged with the given tags
116
+ # - +:match_all+ - Find models that match all of the given tags, not just one
117
+ # - +:conditions+ - A piece of SQL conditions to add to the query
118
+ # - +:include+
119
+ #
120
+ # DEPRECATED: use #tagged_with and #not_tagged_with instead.
121
+ def find_tagged_with(*args)
122
+ ActiveSupport::Deprecation.warn "#find_tagged_with() is deprecated and will be removed in the next release. Use #tagged_with and #not_tagged_with instead", caller
123
+ options = args.extract_options!
124
+ tags = args.first
125
+
126
+ records = self
127
+ records = records.where(options[:conditions]) if options[:conditions]
128
+ records = records.includes(options[:include]) if options[:include]
129
+ records = records.order(options[:order]) if options[:order]
130
+
131
+ if options[:exclude]
132
+ records.not_tagged_with(tags)
133
+ else
134
+ records.tagged_with(tags, options)
135
+ end
136
+ end
137
+
138
+ def caching_tag_list?
139
+ column_names.include?(cached_tag_list_column_name)
140
+ end
141
+
142
+ protected
143
+ def joins_tags(options = {}) # :nodoc:
144
+ options[:suffix] = "_#{options[:suffix]}" if options[:suffix]
145
+
146
+ taggings_alias = connection.quote_table_name(Tagging.table_name + options[:suffix].to_s)
147
+ tags_alias = connection.quote_table_name(Tag.table_name + options[:suffix].to_s)
148
+
149
+ taggings = "INNER JOIN #{Tagging.quoted_table_name} AS #{taggings_alias} " +
150
+ "ON #{taggings_alias}.taggable_id = #{quoted_table_name}.#{primary_key} " +
151
+ "AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
152
+
153
+ tags = "INNER JOIN #{Tag.quoted_table_name} AS #{tags_alias} " +
154
+ "ON #{tags_alias}.id = #{taggings_alias}.tag_id "
155
+ tags += "AND #{tags_alias}.name LIKE #{quote_value(options[:tag_name])}" if options[:tag_name]
156
+
157
+ joins([taggings, tags])
158
+ end
159
+
160
+ def search_all_tags(tags)
161
+ records = self
162
+
163
+ tags.dup.each_with_index do |tag_name, index|
164
+ records = records.joins_tags(:suffix => index, :tag_name => tag_name)
165
+ end
166
+
167
+ records
168
+ end
169
+
170
+ def search_any_tags(tags)
171
+ joins(:tags).where(Tag.arel_table[:name].matches_any(tags.dup))
172
+ end
173
+
174
+ def search_related_tags(tags)
175
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
176
+
177
+ return where('1=0') if tags.empty?
178
+
179
+ sub = select("#{quoted_table_name}.#{primary_key}").search_any_tags(tags)
180
+ _tags = tags.map { |tag| tag.downcase }
181
+
182
+ Tag.select("#{Tag.quoted_table_name}.*, COUNT(#{Tag.quoted_table_name}.id) AS count").
183
+ joins(:taggings).
184
+ where("#{Tagging.table_name}.taggable_type" => base_class.name).
185
+ where("#{Tagging.quoted_table_name}.taggable_id IN (" + sub.to_sql + ")").
186
+ group("#{Tag.quoted_table_name}.name").
187
+ having(Tag.arel_table[:name].does_not_match_all(_tags))
188
+ end
189
+ end
190
+
191
+ module InstanceMethods
192
+ def tag_list
193
+ return @tag_list if @tag_list
194
+
195
+ if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
196
+ @tag_list = TagList.from(cached_value)
197
+ else
198
+ @tag_list = TagList.new(*tags.map(&:name))
199
+ end
200
+ end
201
+
202
+ def tag_list=(value)
203
+ @tag_list = TagList.from(value)
204
+ end
205
+
206
+ def save_cached_tag_list
207
+ if self.class.caching_tag_list?
208
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
209
+ end
210
+ end
211
+
212
+ def save_tags
213
+ return unless @tag_list
214
+
215
+ new_tag_names = @tag_list - tags.map(&:name)
216
+ old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
217
+
218
+ self.class.transaction do
219
+ if old_tags.any?
220
+ taggings.where(:tag_id => old_tags.map(&:id)).each(&:destroy)
221
+ taggings.reset
222
+ end
223
+
224
+ new_tag_names.each do |new_tag_name|
225
+ tags << Tag.find_or_create_with_like_by_name(new_tag_name)
226
+ end
227
+ end
228
+
229
+ true
230
+ end
231
+
232
+ # Calculate the tag counts for the tags used by this model.
233
+ # See <tt>Tag.counts</tt> for available options.
234
+ def tag_counts(options = {})
235
+ return [] if tag_list.blank?
236
+ self.class.tag_counts(options.merge(:tags => tag_list))
237
+ end
238
+
239
+ def reload_with_tag_list(*args) #:nodoc:
240
+ @tag_list = nil
241
+ reload_without_tag_list(*args)
242
+ end
243
+ end
244
+ end
245
+ end
246
+