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.
- 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
|