acts-as-taggable-on 7.0.0 → 9.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +76 -0
  3. data/Appraisals +13 -13
  4. data/CHANGELOG.md +27 -2
  5. data/Gemfile +1 -0
  6. data/README.md +32 -7
  7. data/acts-as-taggable-on.gemspec +2 -2
  8. data/db/migrate/1_acts_as_taggable_on_migration.rb +5 -8
  9. data/db/migrate/2_add_missing_unique_indices.rb +6 -8
  10. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +3 -6
  11. data/db/migrate/4_add_missing_taggable_index.rb +5 -7
  12. data/db/migrate/5_change_collation_for_tag_names.rb +4 -6
  13. data/db/migrate/6_add_missing_indexes_on_taggings.rb +15 -13
  14. data/db/migrate/7_add_tenant_to_taggings.rb +13 -0
  15. data/docker-compose.yml +15 -0
  16. data/gemfiles/activerecord_6.0.gemfile +5 -8
  17. data/gemfiles/activerecord_6.1.gemfile +3 -8
  18. data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +6 -9
  19. data/lib/acts_as_taggable_on/default_parser.rb +8 -10
  20. data/lib/acts_as_taggable_on/engine.rb +2 -0
  21. data/lib/acts_as_taggable_on/generic_parser.rb +2 -0
  22. data/lib/acts_as_taggable_on/tag.rb +33 -27
  23. data/lib/acts_as_taggable_on/tag_list.rb +8 -11
  24. data/lib/acts_as_taggable_on/taggable/cache.rb +64 -62
  25. data/lib/acts_as_taggable_on/taggable/collection.rb +178 -142
  26. data/lib/acts_as_taggable_on/taggable/core.rb +250 -236
  27. data/lib/acts_as_taggable_on/taggable/ownership.rb +110 -98
  28. data/lib/acts_as_taggable_on/taggable/related.rb +60 -47
  29. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +6 -2
  30. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +110 -106
  31. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +57 -53
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +63 -60
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +54 -46
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +14 -8
  35. data/lib/acts_as_taggable_on/taggable.rb +30 -12
  36. data/lib/acts_as_taggable_on/tagger.rb +9 -5
  37. data/lib/acts_as_taggable_on/tagging.rb +8 -4
  38. data/lib/acts_as_taggable_on/tags_helper.rb +3 -1
  39. data/lib/acts_as_taggable_on/utils.rb +4 -2
  40. data/lib/acts_as_taggable_on/version.rb +3 -1
  41. data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
  42. data/spec/acts_as_taggable_on/taggable_spec.rb +6 -2
  43. data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
  44. data/spec/internal/app/models/taggable_model.rb +2 -0
  45. data/spec/internal/config/database.yml.sample +4 -8
  46. data/spec/internal/db/schema.rb +3 -0
  47. data/spec/support/database.rb +36 -26
  48. metadata +13 -22
  49. data/.travis.yml +0 -49
  50. data/UPGRADING.md +0 -8
  51. data/gemfiles/activerecord_5.0.gemfile +0 -21
  52. data/gemfiles/activerecord_5.1.gemfile +0 -21
@@ -1,4 +1,5 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  module ActsAsTaggableOn
3
4
  class Tag < ::ActiveRecord::Base
4
5
  self.table_name = ActsAsTaggableOn.tags_table
@@ -31,35 +32,43 @@ module ActsAsTaggableOn
31
32
  end
32
33
 
33
34
  def self.named_any(list)
34
- clause = list.map { |tag|
35
+ clause = list.map do |tag|
35
36
  sanitize_sql_for_named_any(tag).force_encoding('BINARY')
36
- }.join(' OR ')
37
+ end.join(' OR ')
37
38
  where(clause)
38
39
  end
39
40
 
40
41
  def self.named_like(name)
41
- clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
42
+ clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
43
+ "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
42
44
  where(clause)
43
45
  end
44
46
 
45
47
  def self.named_like_any(list)
46
- clause = list.map { |tag|
47
- sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
48
- }.join(' OR ')
48
+ clause = list.map do |tag|
49
+ sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
50
+ "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
51
+ end.join(' OR ')
49
52
  where(clause)
50
53
  end
51
54
 
52
55
  def self.for_context(context)
53
- joins(:taggings).
54
- where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
55
- select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
56
+ joins(:taggings)
57
+ .where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context])
58
+ .select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
59
+ end
60
+
61
+ def self.for_tenant(tenant)
62
+ joins(:taggings)
63
+ .where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s)
64
+ .select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
56
65
  end
57
66
 
58
67
  ### CLASS METHODS:
59
68
 
60
69
  def self.find_or_create_with_like_by_name(name)
61
70
  if ActsAsTaggableOn.strict_case_match
62
- self.find_or_create_all_with_like_by_name([name]).first
71
+ find_or_create_all_with_like_by_name([name]).first
63
72
  else
64
73
  named_like(name).first || create(name: name)
65
74
  end
@@ -72,27 +81,25 @@ module ActsAsTaggableOn
72
81
 
73
82
  existing_tags = named_any(list)
74
83
  list.map do |tag_name|
75
- begin
76
- tries ||= 3
77
- comparable_tag_name = comparable_name(tag_name)
78
- existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
79
- existing_tag || create(name: tag_name)
80
- rescue ActiveRecord::RecordNotUnique
81
- if (tries -= 1).positive?
82
- ActiveRecord::Base.connection.execute 'ROLLBACK'
83
- existing_tags = named_any(list)
84
- retry
85
- end
86
-
87
- raise DuplicateTagError.new("'#{tag_name}' has already been taken")
84
+ tries ||= 3
85
+ comparable_tag_name = comparable_name(tag_name)
86
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
87
+ existing_tag || create(name: tag_name)
88
+ rescue ActiveRecord::RecordNotUnique
89
+ if (tries -= 1).positive?
90
+ ActiveRecord::Base.connection.execute 'ROLLBACK'
91
+ existing_tags = named_any(list)
92
+ retry
88
93
  end
94
+
95
+ raise DuplicateTagError, "'#{tag_name}' has already been taken"
89
96
  end
90
97
  end
91
98
 
92
99
  ### INSTANCE METHODS:
93
100
 
94
- def ==(object)
95
- super || (object.is_a?(Tag) && name == object.name)
101
+ def ==(other)
102
+ super || (other.is_a?(Tag) && name == other.name)
96
103
  end
97
104
 
98
105
  def to_s
@@ -104,7 +111,6 @@ module ActsAsTaggableOn
104
111
  end
105
112
 
106
113
  class << self
107
-
108
114
  private
109
115
 
110
116
  def comparable_name(str)
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'active_support/core_ext/module/delegation'
3
4
 
4
5
  module ActsAsTaggableOn
5
6
  class TagList < Array
6
- attr_accessor :owner
7
- attr_accessor :parser
7
+ attr_accessor :owner, :parser
8
8
 
9
9
  def initialize(*args)
10
10
  @parser = ActsAsTaggableOn.default_parser
@@ -34,8 +34,8 @@ module ActsAsTaggableOn
34
34
 
35
35
  # Concatenation --- Returns a new tag list built by concatenating the
36
36
  # two tag lists together to produce a third tag list.
37
- def +(other_tag_list)
38
- TagList.new.add(self).add(other_tag_list)
37
+ def +(other)
38
+ TagList.new.add(self).add(other)
39
39
  end
40
40
 
41
41
  # Appends the elements of +other_tag_list+ to +self+.
@@ -65,12 +65,12 @@ module ActsAsTaggableOn
65
65
  # tag_list = TagList.new("Round", "Square,Cube")
66
66
  # tag_list.to_s # 'Round, "Square,Cube"'
67
67
  def to_s
68
- tags = frozen? ? self.dup : self
68
+ tags = frozen? ? dup : self
69
69
  tags.send(:clean!)
70
70
 
71
71
  tags.map do |name|
72
72
  d = ActsAsTaggableOn.delimiter
73
- d = Regexp.new d.join('|') if d.kind_of? Array
73
+ d = Regexp.new d.join('|') if d.is_a? Array
74
74
  name.index(d) ? "\"#{name}\"" : name
75
75
  end.join(ActsAsTaggableOn.glue)
76
76
  end
@@ -85,22 +85,19 @@ module ActsAsTaggableOn
85
85
  map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
86
86
  map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
87
87
 
88
- ActsAsTaggableOn.strict_case_match ? uniq! : uniq!{ |tag| tag.downcase }
88
+ ActsAsTaggableOn.strict_case_match ? uniq! : uniq!(&:downcase)
89
89
  self
90
90
  end
91
91
 
92
-
93
92
  def extract_and_apply_options!(args)
94
93
  options = args.last.is_a?(Hash) ? args.pop : {}
95
94
  options.assert_valid_keys :parse, :parser
96
95
 
97
- parser = options[:parser] ? options[:parser] : @parser
96
+ parser = options[:parser] || @parser
98
97
 
99
98
  args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
100
99
 
101
100
  args.flatten!
102
101
  end
103
-
104
102
  end
105
103
  end
106
-
@@ -1,89 +1,91 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Cache
3
- def self.included(base)
4
- # When included, conditionally adds tag caching methods when the model
5
- # has any "cached_#{tag_type}_list" column
6
- base.extend Columns
7
- end
1
+ # frozen_string_literal: true
8
2
 
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
25
- end
3
+ module ActsAsTaggableOn
4
+ module Taggable
5
+ module Cache
6
+ def self.included(base)
7
+ # When included, conditionally adds tag caching methods when the model
8
+ # has any "cached_#{tag_type}_list" column
9
+ base.extend Columns
26
10
  end
27
11
 
28
- def reset_column_information
29
- super
30
- @acts_as_taggable_on_cache_columns = nil
31
- end
12
+ module Columns
13
+ # ActiveRecord::Base.columns makes a database connection and caches the
14
+ # calculated columns hash for the record as @columns. Since we don't
15
+ # want to add caching methods until we confirm the presence of a
16
+ # caching column, and we don't want to force opening a database
17
+ # connection when the class is loaded, here we intercept and cache
18
+ # the call to :columns as @acts_as_taggable_on_cache_columns
19
+ # to mimic the underlying behavior. While processing this first
20
+ # call to columns, we do the caching column check and dynamically add
21
+ # the class and instance methods
22
+ # FIXME: this method cannot compile in rubinius
23
+ def columns
24
+ @acts_as_taggable_on_cache_columns ||= begin
25
+ db_columns = super
26
+ _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
27
+ db_columns
28
+ end
29
+ end
32
30
 
33
- private
31
+ def reset_column_information
32
+ super
33
+ @acts_as_taggable_on_cache_columns = nil
34
+ end
35
+
36
+ private
34
37
 
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")
38
+ # @private
39
+ def _has_tags_cache_columns?(db_columns)
40
+ db_column_names = db_columns.map(&:name)
41
+ tag_types.any? do |context|
42
+ db_column_names.include?("cached_#{context.to_s.singularize}_list")
43
+ end
40
44
  end
41
- end
42
45
 
43
- # @private
44
- def _add_tags_caching_methods
45
- send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
46
- extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
46
+ # @private
47
+ def _add_tags_caching_methods
48
+ send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
49
+ extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
47
50
 
48
- before_save :save_cached_tag_list
51
+ before_save :save_cached_tag_list
49
52
 
50
- initialize_tags_cache
53
+ initialize_tags_cache
54
+ end
51
55
  end
52
- end
53
56
 
54
- module ClassMethods
55
- def initialize_tags_cache
56
- tag_types.map(&:to_s).each do |tag_type|
57
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
57
+ module ClassMethods
58
+ def initialize_tags_cache
59
+ tag_types.map(&:to_s).each do |tag_type|
60
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
58
61
  def self.caching_#{tag_type.singularize}_list?
59
62
  caching_tag_list_on?("#{tag_type}")
60
63
  end
61
- RUBY
64
+ RUBY
65
+ end
62
66
  end
63
- end
64
67
 
65
- def acts_as_taggable_on(*args)
66
- super(*args)
67
- initialize_tags_cache
68
- end
68
+ def acts_as_taggable_on(*args)
69
+ super(*args)
70
+ initialize_tags_cache
71
+ end
69
72
 
70
- def caching_tag_list_on?(context)
71
- column_names.include?("cached_#{context.to_s.singularize}_list")
73
+ def caching_tag_list_on?(context)
74
+ column_names.include?("cached_#{context.to_s.singularize}_list")
75
+ end
72
76
  end
73
- end
74
77
 
75
- module InstanceMethods
76
- def save_cached_tag_list
77
- tag_types.map(&:to_s).each do |tag_type|
78
- if self.class.send("caching_#{tag_type.singularize}_list?")
79
- if tag_list_cache_set_on(tag_type)
78
+ module InstanceMethods
79
+ def save_cached_tag_list
80
+ tag_types.map(&:to_s).each do |tag_type|
81
+ if self.class.send("caching_#{tag_type.singularize}_list?") && tag_list_cache_set_on(tag_type)
80
82
  list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
81
83
  self["cached_#{tag_type.singularize}_list"] = list
82
84
  end
83
85
  end
84
- end
85
86
 
86
- true
87
+ true
88
+ end
87
89
  end
88
90
  end
89
91
  end