acts-as-taggable-on 1.0.13 → 2.0.0.pre1

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 CHANGED
@@ -1,3 +1,6 @@
1
+ == 2010-02-17
2
+ * Converted the plugin to be compatible with Rails3
3
+
1
4
  == 2009-12-02
2
5
 
3
6
  * PostgreSQL is now supported (via morgoth)
@@ -12,10 +15,10 @@
12
15
  * Removed extraneous down migration cruft (azabaj)
13
16
 
14
17
  == 2008-06-09
15
-
18
+
16
19
  * Added support for Single Table Inheritance
17
20
  * Adding gemspec and rails/init.rb for gemified plugin
18
-
21
+
19
22
  == 2007-12-12
20
23
 
21
24
  * Added ability to use dynamic tag contexts
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :gemcutter
2
+
3
+ gem 'rails', '3.0.0.beta'
4
+
5
+ group :test do
6
+ gem 'rspec', '2.0.0.beta.1'
7
+ gem 'sqlite3-ruby', :require => 'sqlite3'
8
+ end
data/README.rdoc CHANGED
@@ -89,6 +89,9 @@ also improves compatibility with the will_paginate gem:
89
89
  User.tagged_with("awesome").by_date
90
90
  User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
91
91
 
92
+ #Find a user with matching all tags, not just one
93
+ User.tagged_with(["awesome", "cool"], :match_all => :true)
94
+
92
95
  === Relationships
93
96
 
94
97
  You can find objects of the same type based on similar tags on certain contexts.
@@ -151,6 +154,12 @@ A helper is included to assist with generating tag clouds.
151
154
 
152
155
  Here is an example that generates a tag cloud.
153
156
 
157
+ Helper:
158
+
159
+ module PostsHelper
160
+ include TagsHelper
161
+ end
162
+
154
163
  Controller:
155
164
 
156
165
  class PostController < ApplicationController
@@ -160,7 +169,8 @@ Controller:
160
169
  end
161
170
 
162
171
  View:
163
- <% tag_cloud @tags, %w(css1 css2 css3 css4) do |tag, css_class| %>
172
+
173
+ <% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
164
174
  <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
165
175
  <% end %>
166
176
 
@@ -187,5 +197,6 @@ CSS:
187
197
  * slainer68 - STI fix
188
198
  * harrylove - migration instructions and fix-ups
189
199
  * lawrencepit - cached tag work
200
+ * sobrinho - fixed tag_cloud helper
190
201
 
191
202
  Copyright (c) 2007-2009 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
- require 'spec/rake/spectask'
1
+ gem 'rspec', '2.0.0.beta.1'
2
+ require 'rspec/core/rake_task'
2
3
 
3
4
  begin
4
5
  require 'jeweler'
@@ -9,7 +10,7 @@ begin
9
10
  gemspec.email = "michael@intridea.com"
10
11
  gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
11
12
  gemspec.authors = ["Michael Bleigh"]
12
- gemspec.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"] - FileList["**/*.log"]
13
+ gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
13
14
  end
14
15
  Jeweler::GemcutterTasks.new
15
16
  rescue LoadError
@@ -18,12 +19,12 @@ end
18
19
 
19
20
  desc 'Default: run specs'
20
21
  task :default => :spec
21
- Spec::Rake::SpecTask.new do |t|
22
- t.spec_files = FileList["spec/**/*_spec.rb"]
22
+ Rspec::Core::RakeTask.new do |t|
23
+ t.pattern = "spec/**/*_spec.rb"
23
24
  end
24
25
 
25
- Spec::Rake::SpecTask.new('rcov') do |t|
26
- t.spec_files = FileList["spec/**/*_spec.rb"]
26
+ Rspec::Core::RakeTask.new('rcov') do |t|
27
+ t.pattern = "spec/**/*_spec.rb"
27
28
  t.rcov = true
28
29
  t.rcov_opts = ['--exclude', 'spec']
29
30
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.13
1
+ 2.0.0.pre1
@@ -1,7 +1,27 @@
1
- require 'acts_as_taggable_on/group_helper'
2
- require 'acts_as_taggable_on/acts_as_taggable_on'
3
- require 'acts_as_taggable_on/acts_as_tagger'
4
- require 'acts_as_taggable_on/tag'
5
- require 'acts_as_taggable_on/tag_list'
6
- require 'acts_as_taggable_on/tags_helper'
7
- require 'acts_as_taggable_on/tagging'
1
+ begin
2
+ # Try to require the preresolved locked set of gems.
3
+ require File.expand_path("../.bundle/environment", __FILE__)
4
+ rescue LoadError
5
+ # Fall back on doing an unlocked resolve at runtime.
6
+ require "rubygems"
7
+ require "bundler"
8
+ Bundler.setup
9
+ end
10
+
11
+ Bundler.require
12
+
13
+ require "active_record"
14
+ require "action_view"
15
+
16
+ require "acts_as_taggable_on/group_helper"
17
+ require "acts_as_taggable_on/acts_as_taggable_on"
18
+ require "acts_as_taggable_on/acts_as_tagger"
19
+ require "acts_as_taggable_on/tag"
20
+ require "acts_as_taggable_on/tag_list"
21
+ require "acts_as_taggable_on/tags_helper"
22
+ require "acts_as_taggable_on/tagging"
23
+
24
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
25
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
26
+
27
+ ActionView::Base.send :include, TagsHelper
@@ -1,11 +1,13 @@
1
1
  module ActiveRecord
2
2
  module Acts
3
3
  module TaggableOn
4
+
4
5
  def self.included(base)
5
6
  base.extend(ClassMethods)
6
7
  end
7
8
 
8
9
  module ClassMethods
10
+
9
11
  def taggable?
10
12
  false
11
13
  end
@@ -17,17 +19,19 @@ module ActiveRecord
17
19
  def acts_as_taggable_on(*args)
18
20
  args.flatten! if args
19
21
  args.compact! if args
22
+
20
23
  for tag_type in args
21
24
  tag_type = tag_type.to_s
22
- # use aliased_join_table_name for context condition so that sphix can join multiple
25
+ # use aliased_join_table_name for context condition so that sphinx can join multiple
23
26
  # tag references from same model without getting an ambiguous column error
24
- self.class_eval do
27
+ class_eval do
25
28
  has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
26
- :include => :tag, :conditions => ['#{aliased_join_table_name rescue "taggings"}.context = ?',tag_type], :class_name => "Tagging"
29
+ :include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
27
30
  has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
28
31
  end
29
32
 
30
- self.class_eval <<-RUBY
33
+ class_eval <<-RUBY
34
+
31
35
  def self.taggable?
32
36
  true
33
37
  end
@@ -68,7 +72,7 @@ module ActiveRecord
68
72
  def find_matching_contexts(search_context, result_context, options = {})
69
73
  matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
70
74
  end
71
-
75
+
72
76
  def find_matching_contexts_for(klass, search_context, result_context, options = {})
73
77
  matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
74
78
  end
@@ -80,13 +84,14 @@ module ActiveRecord
80
84
  def self.top_#{tag_type}(limit = 10)
81
85
  tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
82
86
  end
87
+
83
88
  RUBY
84
89
  end
85
90
 
86
91
  if respond_to?(:tag_types)
87
92
  write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
88
93
  else
89
- self.class_eval do
94
+ class_eval do
90
95
  write_inheritable_attribute(:tag_types, args.uniq)
91
96
  class_inheritable_reader :tag_types
92
97
 
@@ -96,13 +101,12 @@ module ActiveRecord
96
101
  attr_writer :custom_contexts
97
102
 
98
103
  before_save :save_cached_tag_list
99
- after_save :save_tags
100
104
 
101
- if respond_to?(:named_scope)
102
- named_scope :tagged_with, lambda{ |*args|
103
- find_options_for_find_tagged_with(*args)
104
- }
105
- end
105
+ after_save:save_tags
106
+
107
+ scope :tagged_with, lambda{ |*args|
108
+ find_options_for_find_tagged_with(*args)
109
+ }
106
110
  end
107
111
 
108
112
  include ActiveRecord::Acts::TaggableOn::InstanceMethods
@@ -110,68 +114,72 @@ module ActiveRecord
110
114
  alias_method_chain :reload, :tag_list
111
115
  end
112
116
  end
117
+
113
118
  end
114
119
 
115
120
  module SingletonMethods
121
+
116
122
  include ActiveRecord::Acts::TaggableOn::GroupHelper
123
+
117
124
  # Pass either a tag string, or an array of strings or tags
118
125
  #
119
126
  # Options:
127
+ # :any - find models that match any of the given tags
120
128
  # :exclude - Find models that are not tagged with the given tags
121
129
  # :match_all - Find models that match all of the given tags, not just one
122
130
  # :conditions - A piece of SQL conditions to add to the query
123
131
  # :on - scopes the find to a context
124
- def find_tagged_with(*args)
125
- options = find_options_for_find_tagged_with(*args)
126
- options.blank? ? [] : find(:all,options)
127
- end
132
+ # def find_tagged_with(*args)
133
+ # find_options_for_find_tagged_with(*args)
134
+ # end
128
135
 
129
136
  def caching_tag_list_on?(context)
130
137
  column_names.include?("cached_#{context.to_s.singularize}_list")
131
138
  end
132
139
 
133
140
  def tag_counts_on(context, options = {})
134
- Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
141
+ find_for_tag_counts(options.merge({:on => context.to_s}))
135
142
  end
136
143
 
137
144
  def all_tag_counts(options = {})
138
- Tag.find(:all, find_options_for_tag_counts(options))
145
+ find_for_tag_counts(options)
139
146
  end
140
147
 
141
148
  def find_options_for_find_tagged_with(tags, options = {})
142
- tags = TagList.from(tags)
149
+ tag_list = TagList.from(tags)
143
150
 
144
- return {} if tags.empty?
151
+ return {} if tag_list.empty?
145
152
 
146
153
  joins = []
147
154
  conditions = []
148
155
 
149
156
  context = options.delete(:on)
150
157
 
151
-
152
158
  if options.delete(:exclude)
153
- tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
159
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
154
160
  conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
155
161
 
162
+ elsif options.delete(:any)
163
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
164
+ conditions << "#{table_name}.#{primary_key} IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
165
+
156
166
  else
167
+ tags = Tag.named_any(tag_list)
168
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
169
+
157
170
  tags.each do |tag|
158
- safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
171
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
159
172
  prefix = "#{safe_tag}_#{rand(1024)}"
160
173
 
161
174
  taggings_alias = "#{table_name}_taggings_#{prefix}"
162
- tags_alias = "#{table_name}_tags_#{prefix}"
163
175
 
164
176
  tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
165
177
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
166
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
178
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
179
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
167
180
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
168
181
 
169
- tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
170
- " ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
171
- " AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
172
-
173
182
  joins << tagging_join
174
- joins << tag_join
175
183
  end
176
184
  end
177
185
 
@@ -185,9 +193,12 @@ module ActiveRecord
185
193
  group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
186
194
  end
187
195
 
188
- { :joins => joins.join(" "),
189
- :group => group,
190
- :conditions => conditions.join(" AND ") }.update(options)
196
+ Tag.joins(joins.join(" ")).group(group).where(conditions.join(" AND ")).readonly(false)
197
+
198
+ # { :joins => joins.join(" "),
199
+ # :group => group,
200
+ # :conditions => conditions.join(" AND "),
201
+ # :readonly => false }.update(options)
191
202
  end
192
203
 
193
204
  # Calculate the tag counts for all tags.
@@ -201,10 +212,9 @@ module ActiveRecord
201
212
  # :at_least - Exclude tags with a frequency less than the given value
202
213
  # :at_most - Exclude tags with a frequency greater than the given value
203
214
  # :on - Scope the find to only include a certain context
204
- def find_options_for_tag_counts(options = {})
215
+ def find_for_tag_counts(options = {})
205
216
  options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
206
217
 
207
- scope = scope(:find)
208
218
  start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
209
219
  end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
210
220
 
@@ -221,40 +231,34 @@ module ActiveRecord
221
231
  ]
222
232
 
223
233
  conditions = conditions.compact.join(' AND ')
224
- conditions = merge_conditions(conditions, scope[:conditions]) if scope
225
234
 
226
235
  joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
227
236
  joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
228
-
229
237
  joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
230
- unless self.descends_from_active_record?
238
+
239
+ unless descends_from_active_record?
231
240
  # Current model is STI descendant, so add type checking to the join condition
232
- joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
241
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
233
242
  end
234
243
 
235
- joins << scope[:joins] if scope && scope[:joins]
236
-
237
244
  at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
238
245
  at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
239
246
  having = [at_least, at_most].compact.join(' AND ')
240
247
  group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
241
248
  group_by << " AND #{having}" unless having.blank?
242
249
 
243
- { :select => "#{Tag.table_name}.*, COUNT(*) AS count",
244
- :joins => joins.join(" "),
245
- :conditions => conditions,
246
- :group => group_by,
247
- :limit => options[:limit],
248
- :order => options[:order]
249
- }
250
+ Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(joins.join(" ")).where(conditions).group(group_by).limit(options[:limit]).order(options[:order])
251
+
250
252
  end
251
253
 
252
254
  def is_taggable?
253
255
  true
254
256
  end
257
+
255
258
  end
256
259
 
257
260
  module InstanceMethods
261
+
258
262
  include ActiveRecord::Acts::TaggableOn::GroupHelper
259
263
 
260
264
  def custom_contexts
@@ -269,51 +273,68 @@ module ActiveRecord
269
273
  custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
270
274
  end
271
275
 
272
- def tag_list_on(context, owner=nil)
273
- var_name = context.to_s.singularize + "_list"
276
+ def tag_list_on(context, owner = nil)
274
277
  add_custom_context(context)
275
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
278
+ cache = tag_list_cache_on(context)
279
+ return owner ? cache[owner] : cache[owner] if cache[owner]
276
280
 
277
281
  if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
278
- instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
282
+ cache[owner] = TagList.from(cached_tag_list_on(context))
279
283
  else
280
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
284
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
281
285
  end
282
286
  end
283
287
 
284
- def tags_on(context, owner=nil)
288
+ def all_tags_list_on(context)
289
+ variable_name = "@all_#{context.to_s.singularize}_list"
290
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
291
+ instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
292
+ end
293
+
294
+ def all_tags_on(context)
295
+ opts = ["#{Tagging.table_name}.context = ?", context.to_s]
296
+ base_tags.where(opts).order("#{Tagging.table_name}.created_at")
297
+ end
298
+
299
+ def tags_on(context, owner = nil)
285
300
  if owner
286
- opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
287
- context.to_s, owner.id, owner.class.to_s]}
301
+ opts = ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?", context.to_s, owner.id, owner.class.to_s]
288
302
  else
289
- opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
303
+ opts = ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]
290
304
  end
291
- base_tags.find(:all, opts)
305
+ base_tags.where(opts)
292
306
  end
293
307
 
294
308
  def cached_tag_list_on(context)
295
309
  self["cached_#{context.to_s.singularize}_list"]
296
310
  end
297
311
 
298
- def set_tag_list_on(context,new_list, tagger=nil)
299
- instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
312
+ def tag_list_cache_on(context)
313
+ variable_name = "@#{context.to_s.singularize}_list"
314
+ cache = instance_variable_get(variable_name)
315
+ instance_variable_set(variable_name, cache = {}) unless cache
316
+ cache
317
+ end
318
+
319
+ def set_tag_list_on(context, new_list, tagger = nil)
320
+ tag_list_cache_on(context)[tagger] = TagList.from(new_list)
300
321
  add_custom_context(context)
301
322
  end
302
323
 
303
324
  def tag_counts_on(context, options={})
304
- self.class.tag_counts_on(context, options.merge(:id => self.id))
325
+ self.class.tag_counts_on(context, options.merge(:id => id))
305
326
  end
306
327
 
307
328
  def related_tags_for(context, klass, options = {})
308
329
  search_conditions = related_search_options(context, klass, options)
309
330
 
310
- klass.find(:all, search_conditions)
331
+ klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
311
332
  end
312
333
 
313
334
  def related_search_options(context, klass, options = {})
314
- tags_to_find = self.tags_on(context).collect { |t| t.name }
335
+ tags_to_find = tags_on(context).collect { |t| t.name }
315
336
 
316
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
337
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
317
338
 
318
339
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
319
340
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
@@ -322,17 +343,17 @@ module ActiveRecord
322
343
  :order => "count DESC"
323
344
  }.update(options)
324
345
  end
325
-
346
+
326
347
  def matching_contexts_for(search_context, result_context, klass, options = {})
327
348
  search_conditions = matching_context_search_options(search_context, result_context, klass, options)
328
349
 
329
- klass.find(:all, search_conditions)
350
+ klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
330
351
  end
331
-
352
+
332
353
  def matching_context_search_options(search_context, result_context, klass, options = {})
333
- tags_to_find = self.tags_on(search_context).collect { |t| t.name }
354
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
334
355
 
335
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
356
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
336
357
 
337
358
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
338
359
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
@@ -341,28 +362,49 @@ module ActiveRecord
341
362
  :order => "count DESC"
342
363
  }.update(options)
343
364
  end
344
-
365
+
345
366
  def save_cached_tag_list
346
367
  self.class.tag_types.map(&:to_s).each do |tag_type|
347
368
  if self.class.send("caching_#{tag_type.singularize}_list?")
348
- self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
369
+ self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
349
370
  end
350
371
  end
351
372
  end
352
373
 
353
374
  def save_tags
354
- (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
355
- next unless instance_variable_get("@#{tag_type.singularize}_list")
356
- owner = instance_variable_get("@#{tag_type.singularize}_list").owner
357
- new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
358
- old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
359
-
360
- self.class.transaction do
361
- base_tags.delete(*old_tags) if old_tags.any?
362
- new_tag_names.each do |new_tag_name|
363
- new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
364
- Tagging.create(:tag_id => new_tag.id, :context => tag_type,
365
- :taggable => self, :tagger => owner)
375
+ contexts = custom_contexts + self.class.tag_types.map(&:to_s)
376
+
377
+ transaction do
378
+ contexts.each do |context|
379
+ cache = tag_list_cache_on(context)
380
+
381
+ cache.each do |owner, list|
382
+ new_tags = Tag.find_or_create_all_with_like_by_name(list.uniq)
383
+ taggings = Tagging.where({ :taggable_id => self.id, :taggable_type => self.class.base_class.to_s })
384
+
385
+ # Destroy old taggings:
386
+ if owner
387
+ old_tags = tags_on(context, owner) - new_tags
388
+ old_taggings = Tagging.where({ :taggable_id => self.id, :taggable_type => self.class.base_class.to_s, :tag_id => old_tags, :tagger_id => owner.id, :tagger_type => owner.class.to_s, :context => context })
389
+
390
+ Tagging.destroy_all :id => old_taggings.map(&:id)
391
+ else
392
+ old_tags = tags_on(context) - new_tags
393
+ base_tags.delete(*old_tags)
394
+ end
395
+
396
+ new_tags.reject! { |tag| taggings.any? { |tagging|
397
+ tagging.tag_id == tag.id &&
398
+ tagging.tagger_id == (owner ? owner.id : nil) &&
399
+ tagging.tagger_type == (owner ? owner.class.to_s : nil) &&
400
+ tagging.context == context
401
+ }
402
+ }
403
+
404
+ # create new taggings:
405
+ new_tags.each do |tag|
406
+ Tagging.create!(:tag_id => tag.id, :context => context, :tagger => owner, :taggable => self)
407
+ end
366
408
  end
367
409
  end
368
410
  end
@@ -372,11 +414,12 @@ module ActiveRecord
372
414
 
373
415
  def reload_with_tag_list(*args)
374
416
  self.class.tag_types.each do |tag_type|
375
- self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
417
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
376
418
  end
377
419
 
378
420
  reload_without_tag_list(*args)
379
421
  end
422
+
380
423
  end
381
424
  end
382
425
  end
@@ -1,52 +1,60 @@
1
1
  module ActiveRecord
2
2
  module Acts
3
3
  module Tagger
4
+
4
5
  def self.included(base)
5
6
  base.extend ClassMethods
6
7
  end
7
-
8
+
8
9
  module ClassMethods
10
+
9
11
  def acts_as_tagger(opts={})
10
- has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
12
+ has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
11
13
  :include => :tag, :class_name => "Tagging")
12
14
  has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
15
+
13
16
  include ActiveRecord::Acts::Tagger::InstanceMethods
14
- extend ActiveRecord::Acts::Tagger::SingletonMethods
17
+ extend ActiveRecord::Acts::Tagger::SingletonMethods
15
18
  end
16
-
19
+
17
20
  def is_tagger?
18
21
  false
19
22
  end
23
+
20
24
  end
21
-
25
+
22
26
  module InstanceMethods
27
+
23
28
  def self.included(base)
24
29
  end
25
-
30
+
26
31
  def tag(taggable, opts={})
27
32
  opts.reverse_merge!(:force => true)
28
33
 
29
34
  return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
30
- raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
31
- raise "You need to specify some tags using :with" unless opts.has_key?(:with)
32
- raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
33
- ( opts[:force] || taggable.tag_types.include?(opts[:on]) )
35
+
36
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
37
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
38
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
34
39
 
35
40
  taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
36
41
  taggable.save
37
42
  end
38
-
43
+
39
44
  def is_tagger?
40
45
  self.class.is_tagger?
41
46
  end
47
+
42
48
  end
43
-
49
+
44
50
  module SingletonMethods
51
+
45
52
  def is_tagger?
46
53
  true
47
54
  end
55
+
48
56
  end
49
-
57
+
50
58
  end
51
59
  end
52
60
  end
@@ -2,10 +2,12 @@ module ActiveRecord
2
2
  module Acts
3
3
  module TaggableOn
4
4
  module GroupHelper
5
+
5
6
  # all column names are necessary for PostgreSQL group clause
6
7
  def grouped_column_names_for(object)
7
8
  object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
8
9
  end
10
+
9
11
  end
10
12
  end
11
13
  end