acts-as-taggable-on 7.0.0 → 9.0.1

Sign up to get free protection for your applications and to get access to all the features.
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