acts-as-taggable-on 2.0.0 → 3.2.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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +23 -0
  4. data/Appraisals +20 -0
  5. data/CHANGELOG.md +112 -0
  6. data/CONTRIBUTING.md +44 -0
  7. data/Gemfile +9 -5
  8. data/Guardfile +5 -0
  9. data/{MIT-LICENSE → LICENSE.md} +1 -1
  10. data/README.md +385 -0
  11. data/Rakefile +13 -53
  12. data/UPGRADING.md +13 -0
  13. data/acts-as-taggable-on.gemspec +38 -0
  14. data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
  15. data/db/migrate/2_add_missing_unique_indices.rb +19 -0
  16. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
  17. data/gemfiles/activerecord_3.2.gemfile +15 -0
  18. data/gemfiles/activerecord_4.0.gemfile +15 -0
  19. data/gemfiles/activerecord_4.1.gemfile +15 -0
  20. data/gemfiles/activerecord_edge.gemfile +16 -0
  21. data/lib/acts-as-taggable-on.rb +65 -22
  22. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -24
  23. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +120 -42
  24. data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +35 -0
  25. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +309 -124
  26. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +36 -0
  27. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +96 -72
  28. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +43 -36
  29. data/lib/acts_as_taggable_on/engine.rb +5 -0
  30. data/lib/acts_as_taggable_on/tag.rb +133 -63
  31. data/lib/acts_as_taggable_on/tag_list.rb +162 -81
  32. data/lib/acts_as_taggable_on/taggable.rb +101 -0
  33. data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +34 -13
  34. data/lib/acts_as_taggable_on/tagging.rb +28 -18
  35. data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
  36. data/lib/acts_as_taggable_on/utils.rb +62 -0
  37. data/lib/acts_as_taggable_on/version.rb +4 -0
  38. data/lib/acts_as_taggable_on.rb +6 -0
  39. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +189 -195
  40. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +78 -81
  41. data/spec/acts_as_taggable_on/caching_spec.rb +76 -0
  42. data/spec/acts_as_taggable_on/related_spec.rb +89 -0
  43. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +210 -0
  44. data/spec/acts_as_taggable_on/tag_list_spec.rb +149 -61
  45. data/spec/acts_as_taggable_on/tag_spec.rb +245 -65
  46. data/spec/acts_as_taggable_on/taggable_spec.rb +776 -187
  47. data/spec/acts_as_taggable_on/tagger_spec.rb +119 -70
  48. data/spec/acts_as_taggable_on/tagging_spec.rb +17 -21
  49. data/spec/acts_as_taggable_on/tags_helper_spec.rb +38 -22
  50. data/spec/acts_as_taggable_on/utils_spec.rb +15 -0
  51. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
  52. data/spec/internal/app/models/cached_model.rb +3 -0
  53. data/spec/internal/app/models/cached_model_with_array.rb +5 -0
  54. data/spec/internal/app/models/company.rb +15 -0
  55. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  56. data/spec/internal/app/models/market.rb +2 -0
  57. data/spec/internal/app/models/models.rb +90 -0
  58. data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
  59. data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
  60. data/spec/internal/app/models/other_cached_model.rb +3 -0
  61. data/spec/internal/app/models/other_taggable_model.rb +4 -0
  62. data/spec/internal/app/models/student.rb +2 -0
  63. data/spec/internal/app/models/taggable_model.rb +13 -0
  64. data/spec/internal/app/models/untaggable_model.rb +3 -0
  65. data/spec/internal/app/models/user.rb +3 -0
  66. data/spec/internal/config/database.yml.sample +19 -0
  67. data/spec/internal/db/schema.rb +97 -0
  68. data/spec/schema.rb +57 -18
  69. data/spec/spec_helper.rb +12 -47
  70. data/spec/support/array.rb +9 -0
  71. data/spec/support/database.rb +42 -0
  72. data/spec/support/database_cleaner.rb +21 -0
  73. metadata +286 -63
  74. data/CHANGELOG +0 -25
  75. data/README.rdoc +0 -221
  76. data/VERSION +0 -1
  77. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
  78. data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
  79. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
  80. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  81. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  82. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  83. data/rails/init.rb +0 -1
  84. data/spec/bm.rb +0 -52
  85. data/spec/models.rb +0 -30
  86. /data/{spec/spec.opts → .rspec} +0 -0
data/Rakefile CHANGED
@@ -1,59 +1,19 @@
1
- begin
2
- # Rspec 1.3.0
3
- require 'spec/rake/spectask'
1
+ require 'rubygems'
2
+ require 'bundler/setup'
4
3
 
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
4
+ desc 'Default: run specs'
5
+ task default: :spec
10
6
 
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"
7
+ desc 'Copy sample spec database.yml over if not exists'
8
+ task :copy_db_config do
9
+ cp 'spec/internal/config/database.yml.sample', 'spec/internal/config/database.yml'
35
10
  end
36
11
 
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
12
+ task spec: [:copy_db_config]
13
+
14
+ require 'rspec/core/rake_task'
15
+ RSpec::Core::RakeTask.new do |t|
16
+ t.pattern = 'spec/**/*_spec.rb'
43
17
  end
44
18
 
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
19
+ Bundler::GemHelper.install_tasks
data/UPGRADING.md ADDED
@@ -0,0 +1,13 @@
1
+ When upgrading
2
+
3
+ Re-run the migrations generator
4
+
5
+ rake acts_as_taggable_on_engine:install:migrations
6
+
7
+ It will create any new migrations and skip existing ones
8
+
9
+
10
+ ##Breaking changes:
11
+
12
+ - ActsAsTaggableOn::Tag is not extend with ActsAsTaggableOn::Utils anymore.
13
+ Please use ActsAsTaggableOn::Utils instead
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'acts_as_taggable_on/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'acts-as-taggable-on'
8
+ gem.version = ActsAsTaggableOn::VERSION
9
+ gem.authors = ['Michael Bleigh', 'Joost Baaij']
10
+ gem.email = %w(michael@intridea.com joost@spacebabies.nl)
11
+ 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.}
12
+ gem.summary = 'Advanced tagging for Rails.'
13
+ gem.homepage = 'https://github.com/mbleigh/acts-as-taggable-on'
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.test_files = gem.files.grep(%r{^spec/})
18
+ gem.require_paths = ['lib']
19
+ gem.required_ruby_version = '>= 1.9.3'
20
+
21
+ if File.exist?('UPGRADING.md')
22
+ gem.post_install_message = File.read('UPGRADING.md')
23
+ end
24
+
25
+ gem.add_runtime_dependency 'activerecord', ['>= 3', '< 5']
26
+ gem.add_runtime_dependency 'actionpack', ['>= 3', '< 5']
27
+
28
+ gem.add_development_dependency 'sqlite3'
29
+ gem.add_development_dependency 'mysql2'
30
+ gem.add_development_dependency 'pg'
31
+
32
+ gem.add_development_dependency 'rspec-rails' , '~> 3.0.0.beta2'
33
+ gem.add_development_dependency 'rspec-its', '~> 1.0'
34
+ gem.add_development_dependency 'rspec', '3.0.0.beta2'
35
+ gem.add_development_dependency 'ammeter', '~> 1.0'
36
+ gem.add_development_dependency 'barrier', '~> 1.0'
37
+ gem.add_development_dependency 'database_cleaner', '~> 1.2'
38
+ end
@@ -9,10 +9,12 @@ class ActsAsTaggableOnMigration < ActiveRecord::Migration
9
9
 
10
10
  # You should make sure that the column created is
11
11
  # long enough to store the required class names.
12
- t.references :taggable, :polymorphic => true
13
- t.references :tagger, :polymorphic => true
12
+ t.references :taggable, polymorphic: true
13
+ t.references :tagger, polymorphic: true
14
14
 
15
- t.string :context
15
+ # Limit is created to prevent MySQL error on index
16
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
17
+ t.string :context, limit: 128
16
18
 
17
19
  t.datetime :created_at
18
20
  end
@@ -0,0 +1,19 @@
1
+ class AddMissingUniqueIndices < ActiveRecord::Migration
2
+ def self.up
3
+ add_index :tags, :name, unique: true
4
+
5
+ remove_index :taggings, :tag_id
6
+ remove_index :taggings, [:taggable_id, :taggable_type, :context]
7
+ add_index :taggings,
8
+ [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
9
+ unique: true, name: 'taggings_idx'
10
+ end
11
+
12
+ def self.down
13
+ remove_index :tags, :name
14
+
15
+ remove_index :taggings, name: 'taggings_idx'
16
+ add_index :taggings, :tag_id
17
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :tags, :taggings_count, :integer, default: 0
4
+
5
+ ActsAsTaggableOn::Tag.reset_column_information
6
+ ActsAsTaggableOn::Tag.find_each do |tag|
7
+ ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ remove_column :tags, :taggings_count
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 3.2"
6
+ gem "actionpack", "~> 3.2"
7
+
8
+ group :local_development do
9
+ gem "guard"
10
+ gem "guard-rspec"
11
+ gem "appraisal"
12
+ gem "rake"
13
+ end
14
+
15
+ gemspec :path => "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0"
6
+ gem "actionpack", "~> 4.0"
7
+
8
+ group :local_development do
9
+ gem "guard"
10
+ gem "guard-rspec"
11
+ gem "appraisal"
12
+ gem "rake"
13
+ end
14
+
15
+ gemspec :path => "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.1"
6
+ gem "actionpack", "~> 4.1"
7
+
8
+ group :local_development do
9
+ gem "guard"
10
+ gem "guard-rspec"
11
+ gem "appraisal"
12
+ gem "rake"
13
+ end
14
+
15
+ gemspec :path => "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails"
6
+ gem "actionpack", :github => "rails/rails"
7
+ gem "arel", :github => "rails/arel"
8
+
9
+ group :local_development do
10
+ gem "guard"
11
+ gem "guard-rspec"
12
+ gem "appraisal"
13
+ gem "rake"
14
+ end
15
+
16
+ gemspec :path => "../"
@@ -1,30 +1,73 @@
1
- require "active_record"
2
- require "action_view"
1
+ require 'active_record'
2
+ require 'active_record/version'
3
+ require 'active_support/core_ext/module'
4
+ require 'action_view'
3
5
 
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'digest/sha1'
5
7
 
6
- require "acts_as_taggable_on/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
8
+ module ActsAsTaggableOn
9
+ class DuplicateTagError < StandardError
10
+ end
7
11
 
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"
12
+ def self.setup
13
+ @configuration ||= Configuration.new
14
+ yield @configuration if block_given?
15
+ end
14
16
 
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"
17
+ def self.method_missing(method_name, *args, &block)
18
+ @configuration.respond_to?(method_name) ?
19
+ @configuration.send(method_name, *args, &block) : super
20
+ end
20
21
 
21
- $LOAD_PATH.shift
22
+ def self.respond_to?(method_name, include_private=false)
23
+ @configuration.respond_to? method_name
24
+ end
22
25
 
23
- if defined?(ActiveRecord::Base)
24
- ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
25
- ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
26
+ def self.glue
27
+ setting = @configuration.delimiter
28
+ delimiter = setting.kind_of?(Array) ? setting[0] : setting
29
+ delimiter.ends_with?(' ') ? delimiter : "#{delimiter} "
30
+ end
31
+
32
+ class Configuration
33
+ attr_accessor :delimiter, :force_lowercase, :force_parameterize,
34
+ :strict_case_match, :remove_unused_tags
35
+
36
+ def initialize
37
+ @delimiter = ','
38
+ @force_lowercase = false
39
+ @force_parameterize = false
40
+ @strict_case_match = false
41
+ @remove_unused_tags = false
42
+ end
43
+ end
44
+
45
+ setup
26
46
  end
27
47
 
28
- if defined?(ActionView::Base)
29
- ActionView::Base.send :include, TagsHelper
30
- end
48
+ require 'acts_as_taggable_on/utils'
49
+
50
+ require 'acts_as_taggable_on/taggable'
51
+ require 'acts_as_taggable_on/acts_as_taggable_on/compatibility'
52
+ require 'acts_as_taggable_on/acts_as_taggable_on/core'
53
+ require 'acts_as_taggable_on/acts_as_taggable_on/collection'
54
+ require 'acts_as_taggable_on/acts_as_taggable_on/cache'
55
+ require 'acts_as_taggable_on/acts_as_taggable_on/ownership'
56
+ require 'acts_as_taggable_on/acts_as_taggable_on/related'
57
+ require 'acts_as_taggable_on/acts_as_taggable_on/dirty'
58
+
59
+ require 'acts_as_taggable_on/tagger'
60
+ require 'acts_as_taggable_on/tag'
61
+ require 'acts_as_taggable_on/tag_list'
62
+ require 'acts_as_taggable_on/tags_helper'
63
+ require 'acts_as_taggable_on/tagging'
64
+ require 'acts_as_taggable_on/engine'
65
+
66
+ ActiveSupport.on_load(:active_record) do
67
+ extend ActsAsTaggableOn::Compatibility
68
+ extend ActsAsTaggableOn::Taggable
69
+ include ActsAsTaggableOn::Tagger
70
+ end
71
+ ActiveSupport.on_load(:action_view) do
72
+ include ActsAsTaggableOn::TagsHelper
73
+ end
@@ -1,53 +1,82 @@
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") }
6
-
7
- base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
- base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9
-
10
- base.class_eval do
11
- before_save :save_cached_tag_list
4
+ # When included, conditionally adds tag caching methods when the model
5
+ # has any "cached_#{tag_type}_list" column
6
+ base.instance_eval do
7
+ # @private
8
+ def _has_tags_cache_columns?(db_columns)
9
+ db_column_names = db_columns.map(&:name)
10
+ tag_types.any? do |context|
11
+ db_column_names.include?("cached_#{context.to_s.singularize}_list")
12
+ end
13
+ end
14
+
15
+ # @private
16
+ def _add_tags_caching_methods
17
+ send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
18
+ extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
19
+
20
+ before_save :save_cached_tag_list
21
+
22
+ initialize_tags_cache
23
+ end
24
+
25
+ # ActiveRecord::Base.columns makes a database connection and caches the
26
+ # calculated columns hash for the record as @columns. Since we don't
27
+ # want to add caching methods until we confirm the presence of a
28
+ # caching column, and we don't want to force opening a database
29
+ # connection when the class is loaded, here we intercept and cache
30
+ # the call to :columns as @acts_as_taggable_on_cache_columns
31
+ # to mimic the underlying behavior. While processing this first
32
+ # call to columns, we do the caching column check and dynamically add
33
+ # the class and instance methods
34
+ # FIXME: this method cannot compile in rubinius
35
+ def columns
36
+ @acts_as_taggable_on_cache_columns ||= begin
37
+ db_columns = super
38
+ _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
39
+ db_columns
40
+ end
41
+ end
42
+
12
43
  end
13
-
14
- base.intialize_acts_as_taggable_on_cache
15
44
  end
16
-
45
+
17
46
  module ClassMethods
18
- def intialize_acts_as_taggable_on_cache
47
+ def initialize_tags_cache
19
48
  tag_types.map(&:to_s).each do |tag_type|
20
- class_eval %(
49
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
21
50
  def self.caching_#{tag_type.singularize}_list?
22
51
  caching_tag_list_on?("#{tag_type}")
23
- end
24
- )
25
- end
52
+ end
53
+ RUBY
54
+ end
26
55
  end
27
-
56
+
28
57
  def acts_as_taggable_on(*args)
29
58
  super(*args)
30
- intialize_acts_as_taggable_on_cache
59
+ initialize_tags_cache
31
60
  end
32
-
61
+
33
62
  def caching_tag_list_on?(context)
34
63
  column_names.include?("cached_#{context.to_s.singularize}_list")
35
64
  end
36
65
  end
37
-
38
- module InstanceMethods
66
+
67
+ module InstanceMethods
39
68
  def save_cached_tag_list
40
69
  tag_types.map(&:to_s).each do |tag_type|
41
70
  if self.class.send("caching_#{tag_type.singularize}_list?")
42
71
  if tag_list_cache_set_on(tag_type)
43
- list = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
72
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
44
73
  self["cached_#{tag_type.singularize}_list"] = list
45
74
  end
46
75
  end
47
76
  end
48
-
77
+
49
78
  true
50
79
  end
51
80
  end
52
81
  end
53
- end
82
+ end
@@ -1,15 +1,14 @@
1
1
  module ActsAsTaggableOn::Taggable
2
2
  module Collection
3
3
  def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
5
4
  base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
6
5
  base.initialize_acts_as_taggable_on_collection
7
6
  end
8
-
7
+
9
8
  module ClassMethods
10
9
  def initialize_acts_as_taggable_on_collection
11
10
  tag_types.map(&:to_s).each do |tag_type|
12
- class_eval %(
11
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
12
  def self.#{tag_type.singularize}_counts(options={})
14
13
  tag_counts_on('#{tag_type}', options)
15
14
  end
@@ -19,25 +18,62 @@ module ActsAsTaggableOn::Taggable
19
18
  end
20
19
 
21
20
  def top_#{tag_type}(limit = 10)
22
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
21
+ tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
23
22
  end
24
23
 
25
24
  def self.top_#{tag_type}(limit = 10)
26
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
27
- end
28
- )
29
- end
25
+ tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
26
+ end
27
+ RUBY
28
+ end
30
29
  end
31
-
30
+
32
31
  def acts_as_taggable_on(*args)
33
32
  super(*args)
34
33
  initialize_acts_as_taggable_on_collection
35
34
  end
36
-
35
+
37
36
  def tag_counts_on(context, options = {})
38
- all_tag_counts(options.merge({:on => context.to_s}))
37
+ all_tag_counts(options.merge({on: context.to_s}))
38
+ end
39
+
40
+ def tags_on(context, options = {})
41
+ all_tags(options.merge({on: context.to_s}))
39
42
  end
40
-
43
+
44
+ ##
45
+ # Calculate the tag names.
46
+ # To be used when you don't need tag counts and want to avoid the taggable joins.
47
+ #
48
+ # @param [Hash] options Options:
49
+ # * :start_at - Restrict the tags to those created after a certain time
50
+ # * :end_at - Restrict the tags to those created before a certain time
51
+ # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
52
+ # * :limit - The maximum number of tags to return
53
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
54
+ # * :on - Scope the find to only include a certain context
55
+ def all_tags(options = {})
56
+ options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
57
+
58
+ ## Generate conditions:
59
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
60
+
61
+ ## Generate scope:
62
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
63
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
64
+
65
+ # Joins and conditions
66
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
67
+ tag_scope = tag_scope.where(options[:conditions])
68
+
69
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
70
+
71
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
72
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
73
+
74
+ tag_scope_joins(tag_scope, tagging_scope)
75
+ end
76
+
41
77
  ##
42
78
  # Calculate the tag counts for all tags.
43
79
  #
@@ -53,46 +89,88 @@ module ActsAsTaggableOn::Taggable
53
89
  def all_tag_counts(options = {})
54
90
  options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
55
91
 
56
- start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
57
- end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
58
-
59
- taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
60
- taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
92
+ ## Generate conditions:
61
93
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
62
94
 
63
- conditions = [
64
- taggable_type,
65
- taggable_id,
66
- options[:conditions],
67
- start_at,
68
- end_at
69
- ]
95
+ ## Generate joins:
96
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
97
+ 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
98
+
99
+ ## Generate scope:
100
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
101
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
102
+
103
+ # Joins and conditions
104
+ tagging_scope = tagging_scope.joins(taggable_join)
105
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
106
+ tag_scope = tag_scope.where(options[:conditions])
107
+
108
+ # GROUP BY and HAVING clauses:
109
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
110
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
111
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
112
+ having = having.compact.join(' AND ')
113
+
114
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
115
+
116
+ unless options[:id]
117
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
118
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
119
+ end
120
+
121
+ tagging_scope = tagging_scope.group(group_columns).having(having)
122
+
123
+ tag_scope_joins(tag_scope, tagging_scope)
124
+ end
70
125
 
71
- conditions = conditions.compact.join(' AND ')
126
+ def safe_to_sql(relation)
127
+ connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql
128
+ end
72
129
 
73
- joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
74
- joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
75
- joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
130
+ private
76
131
 
77
- unless descends_from_active_record?
78
- # Current model is STI descendant, so add type checking to the join condition
79
- joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
132
+ def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
133
+ table_name_pkey = "#{table_name}.#{primary_key}"
134
+ if ActsAsTaggableOn::Utils.using_mysql?
135
+ # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
136
+ scoped_ids = select(table_name_pkey).map(&:id)
137
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
138
+ else
139
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
80
140
  end
81
141
 
82
- at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
83
- at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
84
- having = [at_least, at_most].compact.join(' AND ')
85
- group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
86
- group_by << " AND #{having}" unless having.blank?
142
+ tagging_scope
143
+ end
144
+
145
+ def tagging_conditions(options)
146
+ tagging_conditions = []
147
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
148
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
149
+
150
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
151
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
152
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
153
+
154
+ tagging_conditions.push taggable_conditions
87
155
 
88
- Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(joins.join(" ")).where(conditions).group(group_by).limit(options[:limit]).order(options[:order])
156
+ tagging_conditions
157
+ end
158
+
159
+ def tag_scope_joins(tag_scope, tagging_scope)
160
+ tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
161
+ tag_scope.extending(CalculationMethods)
89
162
  end
90
163
  end
91
-
92
- module InstanceMethods
93
- def tag_counts_on(context, options={})
94
- self.class.tag_counts_on(context, options.merge(:id => id))
164
+
165
+ def tag_counts_on(context, options={})
166
+ self.class.tag_counts_on(context, options.merge(id: id))
167
+ end
168
+
169
+ module CalculationMethods
170
+ def count(column_name=:all)
171
+ # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
172
+ super
95
173
  end
96
174
  end
97
175
  end
98
- end
176
+ end