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.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +76 -0
- data/Appraisals +13 -13
- data/CHANGELOG.md +27 -2
- data/Gemfile +1 -0
- data/README.md +32 -7
- data/acts-as-taggable-on.gemspec +2 -2
- data/db/migrate/1_acts_as_taggable_on_migration.rb +5 -8
- data/db/migrate/2_add_missing_unique_indices.rb +6 -8
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +3 -6
- data/db/migrate/4_add_missing_taggable_index.rb +5 -7
- data/db/migrate/5_change_collation_for_tag_names.rb +4 -6
- data/db/migrate/6_add_missing_indexes_on_taggings.rb +15 -13
- data/db/migrate/7_add_tenant_to_taggings.rb +13 -0
- data/docker-compose.yml +15 -0
- data/gemfiles/activerecord_6.0.gemfile +5 -8
- data/gemfiles/activerecord_6.1.gemfile +3 -8
- data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +6 -9
- data/lib/acts_as_taggable_on/default_parser.rb +8 -10
- data/lib/acts_as_taggable_on/engine.rb +2 -0
- data/lib/acts_as_taggable_on/generic_parser.rb +2 -0
- data/lib/acts_as_taggable_on/tag.rb +33 -27
- data/lib/acts_as_taggable_on/tag_list.rb +8 -11
- data/lib/acts_as_taggable_on/taggable/cache.rb +64 -62
- data/lib/acts_as_taggable_on/taggable/collection.rb +178 -142
- data/lib/acts_as_taggable_on/taggable/core.rb +250 -236
- data/lib/acts_as_taggable_on/taggable/ownership.rb +110 -98
- data/lib/acts_as_taggable_on/taggable/related.rb +60 -47
- data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +6 -2
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +110 -106
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +57 -53
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +63 -60
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +54 -46
- data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +14 -8
- data/lib/acts_as_taggable_on/taggable.rb +30 -12
- data/lib/acts_as_taggable_on/tagger.rb +9 -5
- data/lib/acts_as_taggable_on/tagging.rb +8 -4
- data/lib/acts_as_taggable_on/tags_helper.rb +3 -1
- data/lib/acts_as_taggable_on/utils.rb +4 -2
- data/lib/acts_as_taggable_on/version.rb +3 -1
- data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
- data/spec/acts_as_taggable_on/taggable_spec.rb +6 -2
- data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
- data/spec/internal/app/models/taggable_model.rb +2 -0
- data/spec/internal/config/database.yml.sample +4 -8
- data/spec/internal/db/schema.rb +3 -0
- data/spec/support/database.rb +36 -26
- metadata +13 -22
- data/.travis.yml +0 -49
- data/UPGRADING.md +0 -8
- data/gemfiles/activerecord_5.0.gemfile +0 -21
- data/gemfiles/activerecord_5.1.gemfile +0 -21
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
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
|
35
|
+
clause = list.map do |tag|
|
35
36
|
sanitize_sql_for_named_any(tag).force_encoding('BINARY')
|
36
|
-
|
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 '!'",
|
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
|
47
|
-
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
|
48
|
-
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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 ==(
|
95
|
-
super || (
|
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 +(
|
38
|
-
TagList.new.add(self).add(
|
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? ?
|
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.
|
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!
|
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]
|
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
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
29
|
-
|
30
|
-
@
|
31
|
-
|
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
|
-
|
31
|
+
def reset_column_information
|
32
|
+
super
|
33
|
+
@acts_as_taggable_on_cache_columns = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
46
|
+
# @private
|
47
|
+
def _add_tags_caching_methods
|
48
|
+
send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
|
49
|
+
extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
|
47
50
|
|
48
|
-
|
51
|
+
before_save :save_cached_tag_list
|
49
52
|
|
50
|
-
|
53
|
+
initialize_tags_cache
|
54
|
+
end
|
51
55
|
end
|
52
|
-
end
|
53
56
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
64
|
+
RUBY
|
65
|
+
end
|
62
66
|
end
|
63
|
-
end
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
68
|
+
def acts_as_taggable_on(*args)
|
69
|
+
super(*args)
|
70
|
+
initialize_tags_cache
|
71
|
+
end
|
69
72
|
|
70
|
-
|
71
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
87
|
+
true
|
88
|
+
end
|
87
89
|
end
|
88
90
|
end
|
89
91
|
end
|