acts-as-taggable-on 4.0.0 → 8.0.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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/spec.yml +95 -0
  3. data/.gitignore +1 -1
  4. data/Appraisals +12 -10
  5. data/CHANGELOG.md +206 -71
  6. data/CONTRIBUTING.md +13 -0
  7. data/Gemfile +1 -1
  8. data/README.md +79 -13
  9. data/acts-as-taggable-on.gemspec +2 -6
  10. data/db/migrate/1_acts_as_taggable_on_migration.rb +14 -8
  11. data/db/migrate/2_add_missing_unique_indices.rb +14 -9
  12. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +9 -4
  13. data/db/migrate/4_add_missing_taggable_index.rb +8 -3
  14. data/db/migrate/5_change_collation_for_tag_names.rb +7 -2
  15. data/db/migrate/6_add_missing_indexes_on_taggings.rb +22 -0
  16. data/db/migrate/7_add_tenant_to_taggings.rb +16 -0
  17. data/gemfiles/activerecord_5.0.gemfile +11 -5
  18. data/gemfiles/activerecord_5.1.gemfile +21 -0
  19. data/gemfiles/activerecord_5.2.gemfile +21 -0
  20. data/gemfiles/activerecord_6.0.gemfile +21 -0
  21. data/gemfiles/activerecord_6.1.gemfile +23 -0
  22. data/lib/acts-as-taggable-on.rb +6 -2
  23. data/lib/acts_as_taggable_on/tag.rb +23 -23
  24. data/lib/acts_as_taggable_on/tag_list.rb +1 -0
  25. data/lib/acts_as_taggable_on/taggable.rb +18 -1
  26. data/lib/acts_as_taggable_on/taggable/cache.rb +38 -34
  27. data/lib/acts_as_taggable_on/taggable/collection.rb +9 -7
  28. data/lib/acts_as_taggable_on/taggable/core.rb +49 -179
  29. data/lib/acts_as_taggable_on/taggable/ownership.rb +16 -5
  30. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  31. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -0
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +16 -0
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +111 -0
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +70 -0
  35. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +82 -0
  36. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +61 -0
  37. data/lib/acts_as_taggable_on/tagger.rb +3 -3
  38. data/lib/acts_as_taggable_on/tagging.rb +9 -4
  39. data/lib/acts_as_taggable_on/utils.rb +4 -4
  40. data/lib/acts_as_taggable_on/version.rb +1 -2
  41. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +4 -12
  42. data/spec/acts_as_taggable_on/caching_spec.rb +34 -10
  43. data/spec/acts_as_taggable_on/{taggable/dirty_spec.rb → dirty_spec.rb} +28 -13
  44. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +28 -8
  45. data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
  46. data/spec/acts_as_taggable_on/taggable_spec.rb +22 -15
  47. data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
  48. data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
  49. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +2 -0
  50. data/spec/internal/app/models/cached_model_with_array.rb +6 -0
  51. data/spec/internal/app/models/columns_override_model.rb +5 -0
  52. data/spec/internal/app/models/company.rb +1 -1
  53. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  54. data/spec/internal/app/models/market.rb +1 -1
  55. data/spec/internal/app/models/non_standard_id_taggable_model.rb +1 -1
  56. data/spec/internal/app/models/student.rb +2 -0
  57. data/spec/internal/app/models/taggable_model.rb +3 -0
  58. data/spec/internal/app/models/user.rb +1 -1
  59. data/spec/internal/config/database.yml.sample +4 -8
  60. data/spec/internal/db/schema.rb +17 -5
  61. data/spec/spec_helper.rb +0 -1
  62. data/spec/support/database.rb +4 -4
  63. metadata +29 -68
  64. data/.travis.yml +0 -36
  65. data/UPGRADING.md +0 -8
  66. data/db/migrate/6_add_missing_indexes.rb +0 -12
  67. data/gemfiles/activerecord_4.0.gemfile +0 -16
  68. data/gemfiles/activerecord_4.1.gemfile +0 -16
  69. data/gemfiles/activerecord_4.2.gemfile +0 -15
  70. data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
  71. data/spec/internal/app/models/models.rb +0 -90
@@ -0,0 +1,21 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.2.0"
4
+ case ENV["DB"]
5
+ when "postgresql"
6
+ gem 'pg'
7
+ when "mysql"
8
+ gem 'mysql2', '~> 0.3'
9
+ else
10
+ gem "sqlite3", "~> 1.3", "< 1.4"
11
+ end
12
+
13
+ group :local_development do
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem "appraisal"
17
+ gem "rake"
18
+ gem "byebug", platforms: [:mri]
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 6.0.0"
4
+ case ENV["DB"]
5
+ when "postgresql"
6
+ gem 'pg'
7
+ when "mysql"
8
+ gem 'mysql2', '~> 0.4'
9
+ else
10
+ gem 'sqlite3'
11
+ end
12
+
13
+ group :local_development do
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem "appraisal"
17
+ gem "rake"
18
+ gem "byebug", platforms: [:mri]
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,23 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1.0"
6
+ case ENV["DB"]
7
+ when "postgresql"
8
+ gem 'pg'
9
+ when "mysql"
10
+ gem 'mysql2', '~> 0.5'
11
+ else
12
+ gem 'sqlite3'
13
+ end
14
+
15
+ group :local_development do
16
+ gem "guard"
17
+ gem "guard-rspec"
18
+ gem "appraisal"
19
+ gem "rake"
20
+ gem "byebug", platforms: [:mri]
21
+ end
22
+
23
+ gemspec path: "../"
@@ -31,6 +31,7 @@ module ActsAsTaggableOn
31
31
  autoload :Dirty
32
32
  autoload :Ownership
33
33
  autoload :Related
34
+ autoload :TagListType
34
35
  end
35
36
 
36
37
  autoload :Utils
@@ -57,13 +58,14 @@ module ActsAsTaggableOn
57
58
  def self.glue
58
59
  setting = @configuration.delimiter
59
60
  delimiter = setting.kind_of?(Array) ? setting[0] : setting
60
- delimiter.ends_with?(' ') ? delimiter : "#{delimiter} "
61
+ delimiter.end_with?(' ') ? delimiter : "#{delimiter} "
61
62
  end
62
63
 
63
64
  class Configuration
64
65
  attr_accessor :force_lowercase, :force_parameterize,
65
66
  :remove_unused_tags, :default_parser,
66
- :tags_counter
67
+ :tags_counter, :tags_table,
68
+ :taggings_table
67
69
  attr_reader :delimiter, :strict_case_match
68
70
 
69
71
  def initialize
@@ -75,6 +77,8 @@ module ActsAsTaggableOn
75
77
  @tags_counter = true
76
78
  @default_parser = DefaultParser
77
79
  @force_binary_collation = false
80
+ @tags_table = :tags
81
+ @taggings_table = :taggings
78
82
  end
79
83
 
80
84
  def strict_case_match=(force_cs)
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  module ActsAsTaggableOn
3
3
  class Tag < ::ActiveRecord::Base
4
+ self.table_name = ActsAsTaggableOn.tags_table
4
5
 
5
6
  ### ASSOCIATIONS:
6
7
 
@@ -9,7 +10,7 @@ module ActsAsTaggableOn
9
10
  ### VALIDATIONS:
10
11
 
11
12
  validates_presence_of :name
12
- validates_uniqueness_of :name, if: :validates_name_uniqueness?
13
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
13
14
  validates_length_of :name, maximum: 255
14
15
 
15
16
  # monkey patch this method if don't need name uniqueness validation
@@ -50,8 +51,14 @@ module ActsAsTaggableOn
50
51
 
51
52
  def self.for_context(context)
52
53
  joins(:taggings).
53
- where(["taggings.context = ?", context]).
54
- select("DISTINCT tags.*")
54
+ where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
55
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
56
+ end
57
+
58
+ def self.for_tenant(tenant)
59
+ joins(:taggings).
60
+ where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s).
61
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
55
62
  end
56
63
 
57
64
  ### CLASS METHODS:
@@ -70,16 +77,19 @@ module ActsAsTaggableOn
70
77
  return [] if list.empty?
71
78
 
72
79
  existing_tags = named_any(list)
73
-
74
80
  list.map do |tag_name|
75
- comparable_tag_name = comparable_name(tag_name)
76
- existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
77
81
  begin
82
+ tries ||= 3
83
+ comparable_tag_name = comparable_name(tag_name)
84
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
78
85
  existing_tag || create(name: tag_name)
79
86
  rescue ActiveRecord::RecordNotUnique
80
- # Postgres aborts the current transaction with
81
- # PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
82
- # so we have to rollback this transaction
87
+ if (tries -= 1).positive?
88
+ ActiveRecord::Base.connection.execute 'ROLLBACK'
89
+ existing_tags = named_any(list)
90
+ retry
91
+ end
92
+
83
93
  raise DuplicateTagError.new("'#{tag_name}' has already been taken")
84
94
  end
85
95
  end
@@ -101,8 +111,6 @@ module ActsAsTaggableOn
101
111
 
102
112
  class << self
103
113
 
104
-
105
-
106
114
  private
107
115
 
108
116
  def comparable_name(str)
@@ -117,20 +125,12 @@ module ActsAsTaggableOn
117
125
  ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
118
126
  end
119
127
 
120
- def unicode_downcase(string)
121
- if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
122
- ActiveSupport::Multibyte::Unicode.downcase(string)
123
- else
124
- ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
125
- end
128
+ def as_8bit_ascii(string)
129
+ string.to_s.mb_chars
126
130
  end
127
131
 
128
- def as_8bit_ascii(string)
129
- if defined?(Encoding)
130
- string.to_s.dup.force_encoding('BINARY')
131
- else
132
- string.to_s.mb_chars
133
- end
132
+ def unicode_downcase(string)
133
+ as_8bit_ascii(string).downcase
134
134
  end
135
135
 
136
136
  def sanitize_sql_for_named_any(tag)
@@ -41,6 +41,7 @@ module ActsAsTaggableOn
41
41
  # Appends the elements of +other_tag_list+ to +self+.
42
42
  def concat(other_tag_list)
43
43
  super(other_tag_list).send(:clean!)
44
+ self
44
45
  end
45
46
 
46
47
  ##
@@ -54,6 +54,23 @@ module ActsAsTaggableOn
54
54
  taggable_on(true, tag_types)
55
55
  end
56
56
 
57
+ def acts_as_taggable_tenant(tenant)
58
+ if taggable?
59
+ self.tenant_column = tenant
60
+ else
61
+ class_attribute :tenant_column
62
+ self.tenant_column = tenant
63
+ end
64
+
65
+ # each of these add context-specific methods and must be
66
+ # called on each call of taggable_on
67
+ include Core
68
+ include Collection
69
+ include Cache
70
+ include Ownership
71
+ include Related
72
+ end
73
+
57
74
  private
58
75
 
59
76
  # Make a model taggable on specified contexts
@@ -78,6 +95,7 @@ module ActsAsTaggableOn
78
95
  self.tag_types = tag_types
79
96
  class_attribute :preserve_tag_order
80
97
  self.preserve_tag_order = preserve_tag_order
98
+ class_attribute :tenant_column
81
99
 
82
100
  class_eval do
83
101
  has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
@@ -96,7 +114,6 @@ module ActsAsTaggableOn
96
114
  include Cache
97
115
  include Ownership
98
116
  include Related
99
- include Dirty
100
117
  end
101
118
  end
102
119
  end
@@ -3,47 +3,51 @@ module ActsAsTaggableOn::Taggable
3
3
  def self.included(base)
4
4
  # When included, conditionally adds tag caching methods when the model
5
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
6
+ base.extend Columns
7
+ end
8
+
9
+ module Columns
10
+ # ActiveRecord::Base.columns makes a database connection and caches the
11
+ # calculated columns hash for the record as @columns. Since we don't
12
+ # want to add caching methods until we confirm the presence of a
13
+ # caching column, and we don't want to force opening a database
14
+ # connection when the class is loaded, here we intercept and cache
15
+ # the call to :columns as @acts_as_taggable_on_cache_columns
16
+ # to mimic the underlying behavior. While processing this first
17
+ # call to columns, we do the caching column check and dynamically add
18
+ # the class and instance methods
19
+ # FIXME: this method cannot compile in rubinius
20
+ def columns
21
+ @acts_as_taggable_on_cache_columns ||= begin
22
+ db_columns = super
23
+ _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
24
+ db_columns
13
25
  end
26
+ end
14
27
 
15
- # @private
16
- def _add_tags_caching_methods
17
- send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
18
- extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
28
+ def reset_column_information
29
+ super
30
+ @acts_as_taggable_on_cache_columns = nil
31
+ end
19
32
 
20
- before_save :save_cached_tag_list
33
+ private
21
34
 
22
- initialize_tags_cache
35
+ # @private
36
+ def _has_tags_cache_columns?(db_columns)
37
+ db_column_names = db_columns.map(&:name)
38
+ tag_types.any? do |context|
39
+ db_column_names.include?("cached_#{context.to_s.singularize}_list")
23
40
  end
41
+ end
24
42
 
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
43
+ # @private
44
+ def _add_tags_caching_methods
45
+ send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
46
+ extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
42
47
 
43
- def reset_column_information
44
- super
45
- @acts_as_taggable_on_cache_columns = nil
46
- end
48
+ before_save :save_cached_tag_list
49
+
50
+ initialize_tags_cache
47
51
  end
48
52
  end
49
53
 
@@ -94,16 +94,18 @@ module ActsAsTaggableOn::Taggable
94
94
  ## Generate conditions:
95
95
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
96
96
 
97
- ## Generate joins:
98
- taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
99
- 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
100
-
101
97
  ## Generate scope:
102
98
  tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
103
99
  tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
104
100
 
105
- # Joins and conditions
106
- tagging_scope = tagging_scope.joins(taggable_join)
101
+ # Current model is STI descendant, so add type checking to the join condition
102
+ unless descends_from_active_record?
103
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
104
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
105
+ tagging_scope = tagging_scope.joins(taggable_join)
106
+ end
107
+
108
+ # Conditions
107
109
  tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
108
110
  tag_scope = tag_scope.where(options[:conditions])
109
111
 
@@ -174,7 +176,7 @@ module ActsAsTaggableOn::Taggable
174
176
  # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
175
177
  def count(column_name = :all, options = {})
176
178
  # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
177
- ActsAsTaggableOn::Utils.active_record5? ? super(column_name) : super(column_name, options)
179
+ super(column_name)
178
180
  end
179
181
  end
180
182
  end
@@ -1,5 +1,9 @@
1
+ require_relative 'tagged_with_query'
2
+ require_relative 'tag_list_type'
3
+
1
4
  module ActsAsTaggableOn::Taggable
2
5
  module Core
6
+
3
7
  def self.included(base)
4
8
  base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
5
9
 
@@ -25,13 +29,17 @@ module ActsAsTaggableOn::Taggable
25
29
  # the associations tag_taggings & tags are always returned in created order
26
30
  has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
27
31
  as: :taggable,
28
- class_name: ActsAsTaggableOn::Tagging,
29
- dependent: :destroy
32
+ class_name: 'ActsAsTaggableOn::Tagging',
33
+ dependent: :destroy,
34
+ after_add: :dirtify_tag_list,
35
+ after_remove: :dirtify_tag_list
30
36
 
31
37
  has_many context_tags, -> { order(taggings_order) },
32
- class_name: ActsAsTaggableOn::Tag,
38
+ class_name: 'ActsAsTaggableOn::Tag',
33
39
  through: context_taggings,
34
40
  source: :tag
41
+
42
+ attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
35
43
  end
36
44
 
37
45
  taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -40,12 +48,30 @@ module ActsAsTaggableOn::Taggable
40
48
  end
41
49
 
42
50
  def #{tag_type}_list=(new_tags)
51
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_tags).parse
52
+
53
+ if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
54
+ if ActsAsTaggableOn::Utils.legacy_activerecord?
55
+ set_attribute_was("#{tag_type}_list", #{tag_type}_list)
56
+ else
57
+ unless #{tag_type}_list_changed?
58
+ @attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, ActsAsTaggableOn::Taggable::TagListType.new)
59
+ end
60
+ end
61
+ write_attribute("#{tag_type}_list", parsed_new_list)
62
+ end
63
+
43
64
  set_tag_list_on('#{tags_type}', new_tags)
44
65
  end
45
66
 
46
67
  def all_#{tags_type}_list
47
68
  all_tags_list_on('#{tags_type}')
48
69
  end
70
+
71
+ private
72
+ def dirtify_tag_list(tagging)
73
+ attribute_will_change! tagging.context.singularize+"_list"
74
+ end
49
75
  RUBY
50
76
  end
51
77
  end
@@ -84,173 +110,19 @@ module ActsAsTaggableOn::Taggable
84
110
  def tagged_with(tags, options = {})
85
111
  tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
86
112
  options = options.dup
87
- empty_result = where('1 = 0')
88
-
89
- return empty_result if tag_list.empty?
90
-
91
- joins = []
92
- conditions = []
93
- having = []
94
- select_clause = []
95
- order_by = []
96
-
97
- context = options.delete(:on)
98
- owned_by = options.delete(:owned_by)
99
- alias_base_name = undecorated_table_name.gsub('.', '_')
100
- # FIXME use ActiveRecord's connection quote_column_name
101
- quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
102
-
103
- if options.delete(:exclude)
104
- if options.delete(:wild)
105
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(t)}%"]) }.join(' OR ')
106
- else
107
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
108
- end
109
-
110
- 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}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"
111
-
112
- if owned_by
113
- joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
114
- " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
115
- " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
116
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
117
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
118
-
119
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
120
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
121
- end
122
-
123
- elsif any = options.delete(:any)
124
- # get tags, drop out if nothing returned (we need at least one)
125
- tags = if options.delete(:wild)
126
- ActsAsTaggableOn::Tag.named_like_any(tag_list)
127
- else
128
- ActsAsTaggableOn::Tag.named_any(tag_list)
129
- end
130
-
131
- return empty_result if tags.length == 0
132
-
133
- # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
134
- # avoid ambiguous column name
135
- taggings_context = context ? "_#{context}" : ''
136
-
137
- taggings_alias = adjust_taggings_alias(
138
- "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
139
- )
140
-
141
- tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
142
- " WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
143
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
144
-
145
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
146
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
147
-
148
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
149
-
150
- # don't need to sanitize sql, map all ids and join with OR logic
151
- tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
152
- tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
153
- select_clause << " #{table_name}.*" unless context and tag_types.one?
154
-
155
- if owned_by
156
- tagging_cond << ' AND ' +
157
- sanitize_sql([
158
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
159
- owned_by.id,
160
- owned_by.class.base_class.to_s
161
- ])
162
- end
163
-
164
- conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
165
- if options.delete(:order_by_matching_tag_count)
166
- order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
167
- end
168
- else
169
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
170
-
171
- return empty_result unless tags.length == tag_list.length
172
-
173
- tags.each do |tag|
174
- taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
175
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
176
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
177
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
178
- " AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
179
-
180
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
181
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
182
113
 
183
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
114
+ return none if tag_list.empty?
184
115
 
185
- if owned_by
186
- tagging_join << ' AND ' +
187
- sanitize_sql([
188
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
189
- owned_by.id,
190
- owned_by.class.base_class.to_s
191
- ])
192
- end
193
-
194
- joins << tagging_join
195
- end
196
- end
197
-
198
- group ||= [] # Rails interprets this as a no-op in the group() call below
199
- if options.delete(:order_by_matching_tag_count)
200
- select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
201
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
202
- group = group_columns
203
- order_by << "#{taggings_alias}_count DESC"
204
-
205
- elsif options.delete(:match_all)
206
- taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
207
- joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
208
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
209
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
210
-
211
- joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
212
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
213
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
214
-
215
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
216
- group = group_columns
217
- having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
218
- end
219
-
220
- order_by << options[:order] if options[:order].present?
221
-
222
- query = self
223
- query = self.select(select_clause.join(',')) unless select_clause.empty?
224
- query.joins(joins.join(' '))
225
- .where(conditions.join(' AND '))
226
- .group(group)
227
- .having(having)
228
- .order(order_by.join(', '))
229
- .readonly(false)
116
+ ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
230
117
  end
231
118
 
232
119
  def is_taggable?
233
120
  true
234
121
  end
235
122
 
236
- def adjust_taggings_alias(taggings_alias)
237
- if taggings_alias.size > 75
238
- taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
239
- end
240
- taggings_alias
241
- end
242
-
243
123
  def taggable_mixin
244
124
  @taggable_mixin ||= Module.new
245
125
  end
246
-
247
- private
248
-
249
- # Rails 5 has merged sanitize and quote_value
250
- # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/sanitization.rb#L10
251
- def quote_value(value, column = nil)
252
- ActsAsTaggableOn::Utils.active_record5? ? super(value) : super(value, column)
253
- end
254
126
  end
255
127
 
256
128
  # all column names are necessary for PostgreSQL group clause
@@ -283,7 +155,7 @@ module ActsAsTaggableOn::Taggable
283
155
  variable_name = "@#{context.to_s.singularize}_list"
284
156
  if instance_variable_get(variable_name)
285
157
  instance_variable_get(variable_name)
286
- elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
158
+ elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
287
159
  instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
288
160
  else
289
161
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
@@ -312,7 +184,7 @@ module ActsAsTaggableOn::Taggable
312
184
 
313
185
  if ActsAsTaggableOn::Utils.using_postgresql?
314
186
  group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
315
- scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
187
+ scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
316
188
  else
317
189
  scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
318
190
  end.to_a
@@ -332,30 +204,19 @@ module ActsAsTaggableOn::Taggable
332
204
  add_custom_context(context)
333
205
 
334
206
  variable_name = "@#{context.to_s.singularize}_list"
335
- process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
336
207
 
337
- instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
208
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
209
+
210
+ instance_variable_set(variable_name, parsed_new_list)
338
211
  end
339
212
 
340
213
  def tagging_contexts
341
214
  self.class.tag_types.map(&:to_s) + custom_contexts
342
215
  end
343
216
 
344
- def process_dirty_object(context, new_list)
345
- value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list
346
- attrib = "#{context.to_s.singularize}_list"
347
-
348
- if changed_attributes.include?(attrib)
349
- # The attribute already has an unsaved change.
350
- old = changed_attributes[attrib]
351
- @changed_attributes.delete(attrib) if old.to_s == value.to_s
352
- else
353
- old = tag_list_on(context)
354
- if self.class.preserve_tag_order
355
- @changed_attributes[attrib] = old if old.to_s != value.to_s
356
- else
357
- @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
358
- end
217
+ def tenant
218
+ if self.class.tenant_column
219
+ read_attribute(self.class.tenant_column)
359
220
  end
360
221
  end
361
222
 
@@ -417,7 +278,11 @@ module ActsAsTaggableOn::Taggable
417
278
 
418
279
  # Create new taggings:
419
280
  new_tags.each do |tag|
420
- taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
281
+ if tenant
282
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: tenant)
283
+ else
284
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
285
+ end
421
286
  end
422
287
  end
423
288
 
@@ -426,6 +291,10 @@ module ActsAsTaggableOn::Taggable
426
291
 
427
292
  private
428
293
 
294
+ def ensure_included_cache_methods!
295
+ self.class.columns
296
+ end
297
+
429
298
  # Filters the tag lists from the attribute names.
430
299
  def attributes_for_update(attribute_names)
431
300
  tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
@@ -462,3 +331,4 @@ module ActsAsTaggableOn::Taggable
462
331
  end
463
332
  end
464
333
  end
334
+