acts-as-taggable-on 0.0.0 → 1.0.19

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.
Files changed (40) hide show
  1. data/CHANGELOG +2 -5
  2. data/README.rdoc +0 -13
  3. data/Rakefile +15 -45
  4. data/VERSION +1 -1
  5. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  6. data/{lib/generators/acts_as_taggable_on/migration/templates/active_record → generators/acts_as_taggable_on_migration/templates}/migration.rb +13 -12
  7. data/lib/acts-as-taggable-on.rb +7 -30
  8. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +392 -28
  9. data/lib/acts_as_taggable_on/acts_as_tagger.rb +45 -40
  10. data/lib/acts_as_taggable_on/group_helper.rb +12 -0
  11. data/lib/acts_as_taggable_on/tag.rb +16 -43
  12. data/lib/acts_as_taggable_on/tag_list.rb +25 -24
  13. data/lib/acts_as_taggable_on/tagging.rb +7 -16
  14. data/lib/acts_as_taggable_on/tags_helper.rb +2 -6
  15. data/rails/init.rb +5 -1
  16. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +41 -100
  17. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +4 -46
  18. data/spec/acts_as_taggable_on/group_helper_spec.rb +18 -0
  19. data/spec/acts_as_taggable_on/tag_list_spec.rb +2 -2
  20. data/spec/acts_as_taggable_on/tag_spec.rb +15 -57
  21. data/spec/acts_as_taggable_on/taggable_spec.rb +79 -119
  22. data/spec/acts_as_taggable_on/tagger_spec.rb +5 -57
  23. data/spec/acts_as_taggable_on/tagging_spec.rb +5 -11
  24. data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -1
  25. data/spec/schema.rb +2 -12
  26. data/spec/spec.opts +2 -1
  27. data/spec/spec_helper.rb +39 -27
  28. metadata +10 -27
  29. data/Gemfile +0 -6
  30. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +0 -56
  31. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +0 -97
  32. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +0 -220
  33. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -29
  34. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +0 -101
  35. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +0 -64
  36. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  37. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  38. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  39. data/spec/bm.rb +0 -52
  40. data/spec/models.rb +0 -36
data/CHANGELOG CHANGED
@@ -1,6 +1,3 @@
1
- == 2010-02-17
2
- * Converted the plugin to be compatible with Rails3
3
-
4
1
  == 2009-12-02
5
2
 
6
3
  * PostgreSQL is now supported (via morgoth)
@@ -15,10 +12,10 @@
15
12
  * Removed extraneous down migration cruft (azabaj)
16
13
 
17
14
  == 2008-06-09
18
-
15
+
19
16
  * Added support for Single Table Inheritance
20
17
  * Adding gemspec and rails/init.rb for gemified plugin
21
-
18
+
22
19
  == 2007-12-12
23
20
 
24
21
  * Added ability to use dynamic tag contexts
data/README.rdoc CHANGED
@@ -31,16 +31,6 @@ To install the gem, add this to your config/environment.rb:
31
31
 
32
32
  After that, you can run "rake gems:install" to install the gem if you don't already have it.
33
33
 
34
- == Rails 3.0
35
-
36
- Acts As Taggable On is now useable in Rails 3.0, thanks to the excellent work of Szymon Nowak
37
- and Jelle Vandebeeck. Because backwards compatibility is hard to maintain, their work is available
38
- in the feature/rails3_compatibility branch.
39
-
40
- A Rails 3.0 compatible version of the gem is also available:
41
-
42
- gem install acts-as-taggable-on -v=2.0.0.pre1
43
-
44
34
  === Post Installation (Rails)
45
35
 
46
36
  1. script/generate acts_as_taggable_on_migration
@@ -99,9 +89,6 @@ also improves compatibility with the will_paginate gem:
99
89
  User.tagged_with("awesome").by_date
100
90
  User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
101
91
 
102
- #Find a user with matching all tags, not just one
103
- User.tagged_with(["awesome", "cool"], :match_all => :true)
104
-
105
92
  === Relationships
106
93
 
107
94
  You can find objects of the same type based on similar tags on certain contexts.
data/Rakefile CHANGED
@@ -1,46 +1,4 @@
1
- begin
2
- # Rspec 1.3.0
3
- require 'spec/rake/spectask'
4
-
5
- desc 'Default: run specs'
6
- task :default => :spec
7
- Spec::Rake::SpecTask.new do |t|
8
- t.spec_files = FileList["spec/**/*_spec.rb"]
9
- end
10
-
11
- Spec::Rake::SpecTask.new('rcov') do |t|
12
- t.spec_files = FileList["spec/**/*_spec.rb"]
13
- t.rcov = true
14
- t.rcov_opts = ['--exclude', 'spec']
15
- end
16
-
17
- rescue LoadError
18
- # Rspec 2.0
19
- require 'rspec/core/rake_task'
20
-
21
- desc 'Default: run specs'
22
- task :default => :spec
23
- Rspec::Core::RakeTask.new do |t|
24
- t.pattern = "spec/**/*_spec.rb"
25
- end
26
-
27
- Rspec::Core::RakeTask.new('rcov') do |t|
28
- t.pattern = "spec/**/*_spec.rb"
29
- t.rcov = true
30
- t.rcov_opts = ['--exclude', 'spec']
31
- end
32
-
33
- rescue LoadError
34
- puts "Rspec not available. Install it with: gem install rspec"
35
- end
36
-
37
- namespace 'rails2.3' do
38
- task :spec do
39
- gemfile = File.join(File.dirname(__FILE__), 'lib', 'acts_as_taggable_on', 'compatibility', 'Gemfile')
40
- ENV['BUNDLE_GEMFILE'] = gemfile
41
- Rake::Task['spec'].invoke
42
- end
43
- end
1
+ require 'spec/rake/spectask'
44
2
 
45
3
  begin
46
4
  require 'jeweler'
@@ -55,5 +13,17 @@ begin
55
13
  end
56
14
  Jeweler::GemcutterTasks.new
57
15
  rescue LoadError
58
- puts "Jeweler not available. Install it with: gem install jeweler"
59
- end
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ desc 'Default: run specs'
20
+ task :default => :spec
21
+ Spec::Rake::SpecTask.new do |t|
22
+ t.spec_files = FileList["spec/**/*_spec.rb"]
23
+ end
24
+
25
+ Spec::Rake::SpecTask.new('rcov') do |t|
26
+ t.spec_files = FileList["spec/**/*_spec.rb"]
27
+ t.rcov = true
28
+ t.rcov_opts = ['--exclude', 'spec']
29
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 1.0.19
@@ -0,0 +1,7 @@
1
+ class ActsAsTaggableOnMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_taggable_on_migration"
5
+ end
6
+ end
7
+ end
@@ -1,26 +1,27 @@
1
1
  class ActsAsTaggableOnMigration < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :tags do |t|
4
- t.string :name
4
+ t.column :name, :string
5
5
  end
6
-
6
+
7
7
  create_table :taggings do |t|
8
- t.references :tag
9
-
8
+ t.column :tag_id, :integer
9
+ t.column :taggable_id, :integer
10
+ t.column :tagger_id, :integer
11
+ t.column :tagger_type, :string
12
+
10
13
  # You should make sure that the column created is
11
14
  # long enough to store the required class names.
12
- t.references :taggable, :polymorphic => true
13
- t.references :tagger, :polymorphic => true
14
-
15
- t.string :context
16
-
17
- t.datetime :created_at
15
+ t.column :taggable_type, :string
16
+ t.column :context, :string
17
+
18
+ t.column :created_at, :datetime
18
19
  end
19
-
20
+
20
21
  add_index :taggings, :tag_id
21
22
  add_index :taggings, [:taggable_id, :taggable_type, :context]
22
23
  end
23
-
24
+
24
25
  def self.down
25
26
  drop_table :taggings
26
27
  drop_table :tags
@@ -1,30 +1,7 @@
1
- require "active_record"
2
- require "action_view"
3
-
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
5
-
6
- require "acts_as_taggable_on/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
7
-
8
- require "acts_as_taggable_on/acts_as_taggable_on"
9
- require "acts_as_taggable_on/acts_as_taggable_on/core"
10
- require "acts_as_taggable_on/acts_as_taggable_on/collection"
11
- require "acts_as_taggable_on/acts_as_taggable_on/cache"
12
- require "acts_as_taggable_on/acts_as_taggable_on/ownership"
13
- require "acts_as_taggable_on/acts_as_taggable_on/related"
14
-
15
- require "acts_as_taggable_on/acts_as_tagger"
16
- require "acts_as_taggable_on/tag"
17
- require "acts_as_taggable_on/tag_list"
18
- require "acts_as_taggable_on/tags_helper"
19
- require "acts_as_taggable_on/tagging"
20
-
21
- $LOAD_PATH.shift
22
-
23
- if defined?(ActiveRecord::Base)
24
- ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
25
- ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
26
- end
27
-
28
- if defined?(ActionView::Base)
29
- ActionView::Base.send :include, TagsHelper
30
- end
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,39 +1,403 @@
1
- module ActsAsTaggableOn
2
- module Taggable
3
- def taggable?
4
- false
5
- end
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
6
7
 
7
- def acts_as_taggable
8
- acts_as_taggable_on :tags
9
- end
8
+ module ClassMethods
9
+ def taggable?
10
+ false
11
+ end
12
+
13
+ def acts_as_taggable
14
+ acts_as_taggable_on :tags
15
+ end
16
+
17
+ def acts_as_taggable_on(*args)
18
+ args.flatten! if args
19
+ args.compact! if args
20
+ for tag_type in args
21
+ tag_type = tag_type.to_s
22
+ # use aliased_join_table_name for context condition so that sphinx can join multiple
23
+ # tag references from same model without getting an ambiguous column error
24
+ class_eval do
25
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
26
+ :include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
27
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
28
+ end
29
+
30
+ class_eval <<-RUBY
31
+ def self.taggable?
32
+ true
33
+ end
34
+
35
+ def self.caching_#{tag_type.singularize}_list?
36
+ caching_tag_list_on?("#{tag_type}")
37
+ end
38
+
39
+ def self.#{tag_type.singularize}_counts(options={})
40
+ tag_counts_on('#{tag_type}',options)
41
+ end
42
+
43
+ def #{tag_type.singularize}_list
44
+ tag_list_on('#{tag_type}')
45
+ end
46
+
47
+ def #{tag_type.singularize}_list=(new_tags)
48
+ set_tag_list_on('#{tag_type}',new_tags)
49
+ end
50
+
51
+ def #{tag_type.singularize}_counts(options = {})
52
+ tag_counts_on('#{tag_type}',options)
53
+ end
54
+
55
+ def #{tag_type}_from(owner)
56
+ tag_list_on('#{tag_type}', owner)
57
+ end
10
58
 
11
- def acts_as_taggable_on(*tag_types)
12
- tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
59
+ def find_related_#{tag_type}(options = {})
60
+ related_tags_for('#{tag_type}', self.class, options)
61
+ end
62
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
13
63
 
14
- if taggable?
15
- write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
16
- else
17
- if ::ActiveRecord::VERSION::MAJOR < 3
18
- include ActsAsTaggableOn::ActiveRecord::Backports
64
+ def find_related_#{tag_type}_for(klass, options = {})
65
+ related_tags_for('#{tag_type}', klass, options)
66
+ end
67
+
68
+ def find_matching_contexts(search_context, result_context, options = {})
69
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
70
+ end
71
+
72
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
73
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
74
+ end
75
+
76
+ def top_#{tag_type}(limit = 10)
77
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
78
+ end
79
+
80
+ def self.top_#{tag_type}(limit = 10)
81
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ if respond_to?(:tag_types)
87
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
88
+ else
89
+ class_eval do
90
+ write_inheritable_attribute(:tag_types, args.uniq)
91
+ class_inheritable_reader :tag_types
92
+
93
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
94
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
95
+
96
+ attr_writer :custom_contexts
97
+
98
+ before_save :save_cached_tag_list
99
+ after_save :save_tags
100
+
101
+ if respond_to?(:named_scope)
102
+ named_scope :tagged_with, lambda{ |*args|
103
+ find_options_for_find_tagged_with(*args)
104
+ }
105
+ end
106
+ end
107
+
108
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
109
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
110
+ alias_method_chain :reload, :tag_list
111
+ end
19
112
  end
113
+ end
20
114
 
21
- write_inheritable_attribute(:tag_types, tag_types)
22
- class_inheritable_reader(:tag_types)
23
-
24
- class_eval do
25
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
26
- has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
115
+ module SingletonMethods
116
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
117
+ # Pass either a tag string, or an array of strings or tags
118
+ #
119
+ # Options:
120
+ # :any - find models that match any of the given tags
121
+ # :exclude - Find models that are not tagged with the given tags
122
+ # :match_all - Find models that match all of the given tags, not just one
123
+ # :conditions - A piece of SQL conditions to add to the query
124
+ # :on - scopes the find to a context
125
+ def find_tagged_with(*args)
126
+ options = find_options_for_find_tagged_with(*args)
127
+ options.blank? ? [] : find(:all,options)
128
+ end
27
129
 
28
- def self.taggable?
29
- true
130
+ def caching_tag_list_on?(context)
131
+ column_names.include?("cached_#{context.to_s.singularize}_list")
132
+ end
133
+
134
+ def tag_counts_on(context, options = {})
135
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
136
+ end
137
+
138
+ def all_tag_counts(options = {})
139
+ Tag.find(:all, find_options_for_tag_counts(options))
140
+ end
141
+
142
+ def find_options_for_find_tagged_with(tags, options = {})
143
+ tag_list = TagList.from(tags)
144
+
145
+ return {} if tag_list.empty?
146
+
147
+ joins = []
148
+ conditions = []
149
+
150
+ context = options.delete(:on)
151
+
152
+
153
+ if options.delete(:exclude)
154
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
155
+ 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)})"
156
+
157
+ elsif options.delete(:any)
158
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
159
+ 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)})"
160
+
161
+ else
162
+ tags = Tag.named_like_any(tag_list)
163
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
164
+
165
+ tags.each do |tag|
166
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
167
+ prefix = "#{safe_tag}_#{rand(1024)}"
168
+
169
+ taggings_alias = "#{table_name}_taggings_#{prefix}"
170
+
171
+ tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
172
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
173
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
174
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
175
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
176
+
177
+ joins << tagging_join
178
+ end
30
179
  end
180
+
181
+ taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
182
+
183
+ if options.delete(:match_all)
184
+ joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
185
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
186
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
187
+
188
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
189
+ end
190
+
191
+ { :joins => joins.join(" "),
192
+ :group => group,
193
+ :conditions => conditions.join(" AND "),
194
+ :readonly => false }.update(options)
195
+ end
196
+
197
+ # Calculate the tag counts for all tags.
198
+ #
199
+ # Options:
200
+ # :start_at - Restrict the tags to those created after a certain time
201
+ # :end_at - Restrict the tags to those created before a certain time
202
+ # :conditions - A piece of SQL conditions to add to the query
203
+ # :limit - The maximum number of tags to return
204
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
205
+ # :at_least - Exclude tags with a frequency less than the given value
206
+ # :at_most - Exclude tags with a frequency greater than the given value
207
+ # :on - Scope the find to only include a certain context
208
+ def find_options_for_tag_counts(options = {})
209
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
210
+
211
+ scope = scope(:find)
212
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
213
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
214
+
215
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
216
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
217
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
218
+
219
+ conditions = [
220
+ taggable_type,
221
+ taggable_id,
222
+ options[:conditions],
223
+ start_at,
224
+ end_at
225
+ ]
226
+
227
+ conditions = conditions.compact.join(' AND ')
228
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
229
+
230
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
231
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
232
+ joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
233
+
234
+ unless descends_from_active_record?
235
+ # Current model is STI descendant, so add type checking to the join condition
236
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
237
+ end
238
+
239
+ # Based on a proposed patch by donV to ActiveRecord Base
240
+ # This is needed because merge_joins and construct_join are private in ActiveRecord Base
241
+ if scope && scope[:joins]
242
+ case scope[:joins]
243
+ when Array
244
+ scope_joins = scope[:joins].flatten
245
+ strings = scope_joins.select{|j| j.is_a? String}
246
+ joins << strings.join(' ') + " "
247
+ symbols = scope_joins - strings
248
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, symbols, nil)
249
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
250
+ joins.flatten!
251
+ when Symbol, Hash
252
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, scope[:joins], nil)
253
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
254
+ when String
255
+ joins << scope[:joins]
256
+ end
257
+ end
258
+
259
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
260
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
261
+ having = [at_least, at_most].compact.join(' AND ')
262
+ group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
263
+ group_by << " AND #{having}" unless having.blank?
264
+
265
+ { :select => "#{Tag.table_name}.*, COUNT(*) AS count",
266
+ :joins => joins.join(" "),
267
+ :conditions => conditions,
268
+ :group => group_by,
269
+ :limit => options[:limit],
270
+ :order => options[:order]
271
+ }
272
+ end
273
+
274
+ def is_taggable?
275
+ true
276
+ end
277
+ end
278
+
279
+ module InstanceMethods
280
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
281
+
282
+ def custom_contexts
283
+ @custom_contexts ||= []
284
+ end
285
+
286
+ def is_taggable?
287
+ self.class.is_taggable?
288
+ end
289
+
290
+ def add_custom_context(value)
291
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
292
+ end
293
+
294
+ def tag_list_on(context, owner=nil)
295
+ var_name = context.to_s.singularize + "_list"
296
+ add_custom_context(context)
297
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
298
+
299
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
300
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
301
+ else
302
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
303
+ end
304
+ end
305
+
306
+ def tags_on(context, owner=nil)
307
+ if owner
308
+ opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
309
+ context.to_s, owner.id, owner.class.to_s]}
310
+ else
311
+ opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
312
+ end
313
+ base_tags.find(:all, opts)
314
+ end
315
+
316
+ def cached_tag_list_on(context)
317
+ self["cached_#{context.to_s.singularize}_list"]
318
+ end
319
+
320
+ def set_tag_list_on(context,new_list, tagger=nil)
321
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
322
+ add_custom_context(context)
323
+ end
324
+
325
+ def tag_counts_on(context, options={})
326
+ self.class.tag_counts_on(context, options.merge(:id => id))
327
+ end
328
+
329
+ def related_tags_for(context, klass, options = {})
330
+ search_conditions = related_search_options(context, klass, options)
331
+
332
+ klass.find(:all, search_conditions)
333
+ end
334
+
335
+ def related_search_options(context, klass, options = {})
336
+ tags_to_find = tags_on(context).collect { |t| t.name }
337
+
338
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
339
+
340
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
341
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
342
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
343
+ :group => grouped_column_names_for(klass),
344
+ :order => "count DESC"
345
+ }.update(options)
346
+ end
31
347
 
32
- include ActsAsTaggableOn::Taggable::Core
33
- include ActsAsTaggableOn::Taggable::Collection
34
- include ActsAsTaggableOn::Taggable::Cache
35
- include ActsAsTaggableOn::Taggable::Ownership
36
- include ActsAsTaggableOn::Taggable::Related
348
+ def matching_contexts_for(search_context, result_context, klass, options = {})
349
+ search_conditions = matching_context_search_options(search_context, result_context, klass, options)
350
+
351
+ klass.find(:all, search_conditions)
352
+ end
353
+
354
+ def matching_context_search_options(search_context, result_context, klass, options = {})
355
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
356
+
357
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
358
+
359
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
360
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
361
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
362
+ :group => grouped_column_names_for(klass),
363
+ :order => "count DESC"
364
+ }.update(options)
365
+ end
366
+
367
+ def save_cached_tag_list
368
+ self.class.tag_types.map(&:to_s).each do |tag_type|
369
+ if self.class.send("caching_#{tag_type.singularize}_list?")
370
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
371
+ end
372
+ end
373
+ end
374
+
375
+ def save_tags
376
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
377
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
378
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
379
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
380
+ old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
381
+
382
+ transaction do
383
+ base_tags.delete(*old_tags) if old_tags.any?
384
+ new_tag_names.each do |new_tag_name|
385
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
386
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
387
+ :taggable => self, :tagger => owner)
388
+ end
389
+ end
390
+ end
391
+
392
+ true
393
+ end
394
+
395
+ def reload_with_tag_list(*args)
396
+ self.class.tag_types.each do |tag_type|
397
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
398
+ end
399
+
400
+ reload_without_tag_list(*args)
37
401
  end
38
402
  end
39
403
  end