acts-as-taggable-on 3.5.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/spec.yml +95 -0
  3. data/.gitignore +2 -0
  4. data/Appraisals +12 -9
  5. data/CHANGELOG.md +212 -70
  6. data/CONTRIBUTING.md +13 -0
  7. data/Gemfile +2 -1
  8. data/README.md +128 -25
  9. data/acts-as-taggable-on.gemspec +2 -6
  10. data/db/migrate/1_acts_as_taggable_on_migration.rb +14 -8
  11. data/db/migrate/2_add_missing_unique_indices.rb +15 -9
  12. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +9 -4
  13. data/db/migrate/4_add_missing_taggable_index.rb +8 -3
  14. data/db/migrate/5_change_collation_for_tag_names.rb +7 -2
  15. data/db/migrate/6_add_missing_indexes_on_taggings.rb +22 -0
  16. data/db/migrate/7_add_tenant_to_taggings.rb +16 -0
  17. data/gemfiles/activerecord_5.0.gemfile +21 -0
  18. data/gemfiles/activerecord_5.1.gemfile +21 -0
  19. data/gemfiles/activerecord_5.2.gemfile +21 -0
  20. data/gemfiles/activerecord_6.0.gemfile +21 -0
  21. data/gemfiles/activerecord_6.1.gemfile +23 -0
  22. data/lib/acts-as-taggable-on.rb +22 -14
  23. data/lib/acts_as_taggable_on/engine.rb +0 -1
  24. data/lib/acts_as_taggable_on/tag.rb +27 -23
  25. data/lib/acts_as_taggable_on/tag_list.rb +3 -13
  26. data/lib/acts_as_taggable_on/taggable/cache.rb +39 -35
  27. data/lib/acts_as_taggable_on/taggable/collection.rb +14 -9
  28. data/lib/acts_as_taggable_on/taggable/core.rb +59 -184
  29. data/lib/acts_as_taggable_on/taggable/ownership.rb +19 -8
  30. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  31. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -0
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +111 -0
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +70 -0
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +82 -0
  35. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +61 -0
  36. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +16 -0
  37. data/lib/acts_as_taggable_on/taggable.rb +18 -1
  38. data/lib/acts_as_taggable_on/tagger.rb +12 -11
  39. data/lib/acts_as_taggable_on/tagging.rb +9 -14
  40. data/lib/acts_as_taggable_on/utils.rb +4 -5
  41. data/lib/acts_as_taggable_on/version.rb +1 -2
  42. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +14 -13
  43. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -1
  44. data/spec/acts_as_taggable_on/caching_spec.rb +55 -9
  45. data/spec/acts_as_taggable_on/{taggable/dirty_spec.rb → dirty_spec.rb} +28 -13
  46. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +28 -8
  47. data/spec/acts_as_taggable_on/tag_list_spec.rb +27 -1
  48. data/spec/acts_as_taggable_on/tag_spec.rb +31 -1
  49. data/spec/acts_as_taggable_on/taggable_spec.rb +40 -19
  50. data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
  51. data/spec/acts_as_taggable_on/tagging_spec.rb +87 -7
  52. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +2 -0
  53. data/spec/internal/app/models/cached_model_with_array.rb +6 -0
  54. data/spec/internal/app/models/columns_override_model.rb +5 -0
  55. data/spec/internal/app/models/company.rb +1 -1
  56. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  57. data/spec/internal/app/models/market.rb +1 -1
  58. data/spec/internal/app/models/non_standard_id_taggable_model.rb +1 -1
  59. data/spec/internal/app/models/student.rb +2 -0
  60. data/spec/internal/app/models/taggable_model.rb +3 -0
  61. data/spec/internal/app/models/user.rb +1 -1
  62. data/spec/internal/config/database.yml.sample +4 -8
  63. data/spec/internal/db/schema.rb +23 -7
  64. data/spec/spec_helper.rb +0 -1
  65. data/spec/support/database.rb +5 -11
  66. metadata +27 -75
  67. data/.travis.yml +0 -40
  68. data/UPGRADING.md +0 -8
  69. data/gemfiles/activerecord_3.2.gemfile +0 -15
  70. data/gemfiles/activerecord_4.0.gemfile +0 -15
  71. data/gemfiles/activerecord_4.1.gemfile +0 -15
  72. data/gemfiles/activerecord_4.2.gemfile +0 -16
  73. data/lib/acts_as_taggable_on/compatibility.rb +0 -35
  74. data/lib/acts_as_taggable_on/tag_list_parser.rb +0 -21
  75. data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
  76. data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +0 -46
  77. data/spec/internal/app/models/models.rb +0 -90
@@ -27,13 +27,16 @@ module ActsAsTaggableOn::Taggable
27
27
  end
28
28
  end
29
29
 
30
- def owner_tags_on(owner, context)
30
+ def owner_tags(owner)
31
31
  if owner.nil?
32
- scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
32
+ scope = base_tags
33
33
  else
34
- scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
35
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
36
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
34
+ scope = base_tags.where(
35
+ "#{ActsAsTaggableOn::Tagging.table_name}" => {
36
+ tagger_id: owner.id,
37
+ tagger_type: owner.class.base_class.to_s
38
+ }
39
+ )
37
40
  end
38
41
 
39
42
  # when preserving tag order, return tags in created order
@@ -45,6 +48,14 @@ module ActsAsTaggableOn::Taggable
45
48
  end
46
49
  end
47
50
 
51
+ def owner_tags_on(owner, context)
52
+ owner_tags(owner).where(
53
+ "#{ActsAsTaggableOn::Tagging.table_name}" => {
54
+ context: context
55
+ }
56
+ )
57
+ end
58
+
48
59
  def cached_owned_tag_list_on(context)
49
60
  variable_name = "@owned_#{context}_list"
50
61
  (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
@@ -108,9 +119,9 @@ module ActsAsTaggableOn::Taggable
108
119
 
109
120
  # Find all taggings that belong to the taggable (self), are owned by the owner,
110
121
  # have the correct context, and are removed from the list.
111
- ActsAsTaggableOn::Tagging.destroy_all(taggable_id: id, taggable_type: self.class.base_class.to_s,
112
- tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
113
- tag_id: old_tags, context: context) if old_tags.present?
122
+ ActsAsTaggableOn::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s,
123
+ tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
124
+ tag_id: old_tags, context: context).destroy_all if old_tags.present?
114
125
 
115
126
  # Create new taggings:
116
127
  new_tags.each do |tag|
@@ -49,7 +49,7 @@ module ActsAsTaggableOn::Taggable
49
49
  private
50
50
 
51
51
  def exclude_self(klass, id)
52
- "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
52
+ "#{klass.arel_table[klass.primary_key].not_eq(id).to_sql} AND" if [self.class.base_class, self.class].include? klass
53
53
  end
54
54
 
55
55
  def group_columns(klass)
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ class TagListType < ActiveModel::Type::Value
3
+ end
4
+ end
@@ -0,0 +1,111 @@
1
+ module ActsAsTaggableOn::Taggable::TaggedWithQuery
2
+ class AllTagsQuery < QueryBase
3
+ def build
4
+ taggable_model.joins(each_tag_in_list)
5
+ .group(by_taggable)
6
+ .having(tags_that_matches_count)
7
+ .order(order_conditions)
8
+ .readonly(false)
9
+ end
10
+
11
+ private
12
+
13
+ def each_tag_in_list
14
+ arel_join = taggable_arel_table
15
+
16
+ tag_list.each do |tag|
17
+ tagging_alias = tagging_arel_table.alias(tagging_alias(tag))
18
+ arel_join = arel_join
19
+ .join(tagging_alias)
20
+ .on(on_conditions(tag, tagging_alias))
21
+ end
22
+
23
+ if options[:match_all].present?
24
+ arel_join = arel_join
25
+ .join(tagging_arel_table, Arel::Nodes::OuterJoin)
26
+ .on(
27
+ match_all_on_conditions
28
+ )
29
+ end
30
+
31
+ return arel_join.join_sources
32
+ end
33
+
34
+ def on_conditions(tag, tagging_alias)
35
+ on_condition = tagging_alias[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
36
+ .and(tagging_alias[:taggable_type].eq(taggable_model.base_class.name))
37
+ .and(
38
+ tagging_alias[:tag_id].in(
39
+ tag_arel_table.project(tag_arel_table[:id]).where(tag_match_type(tag))
40
+ )
41
+ )
42
+
43
+ if options[:start_at].present?
44
+ on_condition = on_condition.and(tagging_alias[:created_at].gteq(options[:start_at]))
45
+ end
46
+
47
+ if options[:end_at].present?
48
+ on_condition = on_condition.and(tagging_alias[:created_at].lteq(options[:end_at]))
49
+ end
50
+
51
+ if options[:on].present?
52
+ on_condition = on_condition.and(tagging_alias[:context].eq(options[:on]))
53
+ end
54
+
55
+ if (owner = options[:owned_by]).present?
56
+ on_condition = on_condition.and(tagging_alias[:tagger_id].eq(owner.id))
57
+ .and(tagging_alias[:tagger_type].eq(owner.class.base_class.to_s))
58
+ end
59
+
60
+ on_condition
61
+ end
62
+
63
+ def match_all_on_conditions
64
+ on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
65
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
66
+
67
+ if options[:start_at].present?
68
+ on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
69
+ end
70
+
71
+ if options[:end_at].present?
72
+ on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
73
+ end
74
+
75
+ if options[:on].present?
76
+ on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
77
+ end
78
+
79
+ on_condition
80
+ end
81
+
82
+ def by_taggable
83
+ return [] unless options[:match_all].present?
84
+
85
+ taggable_arel_table[taggable_model.primary_key]
86
+ end
87
+
88
+ def tags_that_matches_count
89
+ return [] unless options[:match_all].present?
90
+
91
+ taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
92
+
93
+ tagging_arel_table[:taggable_id].count.eq(
94
+ tag_arel_table.project(Arel.star.count).where(tags_match_type)
95
+ )
96
+ end
97
+
98
+ def order_conditions
99
+ order_by = []
100
+ order_by << tagging_arel_table.project(tagging_arel_table[Arel.star].count.as('taggings_count')).order('taggings_count DESC').to_sql if options[:order_by_matching_tag_count].present? && options[:match_all].blank?
101
+
102
+ order_by << options[:order] if options[:order].present?
103
+ order_by.join(', ')
104
+ end
105
+
106
+ def tagging_alias(tag)
107
+ alias_base_name = taggable_model.base_class.name.downcase
108
+ adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag)}")
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,70 @@
1
+ module ActsAsTaggableOn::Taggable::TaggedWithQuery
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
9
+
10
+ private
11
+
12
+ def all_fields
13
+ taggable_arel_table[Arel.star]
14
+ end
15
+
16
+ def model_has_at_least_one_tag
17
+ tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists
18
+ end
19
+
20
+ def at_least_one_tag
21
+ exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
22
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
23
+ .and(
24
+ tagging_arel_table[:tag_id].in(
25
+ tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type)
26
+ )
27
+ )
28
+
29
+ if options[:start_at].present?
30
+ exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
31
+ end
32
+
33
+ if options[:end_at].present?
34
+ exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
35
+ end
36
+
37
+ if options[:on].present?
38
+ exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on]))
39
+ end
40
+
41
+ if (owner = options[:owned_by]).present?
42
+ exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id))
43
+ .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
44
+ end
45
+
46
+ exists_contition
47
+ end
48
+
49
+ def order_conditions
50
+ order_by = []
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
54
+
55
+ order_by << options[:order] if options[:order].present?
56
+ order_by.join(', ')
57
+ end
58
+
59
+ def alias_name(tag_list)
60
+ alias_base_name = taggable_model.base_class.name.downcase
61
+ taggings_context = options[:on] ? "_#{options[:on]}" : ''
62
+
63
+ taggings_alias = adjust_taggings_alias(
64
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag_list.join('_'))}"
65
+ )
66
+
67
+ taggings_alias
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,82 @@
1
+ module ActsAsTaggableOn::Taggable::TaggedWithQuery
2
+ class ExcludeTagsQuery < QueryBase
3
+ def build
4
+ taggable_model.joins(owning_to_tagger)
5
+ .where(tags_not_in_list)
6
+ .having(tags_that_matches_count)
7
+ .readonly(false)
8
+ end
9
+
10
+ private
11
+
12
+ def tags_not_in_list
13
+ return taggable_arel_table[:id].not_in(
14
+ tagging_arel_table
15
+ .project(tagging_arel_table[:taggable_id])
16
+ .join(tag_arel_table)
17
+ .on(
18
+ tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
19
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
20
+ .and(tags_match_type)
21
+ )
22
+ )
23
+
24
+ # FIXME: missing time scope, this is also missing in the original implementation
25
+ end
26
+
27
+
28
+ def owning_to_tagger
29
+ return [] unless options[:owned_by].present?
30
+
31
+ owner = options[:owned_by]
32
+
33
+ arel_join = taggable_arel_table
34
+ .join(tagging_arel_table)
35
+ .on(
36
+ tagging_arel_table[:tagger_id].eq(owner.id)
37
+ .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
38
+ .and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]))
39
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
40
+ )
41
+
42
+ if options[:match_all].present?
43
+ arel_join = arel_join
44
+ .join(tagging_arel_table, Arel::Nodes::OuterJoin)
45
+ .on(
46
+ match_all_on_conditions
47
+ )
48
+ end
49
+
50
+ return arel_join.join_sources
51
+ end
52
+
53
+ def match_all_on_conditions
54
+ on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
55
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
56
+
57
+ if options[:start_at].present?
58
+ on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
59
+ end
60
+
61
+ if options[:end_at].present?
62
+ on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
63
+ end
64
+
65
+ if options[:on].present?
66
+ on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
67
+ end
68
+
69
+ on_condition
70
+ end
71
+
72
+ def tags_that_matches_count
73
+ return [] unless options[:match_all].present?
74
+
75
+ taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
76
+
77
+ tagging_arel_table[:taggable_id].count.eq(
78
+ tag_arel_table.project(Arel.star.count).where(tags_match_type)
79
+ )
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ module ActsAsTaggableOn::Taggable::TaggedWithQuery
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
10
+
11
+ private
12
+
13
+ attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options
14
+
15
+ def taggable_arel_table
16
+ @taggable_arel_table ||= taggable_model.arel_table
17
+ end
18
+
19
+ def tag_arel_table
20
+ @tag_arel_table ||= tag_model.arel_table
21
+ end
22
+
23
+ def tagging_arel_table
24
+ @tagging_arel_table ||=tagging_model.arel_table
25
+ end
26
+
27
+ def tag_match_type(tag)
28
+ matches_attribute = tag_arel_table[:name]
29
+ matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
30
+
31
+ if options[:wild].present?
32
+ matches_attribute.matches("%#{escaped_tag(tag)}%", "!", ActsAsTaggableOn.strict_case_match)
33
+ else
34
+ matches_attribute.matches(escaped_tag(tag), "!", ActsAsTaggableOn.strict_case_match)
35
+ end
36
+ end
37
+
38
+ def tags_match_type
39
+ matches_attribute = tag_arel_table[:name]
40
+ matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
41
+
42
+ if options[:wild].present?
43
+ matches_attribute.matches_any(tag_list.map{|tag| "%#{escaped_tag(tag)}%"}, "!", ActsAsTaggableOn.strict_case_match)
44
+ else
45
+ matches_attribute.matches_any(tag_list.map{|tag| "#{escaped_tag(tag)}"}, "!", ActsAsTaggableOn.strict_case_match)
46
+ end
47
+ end
48
+
49
+ def escaped_tag(tag)
50
+ tag = tag.downcase unless ActsAsTaggableOn.strict_case_match
51
+ ActsAsTaggableOn::Utils.escape_like(tag)
52
+ end
53
+
54
+ def adjust_taggings_alias(taggings_alias)
55
+ if taggings_alias.size > 75
56
+ taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
57
+ end
58
+ taggings_alias
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'tagged_with_query/query_base'
2
+ require_relative 'tagged_with_query/exclude_tags_query'
3
+ require_relative 'tagged_with_query/any_tags_query'
4
+ require_relative 'tagged_with_query/all_tags_query'
5
+
6
+ module ActsAsTaggableOn::Taggable::TaggedWithQuery
7
+ def self.build(taggable_model, tag_model, tagging_model, tag_list, options)
8
+ if options[:exclude].present?
9
+ ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
10
+ elsif options[:any].present?
11
+ AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
12
+ else
13
+ AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
14
+ end
15
+ end
16
+ end
@@ -54,6 +54,23 @@ module ActsAsTaggableOn
54
54
  taggable_on(true, tag_types)
55
55
  end
56
56
 
57
+ def acts_as_taggable_tenant(tenant)
58
+ if taggable?
59
+ self.tenant_column = tenant
60
+ else
61
+ class_attribute :tenant_column
62
+ self.tenant_column = tenant
63
+ end
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
76
  # Make a model taggable on specified contexts
@@ -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'
@@ -96,7 +114,6 @@ module ActsAsTaggableOn
96
114
  include Cache
97
115
  include Ownership
98
116
  include Related
99
- include Dirty
100
117
  end
101
118
  end
102
119
  end
@@ -15,18 +15,19 @@ module ActsAsTaggableOn
15
15
  # end
16
16
  def acts_as_tagger(opts={})
17
17
  class_eval do
18
- has_many_with_taggable_compatibility :owned_taggings,
19
- opts.merge(
20
- as: :tagger,
21
- dependent: :destroy,
22
- class_name: '::ActsAsTaggableOn::Tagging'
23
- )
18
+ owned_taggings_scope = opts.delete(:scope)
24
19
 
25
- has_many_with_taggable_compatibility :owned_tags,
26
- through: :owned_taggings,
27
- source: :tag,
28
- class_name: '::ActsAsTaggableOn::Tag',
29
- uniq: true
20
+ has_many :owned_taggings, owned_taggings_scope,
21
+ **opts.merge(
22
+ as: :tagger,
23
+ class_name: '::ActsAsTaggableOn::Tagging',
24
+ dependent: :destroy
25
+ )
26
+
27
+ has_many :owned_tags, -> { distinct },
28
+ class_name: '::ActsAsTaggableOn::Tag',
29
+ source: :tag,
30
+ through: :owned_taggings
30
31
  end
31
32
 
32
33
  include ActsAsTaggableOn::Tagger::InstanceMethods
@@ -1,25 +1,20 @@
1
1
  module ActsAsTaggableOn
2
2
  class Tagging < ::ActiveRecord::Base #:nodoc:
3
- #TODO, remove from 4.0.0
4
- attr_accessible :tag,
5
- :tag_id,
6
- :context,
7
- :taggable,
8
- :taggable_type,
9
- :taggable_id,
10
- :tagger,
11
- :tagger_type,
12
- :tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
3
+ self.table_name = ActsAsTaggableOn.taggings_table
13
4
 
5
+ DEFAULT_CONTEXT = 'tags'
14
6
  belongs_to :tag, class_name: '::ActsAsTaggableOn::Tag', counter_cache: ActsAsTaggableOn.tags_counter
15
7
  belongs_to :taggable, polymorphic: true
16
- belongs_to :tagger, polymorphic: true
8
+
9
+ belongs_to :tagger, polymorphic: true, optional: true
17
10
 
18
11
  scope :owned_by, ->(owner) { where(tagger: owner) }
19
12
  scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
20
13
 
21
- scope :by_contexts, ->(contexts = ['tags']) { where(context: contexts) }
22
- scope :by_context, ->(context= 'tags') { by_contexts(context.to_s) }
14
+ scope :by_contexts, ->(contexts) { where(context: (contexts || DEFAULT_CONTEXT)) }
15
+ scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
16
+
17
+ scope :by_tenant, ->(tenant) { where(tenant: tenant) }
23
18
 
24
19
  validates_presence_of :context
25
20
  validates_presence_of :tag_id
@@ -35,7 +30,7 @@ module ActsAsTaggableOn
35
30
  if ActsAsTaggableOn.tags_counter
36
31
  tag.destroy if tag.reload.taggings_count.zero?
37
32
  else
38
- tag.destroy if tag.reload.taggings.count.zero?
33
+ tag.destroy if tag.reload.taggings.none?
39
34
  end
40
35
  end
41
36
  end
@@ -13,7 +13,6 @@ module ActsAsTaggableOn
13
13
  end
14
14
 
15
15
  def using_mysql?
16
- #We should probably use regex for mysql to support prehistoric adapters
17
16
  connection && connection.adapter_name == 'Mysql2'
18
17
  end
19
18
 
@@ -21,14 +20,14 @@ module ActsAsTaggableOn
21
20
  Digest::SHA1.hexdigest(string)[0..6]
22
21
  end
23
22
 
24
- def active_record4?
25
- ::ActiveRecord::VERSION::MAJOR == 4
26
- end
27
-
28
23
  def like_operator
29
24
  using_postgresql? ? 'ILIKE' : 'LIKE'
30
25
  end
31
26
 
27
+ def legacy_activerecord?
28
+ ActiveRecord.version <= Gem::Version.new('5.3.0')
29
+ end
30
+
32
31
  # escape _ and % characters in strings, since these are wildcards in SQL.
33
32
  def escape_like(str)
34
33
  str.gsub(/[!%_]/) { |x| '!' + x }
@@ -1,4 +1,3 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '3.5.0'
2
+ VERSION = '8.1.0'
3
3
  end
4
-
@@ -73,14 +73,6 @@ describe 'Acts As Taggable On' do
73
73
  end
74
74
  end
75
75
 
76
- describe 'Reloading' do
77
- it 'should save a model instantiated by Model.find' do
78
- taggable = TaggableModel.create!(name: 'Taggable')
79
- found_taggable = TaggableModel.find(taggable.id)
80
- found_taggable.save
81
- end
82
- end
83
-
84
76
  describe 'Matching Contexts' do
85
77
  it 'should find objects with tags of matching contexts' do
86
78
  taggable1 = TaggableModel.create!(name: 'Taggable 1')
@@ -142,7 +134,7 @@ describe 'Acts As Taggable On' do
142
134
  taggable1.save
143
135
 
144
136
  column = TaggableModel.connection.quote_column_name("context")
145
- offer_alias = TaggableModel.connection.quote_table_name("taggings")
137
+ offer_alias = TaggableModel.connection.quote_table_name(ActsAsTaggableOn.taggings_table)
146
138
  need_alias = TaggableModel.connection.quote_table_name("need_taggings_taggable_models_join")
147
139
 
148
140
  expect(TaggableModel.joins(:offerings, :needs).to_sql).to include "#{offer_alias}.#{column}"
@@ -154,7 +146,7 @@ describe 'Acts As Taggable On' do
154
146
  describe 'Tagging Contexts' do
155
147
  it 'should eliminate duplicate tagging contexts ' do
156
148
  TaggableModel.acts_as_taggable_on(:skills, :skills)
157
- expect(TaggableModel.tag_types.freq[:skills]).to_not eq(3)
149
+ expect(TaggableModel.tag_types.freq[:skills]).to eq(1)
158
150
  end
159
151
 
160
152
  it 'should not contain embedded/nested arrays' do
@@ -178,6 +170,15 @@ describe 'Acts As Taggable On' do
178
170
  TaggableModel.acts_as_taggable_on([nil])
179
171
  }).to_not raise_error
180
172
  end
173
+
174
+ it 'should include dynamic contexts in tagging_contexts' do
175
+ taggable = TaggableModel.create!(name: 'Dynamic Taggable')
176
+ taggable.set_tag_list_on :colors, 'tag1, tag2, tag3'
177
+ expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
178
+ taggable.save
179
+ taggable = TaggableModel.where(name: 'Dynamic Taggable').first
180
+ expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
181
+ end
181
182
  end
182
183
 
183
184
  context 'when tagging context ends in an "s" when singular (ex. "status", "glass", etc.)' do
@@ -191,7 +192,7 @@ describe 'Acts As Taggable On' do
191
192
  its(:cached_glass_list) { should be_blank }
192
193
 
193
194
  context 'language taggings cache after update' do
194
- before { @taggable.update_attributes(language_list: 'ruby, .net') }
195
+ before { @taggable.update(language_list: 'ruby, .net') }
195
196
  subject { @taggable }
196
197
 
197
198
  its(:language_list) { should == ['ruby', '.net']}
@@ -200,7 +201,7 @@ describe 'Acts As Taggable On' do
200
201
  end
201
202
 
202
203
  context 'status taggings cache after update' do
203
- before { @taggable.update_attributes(status_list: 'happy, married') }
204
+ before { @taggable.update(status_list: 'happy, married') }
204
205
  subject { @taggable }
205
206
 
206
207
  its(:status_list) { should == ['happy', 'married'] }
@@ -213,7 +214,7 @@ describe 'Acts As Taggable On' do
213
214
 
214
215
  context 'glass taggings cache after update' do
215
216
  before do
216
- @taggable.update_attributes(glass_list: 'rectangle, aviator')
217
+ @taggable.update(glass_list: 'rectangle, aviator')
217
218
  end
218
219
 
219
220
  subject { @taggable }
@@ -72,7 +72,7 @@ describe 'acts_as_tagger' do
72
72
  expect(@taggable.tag_list_on(:foo_boo)).to be_empty
73
73
  expect(-> {
74
74
  @tagger.tag(@taggable, with: 'this, and, that', on: :foo_boo, force: false)
75
- }).to raise_error
75
+ }).to raise_error(RuntimeError)
76
76
  end
77
77
 
78
78
  it 'should not create the tag context on-the-fly when the default is over-ridden' do