acts-as-taggable-on 0.0.0 → 1.1.2

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 (39) 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 +425 -27
  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 +19 -32
  12. data/lib/acts_as_taggable_on/tag_list.rb +19 -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 +7 -9
  18. data/spec/acts_as_taggable_on/group_helper_spec.rb +18 -0
  19. data/spec/acts_as_taggable_on/tag_spec.rb +22 -33
  20. data/spec/acts_as_taggable_on/taggable_spec.rb +55 -81
  21. data/spec/acts_as_taggable_on/tagger_spec.rb +6 -41
  22. data/spec/acts_as_taggable_on/tagging_spec.rb +5 -11
  23. data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -1
  24. data/spec/schema.rb +2 -12
  25. data/spec/spec.opts +2 -1
  26. data/spec/spec_helper.rb +39 -27
  27. metadata +10 -27
  28. data/Gemfile +0 -6
  29. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +0 -56
  30. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +0 -97
  31. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +0 -220
  32. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -29
  33. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +0 -101
  34. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +0 -64
  35. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  36. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  37. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  38. data/spec/bm.rb +0 -52
  39. 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.1.2
@@ -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,437 @@
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
58
+
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}
63
+
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
+ if respond_to?(:tag_types)
86
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
87
+ else
88
+ class_eval do
89
+ write_inheritable_attribute(:tag_types, args.uniq)
90
+ class_inheritable_reader :tag_types
91
+
92
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
93
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
94
+
95
+ attr_writer :custom_contexts
96
+
97
+ before_save :save_cached_tag_list
98
+ after_save :save_tags
99
+
100
+ if respond_to?(:named_scope)
101
+ named_scope :tagged_with, lambda{ |*args|
102
+ find_options_for_find_tagged_with(*args)
103
+ }
104
+ end
105
+ end
106
+
107
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
108
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
109
+ alias_method_chain :reload, :tag_list
110
+ end
111
+ end
112
+ end
113
+
114
+ module SingletonMethods
115
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
116
+ # Pass either a tag string, or an array of strings or tags
117
+ #
118
+ # Options:
119
+ # :any - find models that match any of the given tags
120
+ # :exclude - Find models that are not tagged with the given tags
121
+ # :match_all - Find models that match all of the given tags, not just one
122
+ # :conditions - A piece of SQL conditions to add to the query
123
+ # :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
128
+
129
+ def caching_tag_list_on?(context)
130
+ column_names.include?("cached_#{context.to_s.singularize}_list")
131
+ end
132
+
133
+ def tag_counts_on(context, options = {})
134
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
135
+ end
136
+
137
+ def all_tag_counts(options = {})
138
+ Tag.find(:all, find_options_for_tag_counts(options))
139
+ end
140
+
141
+ def find_options_for_find_tagged_with(tags, options = {})
142
+ tag_list = TagList.from(tags)
143
+
144
+ return {} if tag_list.empty?
145
+
146
+ joins = []
147
+ conditions = []
148
+
149
+ context = options.delete(:on)
150
+
151
+
152
+ if options.delete(:exclude)
153
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
154
+ 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
+
156
+ elsif options.delete(:any)
157
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
158
+ 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)})"
159
+
160
+ else
161
+ tags = Tag.named_like_any(tag_list)
162
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
163
+
164
+ tags.each do |tag|
165
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
166
+ prefix = "#{safe_tag}_#{rand(1024)}"
167
+
168
+ taggings_alias = "#{table_name}_taggings_#{prefix}"
169
+
170
+ tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
171
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
172
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
173
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
174
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
175
+
176
+ joins << tagging_join
177
+ end
178
+ end
179
+
180
+ taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
181
+
182
+ if options.delete(:match_all)
183
+ joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
184
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
185
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
186
+
187
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
188
+ end
189
+
190
+ { :joins => joins.join(" "),
191
+ :group => group,
192
+ :conditions => conditions.join(" AND "),
193
+ :readonly => false }.update(options)
194
+ end
195
+
196
+ # Calculate the tag counts for all tags.
197
+ #
198
+ # Options:
199
+ # :start_at - Restrict the tags to those created after a certain time
200
+ # :end_at - Restrict the tags to those created before a certain time
201
+ # :conditions - A piece of SQL conditions to add to the query
202
+ # :limit - The maximum number of tags to return
203
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
204
+ # :at_least - Exclude tags with a frequency less than the given value
205
+ # :at_most - Exclude tags with a frequency greater than the given value
206
+ # :on - Scope the find to only include a certain context
207
+ def find_options_for_tag_counts(options = {})
208
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
209
+
210
+ scope = scope(:find)
211
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
212
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
10
213
 
11
- def acts_as_taggable_on(*tag_types)
12
- tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
214
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
215
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
216
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
13
217
 
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
218
+ conditions = [
219
+ taggable_type,
220
+ taggable_id,
221
+ options[:conditions],
222
+ start_at,
223
+ end_at
224
+ ]
225
+
226
+ conditions = conditions.compact.join(' AND ')
227
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
228
+
229
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
230
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
231
+ joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
232
+
233
+ unless descends_from_active_record?
234
+ # Current model is STI descendant, so add type checking to the join condition
235
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
236
+ end
237
+
238
+ # Based on a proposed patch by donV to ActiveRecord Base
239
+ # This is needed because merge_joins and construct_join are private in ActiveRecord Base
240
+ if scope && scope[:joins]
241
+ case scope[:joins]
242
+ when Array
243
+ scope_joins = scope[:joins].flatten
244
+ strings = scope_joins.select{|j| j.is_a? String}
245
+ joins << strings.join(' ') + " "
246
+ symbols = scope_joins - strings
247
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, symbols, nil)
248
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
249
+ joins.flatten!
250
+ when Symbol, Hash
251
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, scope[:joins], nil)
252
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
253
+ when String
254
+ joins << scope[:joins]
255
+ end
256
+ end
257
+
258
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
259
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
260
+ having = [at_least, at_most].compact.join(' AND ')
261
+ group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
262
+ group_by << " AND #{having}" unless having.blank?
263
+
264
+ { :select => "#{Tag.table_name}.*, COUNT(*) AS count",
265
+ :joins => joins.join(" "),
266
+ :conditions => conditions,
267
+ :group => group_by,
268
+ :limit => options[:limit],
269
+ :order => options[:order]
270
+ }
271
+ end
272
+
273
+ def is_taggable?
274
+ true
275
+ end
276
+ end
277
+
278
+ module InstanceMethods
279
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
280
+
281
+ def custom_contexts
282
+ @custom_contexts ||= []
283
+ end
284
+
285
+ def is_taggable?
286
+ self.class.is_taggable?
287
+ end
288
+
289
+ def add_custom_context(value)
290
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
19
291
  end
20
292
 
21
- write_inheritable_attribute(:tag_types, tag_types)
22
- class_inheritable_reader(:tag_types)
293
+ def tag_list_on(context, owner = nil)
294
+ add_custom_context(context)
295
+ cache = tag_list_cache_on(context)
296
+ return owner ? cache[owner] : cache[owner] if cache[owner]
297
+
298
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
299
+ cache[owner] = TagList.from(cached_tag_list_on(context))
300
+ else
301
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
302
+ end
303
+ end
304
+
305
+ def all_tags_list_on(context)
306
+ variable_name = "@all_#{context.to_s.singularize}_list"
307
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
308
+ instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
309
+ end
23
310
 
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
311
+ def all_tags_on(context)
312
+ opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
313
+ base_tags.find(:all, opts.merge(:order => "#{Tagging.table_name}.created_at"))
314
+ end
27
315
 
28
- def self.taggable?
29
- true
316
+ def tags_on(context, owner = nil)
317
+ if owner
318
+ opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
319
+ context.to_s, owner.id, owner.class.to_s]}
320
+ else
321
+ opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]}
30
322
  end
323
+ base_tags.find(:all, opts)
324
+ end
325
+
326
+ def cached_tag_list_on(context)
327
+ self["cached_#{context.to_s.singularize}_list"]
328
+ end
329
+
330
+ def tag_list_cache_on(context)
331
+ variable_name = "@#{context.to_s.singularize}_list"
332
+ cache = instance_variable_get(variable_name)
333
+ instance_variable_set(variable_name, cache = {}) unless cache
334
+ cache
335
+ end
336
+
337
+ def set_tag_list_on(context, new_list, tagger = nil)
338
+ tag_list_cache_on(context)[tagger] = TagList.from(new_list)
339
+ add_custom_context(context)
340
+ end
341
+
342
+ def tag_counts_on(context, options={})
343
+ self.class.tag_counts_on(context, options.merge(:id => id))
344
+ end
345
+
346
+ def related_tags_for(context, klass, options = {})
347
+ search_conditions = related_search_options(context, klass, options)
348
+
349
+ klass.find(:all, search_conditions)
350
+ end
351
+
352
+ def related_search_options(context, klass, options = {})
353
+ tags_to_find = tags_on(context).collect { |t| t.name }
354
+
355
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
356
+
357
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
358
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
359
+ :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],
360
+ :group => grouped_column_names_for(klass),
361
+ :order => "count DESC"
362
+ }.update(options)
363
+ end
364
+
365
+ def matching_contexts_for(search_context, result_context, klass, options = {})
366
+ search_conditions = matching_context_search_options(search_context, result_context, klass, options)
367
+
368
+ klass.find(:all, search_conditions)
369
+ end
370
+
371
+ def matching_context_search_options(search_context, result_context, klass, options = {})
372
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
373
+
374
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
375
+
376
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
377
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
378
+ :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],
379
+ :group => grouped_column_names_for(klass),
380
+ :order => "count DESC"
381
+ }.update(options)
382
+ end
31
383
 
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
384
+ def save_cached_tag_list
385
+ self.class.tag_types.map(&:to_s).each do |tag_type|
386
+ if self.class.send("caching_#{tag_type.singularize}_list?")
387
+ self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).tags.join(', ')
388
+ end
389
+ end
390
+ end
391
+
392
+ def save_tags
393
+ contexts = custom_contexts + self.class.tag_types.map(&:to_s)
394
+
395
+ transaction do
396
+ contexts.each do |context|
397
+ cache = tag_list_cache_on(context)
398
+
399
+ cache.each do |owner, list|
400
+ new_tags = Tag.find_or_create_all_with_like_by_name(list.uniq)
401
+ taggings = Tagging.find(:all, :conditions => { :taggable_id => self.id, :taggable_type => self.class.to_s })
402
+
403
+ # Destroy old taggings:
404
+ if owner
405
+ old_tags = tags_on(context, owner) - new_tags
406
+ old_taggings = Tagging.find(:all, :conditions => { :taggable_id => self.id, :taggable_type => self.class.to_s, :tag_id => old_tags, :tagger_id => owner, :tagger_type => owner.class.to_s, :context => context })
407
+
408
+ old_taggings.each(&:destroy)
409
+ else
410
+ old_tags = tags_on(context) - new_tags
411
+ base_tags.delete(*old_tags)
412
+ end
413
+
414
+ new_tags.reject! { |tag| taggings.any? { |tagging| tagging.tag == tag &&
415
+ tagging.tagger == owner &&
416
+ tagging.context == context } }
417
+
418
+ # create new taggings:
419
+ new_tags.each do |tag|
420
+ Tagging.create!(:tag_id => tag.id, :context => context, :tagger => owner, :taggable => self)
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+ true
427
+ end
428
+
429
+ def reload_with_tag_list(*args)
430
+ self.class.tag_types.each do |tag_type|
431
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
432
+ end
433
+
434
+ reload_without_tag_list(*args)
37
435
  end
38
436
  end
39
437
  end