acts-as-taggable-on 2.0.3 → 2.1.0

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 (41) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +10 -0
  3. data/Gemfile +2 -5
  4. data/Guardfile +5 -0
  5. data/README.rdoc +19 -16
  6. data/Rakefile +9 -55
  7. data/VERSION +1 -1
  8. data/acts-as-taggable-on.gemspec +27 -0
  9. data/lib/acts-as-taggable-on/version.rb +4 -0
  10. data/lib/acts-as-taggable-on.rb +8 -2
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +40 -31
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +67 -32
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +16 -12
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +17 -9
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +16 -6
  17. data/lib/acts_as_taggable_on/acts_as_tagger.rb +2 -2
  18. data/lib/acts_as_taggable_on/compatibility/Gemfile +3 -1
  19. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
  20. data/lib/acts_as_taggable_on/tag.rb +73 -57
  21. data/lib/acts_as_taggable_on/tag_list.rb +79 -78
  22. data/lib/acts_as_taggable_on/tagging.rb +19 -18
  23. data/lib/acts_as_taggable_on/tags_helper.rb +12 -12
  24. data/lib/acts_as_taggable_on/utils.rb +31 -0
  25. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +3 -2
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +25 -2
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +3 -3
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +3 -3
  29. data/spec/acts_as_taggable_on/tag_spec.rb +41 -21
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +54 -12
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +5 -5
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +7 -7
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -3
  34. data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
  35. data/spec/database.yml.sample +19 -0
  36. data/spec/models.rb +4 -0
  37. data/spec/schema.rb +6 -0
  38. data/spec/spec_helper.rb +60 -33
  39. data/uninstall.rb +1 -0
  40. metadata +130 -15
  41. /data/{spec/spec.opts → .rspec} +0 -0
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.log
2
+ *.sqlite3
3
+ /pkg/*
4
+ .bundle
5
+ .rvmrc
6
+ Gemfile.lock
7
+ spec/database.yml
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ script: "cp spec/database.yml.sample spec/database.yml && bundle install && bundle exec rake"
2
+ rvm:
3
+ - 1.8.7
4
+ - ree
5
+ - 1.9.2
6
+ - rbx
7
+ env:
8
+ - DB=sqlite3
9
+ - DB=mysql
10
+ - DB=postgresql
data/Gemfile CHANGED
@@ -1,6 +1,3 @@
1
- source :gemcutter
1
+ source 'http://rubygems.org'
2
+ gemspec
2
3
 
3
- # Rails 3.0
4
- gem 'rails', '3.0.0.beta'
5
- gem 'rspec', '2.0.0.beta.1'
6
- gem 'sqlite3-ruby', '1.2.5', :require => 'sqlite3'
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec' do
2
+ watch(%r{^spec/.+_spec\.rb})
3
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/README.rdoc CHANGED
@@ -38,21 +38,21 @@ After that, you can run "rake gems:install" to install the gem if you don't alre
38
38
  1. script/generate acts_as_taggable_on_migration
39
39
  2. rake db:migrate
40
40
 
41
- == Rails 3.0
41
+ === Rails 3.0
42
42
 
43
43
  Acts As Taggable On is now useable in Rails 3.0, thanks to the excellent work of Szymon Nowak
44
44
  and Jelle Vandebeeck.
45
45
 
46
46
  To use it, add it to your Gemfile:
47
47
 
48
- gem 'acts-as-taggable-on', '2.0.0.rc1'
48
+ gem 'acts-as-taggable-on'
49
49
 
50
- === Post Installation
50
+ ==== Post Installation
51
51
 
52
52
  1. rails generate acts_as_taggable_on:migration
53
53
  2. rake db:migrate
54
54
 
55
- = Testing
55
+ == Testing
56
56
 
57
57
  Acts As Taggable On uses RSpec for its test coverage. Inside the plugin
58
58
  directory, you can run the specs for RoR 3.0.0 with:
@@ -68,7 +68,7 @@ If you already have RSpec on your application, the specs will run while using:
68
68
  rake spec:plugins
69
69
 
70
70
 
71
- = Usage
71
+ == Usage
72
72
 
73
73
  class User < ActiveRecord::Base
74
74
  # Alias for <tt>acts_as_taggable_on :tags</tt>:
@@ -89,14 +89,14 @@ rake spec:plugins
89
89
  User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
90
90
  @frankie.skill_counts
91
91
 
92
- == Finding Tagged Objects
92
+ === Finding Tagged Objects
93
93
 
94
94
  Acts As Taggable On utilizes named_scopes to create an association for tags.
95
95
  This way you can mix and match to filter down your results, and it also improves
96
96
  compatibility with the will_paginate gem:
97
97
 
98
98
  class User < ActiveRecord::Base
99
- acts_as_taggable_on :tags
99
+ acts_as_taggable_on :tags, :skills
100
100
  named_scope :by_join_date, :order => "created_at DESC"
101
101
  end
102
102
 
@@ -108,8 +108,11 @@ compatibility with the will_paginate gem:
108
108
 
109
109
  # Find a user with any of the tags:
110
110
  User.tagged_with(["awesome", "cool"], :any => true)
111
-
112
- == Relationships
111
+
112
+ # Find a user with any of tags based on context:
113
+ User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
114
+
115
+ === Relationships
113
116
 
114
117
  You can find objects of the same type based on similar tags on certain contexts.
115
118
  Also, objects will be returned in descending order based on the total number of
@@ -128,7 +131,7 @@ matched tags.
128
131
  @bobby.find_related_skills # => [<User name="Tom">]
129
132
  @frankie.find_related_skills # => [<User name="Tom">]
130
133
 
131
- == Dynamic Tag Contexts
134
+ === Dynamic Tag Contexts
132
135
 
133
136
  In addition to the generated tag contexts in the definition, it is also possible
134
137
  to allow for dynamic tag contexts (this could be user generated tag contexts!)
@@ -139,9 +142,9 @@ to allow for dynamic tag contexts (this could be user generated tag contexts!)
139
142
  @user.save
140
143
  @user.tags_on(:customs) # => [<Tag name='same'>,...]
141
144
  @user.tag_counts_on(:customs)
142
- User.find_tagged_with("same", :on => :customs) # => [@user]
145
+ User.tagged_with("same", :on => :customs) # => [@user]
143
146
 
144
- == Tag Ownership
147
+ === Tag Ownership
145
148
 
146
149
  Tags can have owners:
147
150
 
@@ -158,7 +161,7 @@ Tags can have owners:
158
161
  @some_user.owned_tags
159
162
  @some_photo.locations_from(@some_user)
160
163
 
161
- == Tag cloud calculations
164
+ === Tag cloud calculations
162
165
 
163
166
  To construct tag clouds, the frequency of each tag needs to be calculated.
164
167
  Because we specified +acts_as_taggable_on+ on the <tt>User</tt> class, we can
@@ -174,7 +177,7 @@ Here is an example that generates a tag cloud.
174
177
  Helper:
175
178
 
176
179
  module PostsHelper
177
- include TagsHelper
180
+ include ActsAsTaggableOn::TagsHelper
178
181
  end
179
182
 
180
183
  Controller:
@@ -198,7 +201,7 @@ CSS:
198
201
  .css3 { font-size: 1.4em; }
199
202
  .css4 { font-size: 1.6em; }
200
203
 
201
- = Contributors
204
+ == Contributors
202
205
 
203
206
  * TomEric (i76) - Maintainer
204
207
  * Michael Bleigh - Original Author
@@ -208,7 +211,7 @@ CSS:
208
211
  * Pradeep Elankumaran - Taggers
209
212
  * Sinclair Bain - Patch King
210
213
 
211
- == Patch Contributors
214
+ === Patch Contributors
212
215
 
213
216
  * tristanzdunn - Related objects of other classes
214
217
  * azabaj - Fixed migrate down
data/Rakefile CHANGED
@@ -1,59 +1,13 @@
1
- begin
2
- # Rspec 1.3.0
3
- require 'spec/rake/spectask'
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup :default, :development
4
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
5
+ desc 'Default: run specs'
6
+ task :default => :spec
10
7
 
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
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = "spec/**/*_spec.rb"
43
11
  end
44
12
 
45
- begin
46
- require 'jeweler'
47
- Jeweler::Tasks.new do |gemspec|
48
- gemspec.name = "acts-as-taggable-on"
49
- gemspec.summary = "ActsAsTaggableOn is a tagging plugin for Rails that provides multiple tagging contexts on a single model."
50
- gemspec.description = "With ActsAsTaggableOn, you could tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality."
51
- gemspec.email = "michael@intridea.com"
52
- gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
53
- gemspec.authors = ["Michael Bleigh"]
54
- gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
55
- end
56
- Jeweler::GemcutterTasks.new
57
- rescue LoadError
58
- puts "Jeweler not available. Install it with: gem install jeweler"
59
- end
13
+ Bundler::GemHelper.install_tasks
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.3
1
+ 2.0.6
@@ -0,0 +1,27 @@
1
+ $:.push File.dirname(__FILE__) + '/lib'
2
+ require 'acts-as-taggable-on/version'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = %q{acts-as-taggable-on}
6
+ gem.authors = ["Michael Bleigh"]
7
+ gem.date = %q{2010-05-19}
8
+ gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
9
+ gem.summary = "Advanced tagging for Rails."
10
+ gem.email = %q{michael@intridea.com}
11
+ gem.homepage = ''
12
+
13
+ gem.add_runtime_dependency 'rails'
14
+ gem.add_development_dependency 'rspec', '~> 2.5'
15
+ gem.add_development_dependency 'sqlite3'
16
+ gem.add_development_dependency 'mysql2', '< 0.3'
17
+ gem.add_development_dependency 'pg'
18
+ gem.add_development_dependency 'guard'
19
+ gem.add_development_dependency 'guard-rspec'
20
+
21
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ gem.files = `git ls-files`.split("\n")
23
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ gem.name = "acts-as-taggable-on"
25
+ gem.require_paths = ['lib']
26
+ gem.version = ActsAsTaggableOn::VERSION
27
+ end
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn
2
+ VERSION = '2.1.0'
3
+ end
4
+
@@ -1,9 +1,13 @@
1
1
  require "active_record"
2
+ require "active_record/version"
2
3
  require "action_view"
4
+ RAILS_3 = ::ActiveRecord::VERSION::MAJOR >= 3
3
5
 
4
6
  $LOAD_PATH.unshift(File.dirname(__FILE__))
5
7
 
6
- require "acts_as_taggable_on/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
8
+ require "acts_as_taggable_on/compatibility/active_record_backports" unless RAILS_3
9
+
10
+ require "acts_as_taggable_on/utils"
7
11
 
8
12
  require "acts_as_taggable_on/acts_as_taggable_on"
9
13
  require "acts_as_taggable_on/acts_as_taggable_on/core"
@@ -12,6 +16,7 @@ require "acts_as_taggable_on/acts_as_taggable_on/cache"
12
16
  require "acts_as_taggable_on/acts_as_taggable_on/ownership"
13
17
  require "acts_as_taggable_on/acts_as_taggable_on/related"
14
18
 
19
+ #require "acts_as_taggable_on/utils"
15
20
  require "acts_as_taggable_on/acts_as_tagger"
16
21
  require "acts_as_taggable_on/tag"
17
22
  require "acts_as_taggable_on/tag_list"
@@ -20,11 +25,12 @@ require "acts_as_taggable_on/tagging"
20
25
 
21
26
  $LOAD_PATH.shift
22
27
 
28
+
23
29
  if defined?(ActiveRecord::Base)
24
30
  ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
25
31
  ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
26
32
  end
27
33
 
28
34
  if defined?(ActionView::Base)
29
- ActionView::Base.send :include, TagsHelper
35
+ ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
30
36
  end
@@ -1,8 +1,8 @@
1
1
  module ActsAsTaggableOn::Taggable
2
2
  module Cache
3
3
  def self.included(base)
4
- # Skip adding caching capabilities if no cache columns exist
5
- return unless base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
4
+ # Skip adding caching capabilities if table not exists or no cache columns exist
5
+ return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6
6
 
7
7
  base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
8
  base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
@@ -11,11 +11,11 @@ module ActsAsTaggableOn::Taggable
11
11
  before_save :save_cached_tag_list
12
12
  end
13
13
 
14
- base.intialize_acts_as_taggable_on_cache
14
+ base.initialize_acts_as_taggable_on_cache
15
15
  end
16
16
 
17
17
  module ClassMethods
18
- def intialize_acts_as_taggable_on_cache
18
+ def initialize_acts_as_taggable_on_cache
19
19
  tag_types.map(&:to_s).each do |tag_type|
20
20
  class_eval %(
21
21
  def self.caching_#{tag_type.singularize}_list?
@@ -27,7 +27,7 @@ module ActsAsTaggableOn::Taggable
27
27
 
28
28
  def acts_as_taggable_on(*args)
29
29
  super(*args)
30
- intialize_acts_as_taggable_on_cache
30
+ initialize_acts_as_taggable_on_cache
31
31
  end
32
32
 
33
33
  def caching_tag_list_on?(context)
@@ -50,4 +50,4 @@ module ActsAsTaggableOn::Taggable
50
50
  end
51
51
  end
52
52
  end
53
- end
53
+ end
@@ -61,63 +61,72 @@ module ActsAsTaggableOn::Taggable
61
61
 
62
62
  ## Generate conditions:
63
63
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
64
-
65
- start_at_conditions = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
66
- end_at_conditions = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
67
64
 
68
- taggable_conditions = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
69
- taggable_conditions << sanitize_sql([" AND #{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
70
-
71
- conditions = [
65
+ start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
66
+ end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
67
+
68
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
69
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
70
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
71
+
72
+ tagging_conditions = [
72
73
  taggable_conditions,
73
- options[:conditions],
74
74
  scope[:conditions],
75
75
  start_at_conditions,
76
76
  end_at_conditions
77
77
  ].compact.reverse
78
78
 
79
+ tag_conditions = [
80
+ options[:conditions]
81
+ ].compact.reverse
82
+
79
83
  ## Generate joins:
80
- tagging_join = "LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"
81
- tagging_join << sanitize_sql([" AND #{Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
82
-
83
- taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
84
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
84
85
  taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
85
86
 
86
- joins = [
87
- tagging_join,
87
+ tagging_joins = [
88
88
  taggable_join,
89
89
  scope[:joins]
90
- ].compact.reverse
90
+ ].compact
91
91
 
92
+ tag_joins = [
93
+ ].compact
94
+
95
+ [tagging_joins, tag_joins].each(&:reverse!) if ActiveRecord::VERSION::MAJOR < 3
92
96
 
93
97
  ## Generate scope:
94
- scope = Tag.scoped(:select => "#{Tag.table_name}.*, COUNT(*) AS count").order(options[:order]).limit(options[:limit])
95
-
98
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
99
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
100
+
96
101
  # Joins and conditions
97
- joins.each { |join| scope = scope.joins(join) }
98
- conditions.each { |condition| scope = scope.where(condition) }
99
-
102
+ tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
103
+ tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
104
+
105
+ tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
106
+ tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
107
+
100
108
  # GROUP BY and HAVING clauses:
101
- at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
102
- at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
103
- having = [at_least, at_most].compact.join(' AND ')
109
+ at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
110
+ at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
111
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
112
+
113
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
104
114
 
105
115
  if ActiveRecord::VERSION::MAJOR >= 3
106
116
  # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
107
117
  scoped_select = "#{table_name}.#{primary_key}"
108
- scope = scope.where("#{Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})")
109
-
110
- # We have having() in RoR 3.0 so use it:
111
- having = having.blank? ? "COUNT(*) > 0" : "COUNT(*) > 0 AND #{having}"
112
- scope = scope.group(grouped_column_names_for(Tag)).having(having)
118
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
119
+ group(group_columns).
120
+ having(having)
113
121
  else
114
122
  # Having is not available in 2.3.x:
115
- group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
123
+ group_by = "#{group_columns} HAVING COUNT(*) > 0"
116
124
  group_by << " AND #{having}" unless having.blank?
117
- scope = scope.group(group_by)
125
+ tagging_scope = tagging_scope.group(group_by)
118
126
  end
119
127
 
120
- scope
128
+ tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
129
+ tag_scope
121
130
  end
122
131
  end
123
132
 
@@ -1,5 +1,5 @@
1
1
  module ActsAsTaggableOn::Taggable
2
- module Core
2
+ module Core
3
3
  def self.included(base)
4
4
  base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
5
  base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
@@ -20,9 +20,9 @@ module ActsAsTaggableOn::Taggable
20
20
  context_tags = tags_type.to_sym
21
21
 
22
22
  class_eval do
23
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagging",
24
- :conditions => ['#{Tagging.table_name}.tagger_id IS NULL AND #{Tagging.table_name}.context = ?', tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag
23
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
+ has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
26
26
  end
27
27
 
28
28
  class_eval %(
@@ -66,34 +66,54 @@ module ActsAsTaggableOn::Taggable
66
66
  # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
67
67
  # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
68
68
  def tagged_with(tags, options = {})
69
- tag_list = TagList.from(tags)
69
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
70
+ empty_result = scoped(:conditions => "1 = 0")
70
71
 
71
- return {} if tag_list.empty?
72
+ return empty_result if tag_list.empty?
72
73
 
73
74
  joins = []
74
75
  conditions = []
75
76
 
76
77
  context = options.delete(:on)
78
+ alias_base_name = undecorated_table_name.gsub('.','_')
77
79
 
78
80
  if options.delete(:exclude)
79
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
80
- 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)})"
81
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
82
+ conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
81
83
 
82
84
  elsif options.delete(:any)
83
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
84
- 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)})"
85
+ # get tags, drop out if nothing returned (we need at least one)
86
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
87
+ return scoped(:conditions => "1 = 0") unless tags.length > 0
88
+
89
+ # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
90
+ # avoid ambiguous column name
91
+ taggings_context = context ? "_#{context}" : ''
92
+
93
+ #TODO: fix alias to be smaller
94
+ taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
95
+
96
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
98
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
99
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
100
+
101
+ # don't need to sanitize sql, map all ids and join with OR logic
102
+ conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
103
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
104
+
105
+ joins << tagging_join
85
106
 
86
107
  else
87
- tags = Tag.named_any(tag_list)
88
- return where("1 = 0") unless tags.length == tag_list.length
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
109
+ return empty_result unless tags.length == tag_list.length
89
110
 
90
111
  tags.each do |tag|
91
- safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
92
- prefix = "#{safe_tag}_#{rand(1024)}"
112
+ prefix = "#{tag.safe_name}_#{rand(1024)}"
93
113
 
94
- taggings_alias = "#{table_name}_taggings_#{prefix}"
114
+ taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
95
115
 
96
- tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
116
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
117
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
98
118
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
99
119
  " AND #{taggings_alias}.tag_id = #{tag.id}"
@@ -103,20 +123,23 @@ module ActsAsTaggableOn::Taggable
103
123
  end
104
124
  end
105
125
 
106
- taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
126
+ taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
107
127
 
108
128
  if options.delete(:match_all)
109
- joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
129
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
110
130
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
111
131
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
112
132
 
113
- group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
114
- end
115
133
 
134
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
135
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
136
+ end
116
137
 
117
- scoped(:joins => joins.join(" "),
138
+ scoped(:select => select_clause,
139
+ :joins => joins.join(" "),
118
140
  :group => group,
119
141
  :conditions => conditions.join(" AND "),
142
+ :order => options[:order],
120
143
  :readonly => false)
121
144
  end
122
145
 
@@ -154,7 +177,7 @@ module ActsAsTaggableOn::Taggable
154
177
 
155
178
  def tag_list_cache_on(context)
156
179
  variable_name = "@#{context.to_s.singularize}_list"
157
- instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name)))
180
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
158
181
  end
159
182
 
160
183
  def tag_list_on(context)
@@ -166,40 +189,52 @@ module ActsAsTaggableOn::Taggable
166
189
  variable_name = "@all_#{context.to_s.singularize}_list"
167
190
  return instance_variable_get(variable_name) if instance_variable_get(variable_name)
168
191
 
169
- instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
192
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
170
193
  end
171
194
 
172
195
  ##
173
196
  # Returns all tags of a given context
174
197
  def all_tags_on(context)
175
- opts = ["#{Tagging.table_name}.context = ?", context.to_s]
176
- base_tags.where(opts).order("#{Tagging.table_name}.created_at").group("#{Tagging.table_name}.tag_id").all
198
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
199
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
200
+
201
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
202
+ scope = base_tags.where(opts)
203
+
204
+ if ActsAsTaggableOn::Tag.using_postgresql?
205
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
206
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
207
+ else
208
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
209
+ end
210
+
211
+ scope.all
177
212
  end
178
213
 
179
214
  ##
180
215
  # Returns all tags that are not owned of a given context
181
216
  def tags_on(context)
182
- base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
217
+ base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
183
218
  end
184
219
 
185
220
  def set_tag_list_on(context, new_list)
186
221
  add_custom_context(context)
187
222
 
188
223
  variable_name = "@#{context.to_s.singularize}_list"
189
- instance_variable_set(variable_name, TagList.from(new_list))
224
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
190
225
  end
191
226
 
192
227
  def tagging_contexts
193
228
  custom_contexts + self.class.tag_types.map(&:to_s)
194
229
  end
195
230
 
196
- def reload
231
+ def reload(*args)
197
232
  self.class.tag_types.each do |context|
198
233
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
199
234
  instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
200
235
  end
201
236
 
202
- super
237
+ super(*args)
203
238
  end
204
239
 
205
240
  def save_tags
@@ -209,7 +244,7 @@ module ActsAsTaggableOn::Taggable
209
244
  tag_list = tag_list_cache_on(context).uniq
210
245
 
211
246
  # Find existing tags or create non-existing tags:
212
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
247
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
213
248
 
214
249
  current_tags = tags_on(context)
215
250
  old_tags = current_tags - tag_list
@@ -221,7 +256,7 @@ module ActsAsTaggableOn::Taggable
221
256
 
222
257
  if old_taggings.present?
223
258
  # Destroy old taggings:
224
- Tagging.destroy_all :id => old_taggings.map(&:id)
259
+ ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
225
260
  end
226
261
 
227
262
  # Create new taggings:
@@ -234,4 +269,4 @@ module ActsAsTaggableOn::Taggable
234
269
  end
235
270
  end
236
271
  end
237
- end
272
+ end