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,70 +1,74 @@
|
|
1
|
-
|
2
|
-
class AnyTagsQuery < QueryBase
|
3
|
-
def build
|
4
|
-
taggable_model.select(all_fields)
|
5
|
-
.where(model_has_at_least_one_tag)
|
6
|
-
.order(Arel.sql(order_conditions))
|
7
|
-
.readonly(false)
|
8
|
-
end
|
1
|
+
# frozen_string_literal: true
|
9
2
|
|
10
|
-
|
3
|
+
module ActsAsTaggableOn
|
4
|
+
module Taggable
|
5
|
+
module TaggedWithQuery
|
6
|
+
class AnyTagsQuery < QueryBase
|
7
|
+
def build
|
8
|
+
taggable_model.select(all_fields)
|
9
|
+
.where(model_has_at_least_one_tag)
|
10
|
+
.order(Arel.sql(order_conditions))
|
11
|
+
.readonly(false)
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
taggable_arel_table[Arel.star]
|
14
|
-
end
|
14
|
+
private
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def all_fields
|
17
|
+
taggable_arel_table[Arel.star]
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
.and(
|
24
|
-
tagging_arel_table[:tag_id].in(
|
25
|
-
tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type)
|
26
|
-
)
|
27
|
-
)
|
20
|
+
def model_has_at_least_one_tag
|
21
|
+
tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists
|
22
|
+
end
|
28
23
|
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
def at_least_one_tag
|
25
|
+
exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
|
26
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
27
|
+
.and(
|
28
|
+
tagging_arel_table[:tag_id].in(
|
29
|
+
tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type)
|
30
|
+
)
|
31
|
+
)
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
if options[:start_at].present?
|
34
|
+
exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
|
35
|
+
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
if options[:end_at].present?
|
38
|
+
exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
41
|
+
if options[:on].present?
|
42
|
+
exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on]))
|
43
|
+
end
|
45
44
|
|
46
|
-
|
47
|
-
|
45
|
+
if (owner = options[:owned_by]).present?
|
46
|
+
exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id))
|
47
|
+
.and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
if options[:order_by_matching_tag_count].present?
|
52
|
-
order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc"
|
53
|
-
end
|
50
|
+
exists_contition
|
51
|
+
end
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
def order_conditions
|
54
|
+
order_by = []
|
55
|
+
if options[:order_by_matching_tag_count].present?
|
56
|
+
order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc"
|
57
|
+
end
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
order_by << options[:order] if options[:order].present?
|
60
|
+
order_by.join(', ')
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
def alias_name(tag_list)
|
64
|
+
alias_base_name = taggable_model.base_class.name.downcase
|
65
|
+
taggings_context = options[:on] ? "_#{options[:on]}" : ''
|
66
66
|
|
67
|
-
|
67
|
+
adjust_taggings_alias(
|
68
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag_list.join('_'))}"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
68
72
|
end
|
69
73
|
end
|
70
74
|
end
|
@@ -1,82 +1,85 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsTaggableOn
|
4
|
+
module Taggable
|
5
|
+
module TaggedWithQuery
|
6
|
+
class ExcludeTagsQuery < QueryBase
|
7
|
+
def build
|
8
|
+
taggable_model.joins(owning_to_tagger)
|
9
|
+
.where(tags_not_in_list)
|
10
|
+
.having(tags_that_matches_count)
|
11
|
+
.readonly(false)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def tags_not_in_list
|
17
|
+
taggable_arel_table[:id].not_in(
|
14
18
|
tagging_arel_table
|
15
19
|
.project(tagging_arel_table[:taggable_id])
|
16
20
|
.join(tag_arel_table)
|
17
21
|
.on(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
|
23
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
24
|
+
.and(tags_match_type)
|
25
|
+
)
|
22
26
|
)
|
23
27
|
|
24
|
-
|
25
|
-
|
28
|
+
# FIXME: missing time scope, this is also missing in the original implementation
|
29
|
+
end
|
26
30
|
|
31
|
+
def owning_to_tagger
|
32
|
+
return [] if options[:owned_by].blank?
|
27
33
|
|
28
|
-
|
29
|
-
return [] unless options[:owned_by].present?
|
34
|
+
owner = options[:owned_by]
|
30
35
|
|
31
|
-
|
36
|
+
arel_join = taggable_arel_table
|
37
|
+
.join(tagging_arel_table)
|
38
|
+
.on(
|
39
|
+
tagging_arel_table[:tagger_id].eq(owner.id)
|
40
|
+
.and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
|
41
|
+
.and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]))
|
42
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
43
|
+
)
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
)
|
45
|
+
if options[:match_all].present?
|
46
|
+
arel_join = arel_join
|
47
|
+
.join(tagging_arel_table, Arel::Nodes::OuterJoin)
|
48
|
+
.on(
|
49
|
+
match_all_on_conditions
|
50
|
+
)
|
51
|
+
end
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
.join(tagging_arel_table, Arel::Nodes::OuterJoin)
|
45
|
-
.on(
|
46
|
-
match_all_on_conditions
|
47
|
-
)
|
48
|
-
end
|
53
|
+
arel_join.join_sources
|
54
|
+
end
|
49
55
|
|
50
|
-
|
51
|
-
|
56
|
+
def match_all_on_conditions
|
57
|
+
on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
|
58
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
52
59
|
|
53
|
-
|
54
|
-
|
55
|
-
|
60
|
+
if options[:start_at].present?
|
61
|
+
on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
|
62
|
+
end
|
56
63
|
|
57
|
-
|
58
|
-
|
59
|
-
|
64
|
+
if options[:end_at].present?
|
65
|
+
on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
|
66
|
+
end
|
60
67
|
|
61
|
-
|
62
|
-
on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
|
63
|
-
end
|
68
|
+
on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) if options[:on].present?
|
64
69
|
|
65
|
-
|
66
|
-
|
67
|
-
end
|
70
|
+
on_condition
|
71
|
+
end
|
68
72
|
|
69
|
-
|
70
|
-
|
73
|
+
def tags_that_matches_count
|
74
|
+
return [] if options[:match_all].blank?
|
71
75
|
|
72
|
-
|
73
|
-
return [] unless options[:match_all].present?
|
76
|
+
taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
|
74
77
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
tagging_arel_table[:taggable_id].count.eq(
|
79
|
+
tag_arel_table.project(Arel.star.count).where(tags_match_type)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
80
83
|
end
|
81
84
|
end
|
82
85
|
end
|
@@ -1,61 +1,69 @@
|
|
1
|
-
|
2
|
-
class QueryBase
|
3
|
-
def initialize(taggable_model, tag_model, tagging_model, tag_list, options)
|
4
|
-
@taggable_model = taggable_model
|
5
|
-
@tag_model = tag_model
|
6
|
-
@tagging_model = tagging_model
|
7
|
-
@tag_list = tag_list
|
8
|
-
@options = options
|
9
|
-
end
|
1
|
+
# frozen_string_literal: true
|
10
2
|
|
11
|
-
|
3
|
+
module ActsAsTaggableOn
|
4
|
+
module Taggable
|
5
|
+
module TaggedWithQuery
|
6
|
+
class QueryBase
|
7
|
+
def initialize(taggable_model, tag_model, tagging_model, tag_list, options)
|
8
|
+
@taggable_model = taggable_model
|
9
|
+
@tag_model = tag_model
|
10
|
+
@tagging_model = tagging_model
|
11
|
+
@tag_list = tag_list
|
12
|
+
@options = options
|
13
|
+
end
|
12
14
|
|
13
|
-
|
15
|
+
private
|
14
16
|
|
15
|
-
|
16
|
-
@taggable_arel_table ||= taggable_model.arel_table
|
17
|
-
end
|
17
|
+
attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def taggable_arel_table
|
20
|
+
@taggable_arel_table ||= taggable_model.arel_table
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
def tag_arel_table
|
24
|
+
@tag_arel_table ||= tag_model.arel_table
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
def tagging_arel_table
|
28
|
+
@tagging_arel_table ||= tagging_model.arel_table
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
matches_attribute.matches(escaped_tag(tag), "!", ActsAsTaggableOn.strict_case_match)
|
35
|
-
end
|
36
|
-
end
|
31
|
+
def tag_match_type(tag)
|
32
|
+
matches_attribute = tag_arel_table[:name]
|
33
|
+
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
|
37
34
|
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
if options[:wild].present?
|
36
|
+
matches_attribute.matches("%#{escaped_tag(tag)}%", '!', ActsAsTaggableOn.strict_case_match)
|
37
|
+
else
|
38
|
+
matches_attribute.matches(escaped_tag(tag), '!', ActsAsTaggableOn.strict_case_match)
|
39
|
+
end
|
40
|
+
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
matches_attribute.matches_any(tag_list.map{|tag| "#{escaped_tag(tag)}"}, "!", ActsAsTaggableOn.strict_case_match)
|
46
|
-
end
|
47
|
-
end
|
42
|
+
def tags_match_type
|
43
|
+
matches_attribute = tag_arel_table[:name]
|
44
|
+
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
46
|
+
if options[:wild].present?
|
47
|
+
matches_attribute.matches_any(tag_list.map do |tag|
|
48
|
+
"%#{escaped_tag(tag)}%"
|
49
|
+
end, '!', ActsAsTaggableOn.strict_case_match)
|
50
|
+
else
|
51
|
+
matches_attribute.matches_any(tag_list.map do |tag|
|
52
|
+
escaped_tag(tag).to_s
|
53
|
+
end, '!', ActsAsTaggableOn.strict_case_match)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def escaped_tag(tag)
|
58
|
+
tag = tag.downcase unless ActsAsTaggableOn.strict_case_match
|
59
|
+
ActsAsTaggableOn::Utils.escape_like(tag)
|
60
|
+
end
|
53
61
|
|
54
|
-
|
55
|
-
|
56
|
-
|
62
|
+
def adjust_taggings_alias(taggings_alias)
|
63
|
+
taggings_alias = "taggings_alias_#{Digest::SHA1.hexdigest(taggings_alias)}" if taggings_alias.size > 75
|
64
|
+
taggings_alias
|
65
|
+
end
|
57
66
|
end
|
58
|
-
taggings_alias
|
59
67
|
end
|
60
68
|
end
|
61
69
|
end
|
@@ -1,16 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'tagged_with_query/query_base'
|
2
4
|
require_relative 'tagged_with_query/exclude_tags_query'
|
3
5
|
require_relative 'tagged_with_query/any_tags_query'
|
4
6
|
require_relative 'tagged_with_query/all_tags_query'
|
5
7
|
|
6
|
-
module ActsAsTaggableOn
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
module ActsAsTaggableOn
|
9
|
+
module Taggable
|
10
|
+
module TaggedWithQuery
|
11
|
+
def self.build(taggable_model, tag_model, tagging_model, tag_list, options)
|
12
|
+
if options[:exclude].present?
|
13
|
+
ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
|
14
|
+
elsif options[:any].present?
|
15
|
+
AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
|
16
|
+
else
|
17
|
+
AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
|
18
|
+
end
|
19
|
+
end
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsTaggableOn
|
2
4
|
module Taggable
|
3
|
-
|
4
5
|
def taggable?
|
5
6
|
false
|
6
7
|
end
|
@@ -54,19 +55,35 @@ module ActsAsTaggableOn
|
|
54
55
|
taggable_on(true, tag_types)
|
55
56
|
end
|
56
57
|
|
58
|
+
def acts_as_taggable_tenant(tenant)
|
59
|
+
if taggable?
|
60
|
+
else
|
61
|
+
class_attribute :tenant_column
|
62
|
+
end
|
63
|
+
self.tenant_column = tenant
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
76
|
+
# Make a model taggable on specified contexts
|
77
|
+
# and optionally preserves the order in which tags are created
|
78
|
+
#
|
79
|
+
# Separate methods used above for backwards compatibility
|
80
|
+
# so that the original acts_as_taggable_on method is unaffected
|
81
|
+
# as it's not possible to add another argument to the method
|
82
|
+
# without the tag_types being enclosed in square brackets
|
83
|
+
#
|
84
|
+
# NB: method overridden in core module in order to create tag type
|
85
|
+
# associations and methods after this logic has executed
|
86
|
+
#
|
70
87
|
def taggable_on(preserve_tag_order, *tag_types)
|
71
88
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
72
89
|
|
@@ -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'
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsTaggableOn
|
2
4
|
module Tagger
|
3
5
|
def self.included(base)
|
@@ -13,7 +15,7 @@ module ActsAsTaggableOn
|
|
13
15
|
# class User < ActiveRecord::Base
|
14
16
|
# acts_as_tagger
|
15
17
|
# end
|
16
|
-
def acts_as_tagger(opts={})
|
18
|
+
def acts_as_tagger(opts = {})
|
17
19
|
class_eval do
|
18
20
|
owned_taggings_scope = opts.delete(:scope)
|
19
21
|
|
@@ -54,14 +56,16 @@ module ActsAsTaggableOn
|
|
54
56
|
#
|
55
57
|
# Example:
|
56
58
|
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
57
|
-
def tag(taggable, opts={})
|
59
|
+
def tag(taggable, opts = {})
|
58
60
|
opts.reverse_merge!(force: true)
|
59
61
|
skip_save = opts.delete(:skip_save)
|
60
62
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
61
63
|
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
raise 'You need to specify a tag context using :on' unless opts.key?(:on)
|
65
|
+
raise 'You need to specify some tags using :with' unless opts.key?(:with)
|
66
|
+
unless opts[:force] || taggable.tag_types.include?(opts[:on])
|
67
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class}"
|
68
|
+
end
|
65
69
|
|
66
70
|
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
|
67
71
|
taggable.save unless skip_save
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsTaggableOn
|
2
|
-
class Tagging < ::ActiveRecord::Base
|
4
|
+
class Tagging < ::ActiveRecord::Base # :nodoc:
|
3
5
|
self.table_name = ActsAsTaggableOn.taggings_table
|
4
6
|
|
5
7
|
DEFAULT_CONTEXT = 'tags'
|
@@ -14,10 +16,12 @@ module ActsAsTaggableOn
|
|
14
16
|
scope :by_contexts, ->(contexts) { where(context: (contexts || DEFAULT_CONTEXT)) }
|
15
17
|
scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
|
16
18
|
|
19
|
+
scope :by_tenant, ->(tenant) { where(tenant: tenant) }
|
20
|
+
|
17
21
|
validates_presence_of :context
|
18
22
|
validates_presence_of :tag_id
|
19
23
|
|
20
|
-
validates_uniqueness_of :tag_id, scope: [
|
24
|
+
validates_uniqueness_of :tag_id, scope: %i[taggable_type taggable_id context tagger_id tagger_type]
|
21
25
|
|
22
26
|
after_destroy :remove_unused_tags
|
23
27
|
|
@@ -27,8 +31,8 @@ module ActsAsTaggableOn
|
|
27
31
|
if ActsAsTaggableOn.remove_unused_tags
|
28
32
|
if ActsAsTaggableOn.tags_counter
|
29
33
|
tag.destroy if tag.reload.taggings_count.zero?
|
30
|
-
|
31
|
-
tag.destroy
|
34
|
+
elsif tag.reload.taggings.none?
|
35
|
+
tag.destroy
|
32
36
|
end
|
33
37
|
end
|
34
38
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsTaggableOn
|
2
4
|
module TagsHelper
|
3
5
|
# See the wiki for an example using tag_cloud.
|
4
6
|
def tag_cloud(tags, classes)
|
5
7
|
return [] if tags.empty?
|
6
8
|
|
7
|
-
max_count = tags.
|
9
|
+
max_count = tags.max_by(&:taggings_count).taggings_count.to_f
|
8
10
|
|
9
11
|
tags.each do |tag|
|
10
12
|
index = ((tag.taggings_count / max_count) * (classes.size - 1))
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This module is deprecated and will be removed in the incoming versions
|
2
4
|
|
3
5
|
module ActsAsTaggableOn
|
@@ -9,7 +11,7 @@ module ActsAsTaggableOn
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def using_postgresql?
|
12
|
-
connection && connection.adapter_name
|
14
|
+
connection && %w[PostgreSQL PostGIS].include?(connection.adapter_name)
|
13
15
|
end
|
14
16
|
|
15
17
|
def using_mysql?
|
@@ -30,7 +32,7 @@ module ActsAsTaggableOn
|
|
30
32
|
|
31
33
|
# escape _ and % characters in strings, since these are wildcards in SQL.
|
32
34
|
def escape_like(str)
|
33
|
-
str.gsub(/[!%_]/) { |x|
|
35
|
+
str.gsub(/[!%_]/) { |x| "!#{x}" }
|
34
36
|
end
|
35
37
|
end
|
36
38
|
end
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
describe ActsAsTaggableOn::Tag do
|
15
15
|
before(:each) do
|
16
16
|
@tag = ActsAsTaggableOn::Tag.new
|
17
|
-
@user = TaggableModel.create(name: 'Pablo')
|
17
|
+
@user = TaggableModel.create(name: 'Pablo', tenant_id: 100)
|
18
18
|
end
|
19
19
|
|
20
20
|
|
@@ -70,6 +70,21 @@ describe ActsAsTaggableOn::Tag do
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
+
describe 'for tenant' do
|
74
|
+
before(:each) do
|
75
|
+
@user.skill_list.add('ruby')
|
76
|
+
@user.save
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should return tags for the tenant' do
|
80
|
+
expect(ActsAsTaggableOn::Tag.for_tenant('100').pluck(:name)).to include('ruby')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should not return tags for other tenants' do
|
84
|
+
expect(ActsAsTaggableOn::Tag.for_tenant('200').pluck(:name)).to_not include('ruby')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
73
88
|
describe 'find or create by name' do
|
74
89
|
before(:each) do
|
75
90
|
@tag.name = 'awesome'
|
@@ -109,6 +109,10 @@ describe 'Taggable' do
|
|
109
109
|
expect(@taggable.tag_types).to eq(TaggableModel.tag_types)
|
110
110
|
end
|
111
111
|
|
112
|
+
it 'should have tenant column' do
|
113
|
+
expect(TaggableModel.tenant_column).to eq(:tenant_id)
|
114
|
+
end
|
115
|
+
|
112
116
|
it 'should have tag_counts_on' do
|
113
117
|
expect(TaggableModel.tag_counts_on(:tags)).to be_empty
|
114
118
|
|
@@ -676,11 +680,11 @@ describe 'Taggable' do
|
|
676
680
|
end
|
677
681
|
|
678
682
|
it 'should return all column names joined for TaggableModel GROUP clause' do
|
679
|
-
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq('taggable_models.id, taggable_models.name, taggable_models.type')
|
683
|
+
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq('taggable_models.id, taggable_models.name, taggable_models.type, taggable_models.tenant_id')
|
680
684
|
end
|
681
685
|
|
682
686
|
it 'should return all column names joined for NonStandardIdTaggableModel GROUP clause' do
|
683
|
-
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type")
|
687
|
+
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type, taggable_models.tenant_id")
|
684
688
|
end
|
685
689
|
end
|
686
690
|
|