acts-as-taggable-on-mongoid 6.0.1.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +63 -0
  3. data/.gitignore +54 -0
  4. data/.reek.yml +8 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +59 -0
  7. data/.ruby-version +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +10 -0
  10. data/Gemfile.lock +203 -0
  11. data/LICENSE.txt +21 -0
  12. data/PULL_REQUEST_TEMPLATE.md +11 -0
  13. data/README.md +741 -0
  14. data/Rakefile +8 -0
  15. data/acts-as-taggable-on-mongoid.gemspec +54 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/codecov.yml +3 -0
  19. data/config/pronto-circleci.yml +7 -0
  20. data/lib/acts-as-taggable-on-mongoid.rb +80 -0
  21. data/lib/acts_as_taggable_on_mongoid/configuration.rb +94 -0
  22. data/lib/acts_as_taggable_on_mongoid/default_parser.rb +120 -0
  23. data/lib/acts_as_taggable_on_mongoid/errors/duplicate_tag_error.rb +9 -0
  24. data/lib/acts_as_taggable_on_mongoid/generic_parser.rb +44 -0
  25. data/lib/acts_as_taggable_on_mongoid/models/tag.rb +103 -0
  26. data/lib/acts_as_taggable_on_mongoid/models/tagging.rb +80 -0
  27. data/lib/acts_as_taggable_on_mongoid/tag_list.rb +169 -0
  28. data/lib/acts_as_taggable_on_mongoid/taggable.rb +131 -0
  29. data/lib/acts_as_taggable_on_mongoid/taggable/changeable.rb +71 -0
  30. data/lib/acts_as_taggable_on_mongoid/taggable/core.rb +219 -0
  31. data/lib/acts_as_taggable_on_mongoid/taggable/list_tags.rb +45 -0
  32. data/lib/acts_as_taggable_on_mongoid/taggable/tag_type_definition.rb +189 -0
  33. data/lib/acts_as_taggable_on_mongoid/taggable/tag_type_definition/attributes.rb +77 -0
  34. data/lib/acts_as_taggable_on_mongoid/taggable/tag_type_definition/changeable.rb +140 -0
  35. data/lib/acts_as_taggable_on_mongoid/taggable/tag_type_definition/names.rb +39 -0
  36. data/lib/acts_as_taggable_on_mongoid/taggable/utils/tag_list_diff.rb +121 -0
  37. data/lib/acts_as_taggable_on_mongoid/version.rb +5 -0
  38. metadata +352 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Taggable
5
+ # Overides of methods from Mongoid::Changeable
6
+ module Changeable
7
+ def tag_list_on_changed(tag_definition)
8
+ attribute_will_change!(tag_definition.tag_list_name)
9
+ end
10
+
11
+ def reload(*args)
12
+ tag_types.each_value do |tag_definition|
13
+ instance_variable_set tag_definition.all_tag_list_variable_name, nil
14
+ instance_variable_set tag_definition.tag_list_variable_name, nil
15
+ end
16
+
17
+ super(*args)
18
+ end
19
+
20
+ def changed
21
+ changed_values = super
22
+ tag_list_names = tag_types.values.map(&:tag_list_name).map(&:to_s)
23
+
24
+ changed_attributes.each_key do |key|
25
+ next unless tag_list_names.include?(key.to_s)
26
+
27
+ if public_send("#{key}_changed?")
28
+ changed_values << key unless changed_values.include?(key)
29
+ else
30
+ changed_values.delete(key)
31
+ end
32
+ end
33
+
34
+ changed_values
35
+ end
36
+
37
+ def changes
38
+ changed_values = super
39
+
40
+ tag_types.each_value do |tag_definition|
41
+ tag_list_name = tag_definition.tag_list_name
42
+
43
+ next unless changed_attributes.key? tag_list_name
44
+
45
+ changed_values[tag_list_name] = [changed_attributes[tag_list_name], public_send(tag_list_name)]
46
+ end
47
+
48
+ changed_values
49
+ end
50
+
51
+ def setters
52
+ setter_values = super
53
+ tag_list_names = tag_types.values.map(&:tag_list_name).map(&:to_s)
54
+
55
+ setter_values.delete_if do |key, _value|
56
+ tag_list_names.include?(key.to_s)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def attribute_will_change!(attribute_name)
63
+ return super if tag_types.none? { |_tag_name, tag_definition| tag_definition.tag_list_name.to_s == attribute_name.to_s }
64
+
65
+ return if changed_attributes.key?(attribute_name)
66
+
67
+ changed_attributes[attribute_name] = public_send(attribute_name)&.dup
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Taggable
5
+ # A collection of generic methods which use the tag definition to perform actions.
6
+ #
7
+ # These methods are called by the individual tag generated methods to do their work so that
8
+ # the code can be defined in only one location and "shared" by all tags rather than putting the code
9
+ # and definitions into the dynamically defined methods directly.
10
+ #
11
+ # This module actually consists almost exclusively of utility functions
12
+
13
+ # :reek:FeatureEnvy
14
+ # :reek:UtilityFunction
15
+ module Core
16
+ extend ActiveSupport::Concern
17
+
18
+ DYNAMIC_MODULE_NAME = :DynamicAttributes
19
+
20
+ included do
21
+ # TODO: allow custom contexts
22
+ # attr_writer :custom_contexts
23
+
24
+ after_save :save_tags
25
+ end
26
+
27
+ class_methods do
28
+ def taggable_mixin
29
+ # https://thepugautomatic.com/2013/07/dsom/
30
+ # Provides a description of what we're doing here and why.
31
+ if const_defined?(DYNAMIC_MODULE_NAME, false)
32
+ mod = const_get(DYNAMIC_MODULE_NAME)
33
+ else
34
+ mod = const_set(DYNAMIC_MODULE_NAME, Module.new)
35
+
36
+ include mod
37
+ end
38
+
39
+ mod
40
+ end
41
+ end
42
+
43
+ def apply_pre_processed_defaults
44
+ super
45
+
46
+ set_tag_list_defaults
47
+ end
48
+
49
+ private
50
+
51
+ def tag_list_cache_set_on(tag_definition)
52
+ variable_name = tag_definition.tag_list_variable_name
53
+
54
+ instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
55
+ end
56
+
57
+ def tag_list_cache_on(tag_definition)
58
+ variable_name = tag_definition.tag_list_variable_name
59
+
60
+ # if instance_variable_get(variable_name)
61
+ # instance_variable_get(variable_name)
62
+ # elsif cached_tag_list_on(tag_definition) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(tag_definition)
63
+ # instance_variable_set(variable_name, tag_definition.parse(cached_tag_list_on(tag_definition)))
64
+ # else
65
+ # tag_list_set(ActsAsTaggableOnMongoid::TagList.new(tag_definition, tags_on(tag_definition).map(&:tag_name)))
66
+ # end
67
+
68
+ instance_variable_get(variable_name) ||
69
+ tag_list_set(ActsAsTaggableOnMongoid::TagList.new(tag_definition, tags_on(tag_definition).map(&:tag_name)))
70
+ end
71
+
72
+ def tag_list_on(tag_definition)
73
+ # add_custom_context(tag_definition)
74
+
75
+ tag_list_cache_on(tag_definition)
76
+ end
77
+
78
+ def all_tags_list_on(tag_definition)
79
+ variable_name = tag_definition.all_tag_list_variable_name
80
+ cached_variable = instance_variable_get(variable_name)
81
+
82
+ return cached_variable if instance_variable_defined?(variable_name) && cached_variable
83
+
84
+ instance_variable_set(variable_name, ActsAsTaggableOnMongoid::TagList.new(tag_definition, all_tags_on(tag_definition).map(&:name)).freeze)
85
+ end
86
+
87
+ ##
88
+ # Returns all tags of a given context
89
+ def all_tags_on(tag_definition)
90
+ tag_definition.tags_table.for_tag(tag_definition).to_a
91
+ end
92
+
93
+ ##
94
+ # Returns all tags that are not owned of a given context
95
+ def tags_on(tag_definition)
96
+ # scope = public_send(tag_definition.taggings_name).where(context: tag_definition.tag_type, tagger_id: nil)
97
+ scope = public_send(tag_definition.taggings_name).where(context: tag_definition.tag_type)
98
+
99
+ # when preserving tag order, return tags in created order
100
+ # if we added the order to the association this would always apply
101
+ scope = scope.order_by(*tag_definition.taggings_order) if tag_definition.preserve_tag_order?
102
+
103
+ scope
104
+ end
105
+
106
+ def mark_tag_list_changed(new_list)
107
+ tag_definition = new_list.tag_definition
108
+ current_tag_list = public_send(tag_definition.tag_list_name)
109
+
110
+ if (tag_definition.preserve_tag_order? && new_list != current_tag_list) ||
111
+ new_list.sort != current_tag_list.sort
112
+ current_tag_list.notify_will_change
113
+ end
114
+ end
115
+
116
+ def tag_list_set(new_list)
117
+ # add_custom_context(tag_definition, owner)
118
+
119
+ new_list.taggable = self
120
+
121
+ instance_variable_set(new_list.tag_definition.tag_list_variable_name, new_list)
122
+ end
123
+
124
+ ##
125
+ # Find existing tags or create non-existing tags
126
+ def load_tags(tag_definition, tag_list)
127
+ tag_definition.tags_table.find_or_create_all_with_like_by_name(tag_definition, tag_list)
128
+ end
129
+
130
+ def set_tag_list_defaults
131
+ return unless new_record?
132
+
133
+ tag_types.each_value do |tag_definition|
134
+ default = tag_definition.default
135
+ next unless default.present?
136
+
137
+ tag_list_name = tag_definition.tag_list_name
138
+ next if public_send(tag_list_name).present?
139
+
140
+ public_send("#{tag_list_name}=", default)
141
+ end
142
+ end
143
+
144
+ def save_tags
145
+ # Don't call save_tags again if a related classes save while processing this funciton causes this object to re-save.
146
+ return if @saving_tag_list
147
+
148
+ @saving_tag_list = true
149
+
150
+ tag_types.each_value do |tag_definition|
151
+ next unless tag_list_cache_set_on(tag_definition)
152
+
153
+ # List of currently assigned tag names
154
+ tag_list_diff = extract_tag_list_changes(tag_definition)
155
+
156
+ # Destroy old taggings:
157
+ tag_list_diff.destroy_old_tags self
158
+
159
+ # Create new taggings:
160
+ tag_list_diff.create_new_tags self
161
+ end
162
+
163
+ @saving_tag_list = false
164
+
165
+ true
166
+ end
167
+
168
+ def extract_tag_list_changes(tag_definition)
169
+ tag_list = tag_list_cache_on(tag_definition).uniq
170
+
171
+ # Find existing tags or create non-existing tags:
172
+ tags = find_or_create_tags_from_list_with_context(tag_definition, tag_list)
173
+ current_tags = tags_on(tag_definition).map(&:tag).compact
174
+
175
+ tag_list_diff = ActsAsTaggableOnMongoid::Taggable::Utils::TagListDiff.new tag_definition: tag_definition,
176
+ tags: tags,
177
+ current_tags: current_tags
178
+
179
+ tag_list_diff.call
180
+
181
+ tag_list_diff
182
+ end
183
+
184
+ def dirtify_tag_list(tagging)
185
+ tag_definition = tag_types[tagging.context]
186
+
187
+ return unless tag_definition
188
+
189
+ attribute_will_change! tag_definition.tag_list_name
190
+ end
191
+
192
+ ##
193
+ # Imported from `ActsAsTaggableOn`. It is simply easier to define a custom Tag class and define
194
+ # the tag to use that Tag class.
195
+ #
196
+ # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
197
+ # context is provided so that you may conditionally use a Tag subclass
198
+ # only for some contexts.
199
+ #
200
+ # @example Custom Tag class for one context
201
+ # class Company < ActiveRecord::Base
202
+ # acts_as_taggable_on :markets, :locations
203
+ #
204
+ # def find_or_create_tags_from_list_with_context(tag_list)
205
+ # if context.to_sym == :markets
206
+ # MarketTag.find_or_create_all_with_like_by_name(tag_list)
207
+ # else
208
+ # super
209
+ # end
210
+ # end
211
+ #
212
+ # @param [Array<String>] tag_list Tags to find or create
213
+ # @param [Symbol] context The tag context for the tag_list
214
+ def find_or_create_tags_from_list_with_context(tag_definition, tag_list)
215
+ load_tags(tag_definition, tag_list)
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Taggable
5
+ # This module adds methods for tracking tag definitions within Taggable classes
6
+ module ListTags
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :tag_types
11
+
12
+ self.tag_types ||= {}.with_indifferent_access
13
+ end
14
+
15
+ def tag_definition(tag_type)
16
+ return unless tag_type.present?
17
+
18
+ tag_types[tag_type] ||= ActsAsTaggableOnMongoid::Taggable::TagTypeDefinition.new(self, tag_type)
19
+ end
20
+
21
+ class_methods do
22
+ # In order to allow dynamic tags, return a default tag_definition for any missing tag_type.
23
+ # This means that any dynamic tag necessarily is created with the current defaults
24
+ def define_tag(tag_type, options = {})
25
+ return if tag_type.blank?
26
+
27
+ tag_definition = tag_types[tag_type]
28
+
29
+ return tag_definition if tag_definition
30
+
31
+ # tag_types is a class_attribute
32
+ # As such, we have to replace it each time with a new array so that inherited classes and instances
33
+ # are able to maintain separate lists if need be.
34
+ new_tag_types = {}.with_indifferent_access.merge!(self.tag_types || {})
35
+ self.tag_types = new_tag_types
36
+ tag_definition = new_tag_types[tag_type] = ActsAsTaggableOnMongoid::Taggable::TagTypeDefinition.new(self, tag_type, options)
37
+
38
+ tag_definition.define_base_relations
39
+ tag_definition.define_relations
40
+ tag_definition.add_tag_list
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Taggable
5
+ # TagTypeDefinition represents the definition for a aingle tag_type in a model.
6
+ #
7
+ # The options passed into a tag_type defined through acts_as_taggable* method are stored in the tag definition
8
+ # which then drives the creation of the relations and methods that are added to the model.
9
+ #
10
+ # The TagTypeDefinition mirrors the Configuraiton attributes and defaults any value that isn't passed in
11
+ # to the value in the Configuration (ActsAsTaggableOnMongoid.configuration)
12
+ class TagTypeDefinition
13
+ attr_reader :owner,
14
+ :tag_type
15
+
16
+ include ActsAsTaggableOnMongoid::Taggable::TagTypeDefinition::Attributes
17
+ include ActsAsTaggableOnMongoid::Taggable::TagTypeDefinition::Names
18
+ include ActsAsTaggableOnMongoid::Taggable::TagTypeDefinition::Changeable
19
+
20
+ def initialize(owner, tag_type, options = {})
21
+ options.assert_valid_keys(:parser,
22
+ :preserve_tag_order,
23
+ :cached_in_model,
24
+ :force_lowercase,
25
+ :force_parameterize,
26
+ :remove_unused_tags,
27
+ :tags_table,
28
+ :taggings_table,
29
+ :default)
30
+
31
+ self.default_value = options.delete(:default)
32
+
33
+ options.each do |key, value|
34
+ instance_variable_set("@#{key}", value)
35
+ end
36
+
37
+ @owner = owner
38
+ @tag_type = tag_type
39
+ end
40
+
41
+ # rubocop:disable Layout/SpaceAroundOperators
42
+ # :reek:FeatureEnvy
43
+
44
+ # I've defined the parser as being required to return an array of strings.
45
+ # This parse function will take that array and make it a TagList which will then use the tag_definition
46
+ # to apply the rules to that list (like case sensitivity and parameterization, etc.) to get the final
47
+ # list.
48
+ def parse(*tag_list)
49
+ options = tag_list.extract_options!
50
+ options[:parser] ||= parser if options.key?(:parse) || options.key?(:parser)
51
+
52
+ ActsAsTaggableOnMongoid::TagList.new(self, *tag_list, options)
53
+ end
54
+
55
+ # rubocop:enable Layout/SpaceAroundOperators
56
+
57
+ def taggings_order
58
+ @taggings_order = if preserve_tag_order?
59
+ [:created_at.asc, :id.asc]
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def define_base_relations
66
+ tag_definition = self
67
+
68
+ add_base_tags_method
69
+
70
+ owner.class_eval do
71
+ taggings_name = tag_definition.taggings_name
72
+
73
+ break if relations[taggings_name.to_s]
74
+
75
+ has_many taggings_name,
76
+ as: :taggable,
77
+ dependent: :destroy,
78
+ class_name: tag_definition.taggings_table.name,
79
+ after_add: :dirtify_tag_list,
80
+ after_remove: :dirtify_tag_list
81
+ end
82
+ end
83
+
84
+ # Mongoid does not allow the `through` option for relations, so we de-normalize data and manually add the methods we need
85
+ # for through like functionality.
86
+ def add_base_tags_method
87
+ tag_definition = self
88
+ base_tags_method = tag_definition.base_tags_method
89
+
90
+ owner.taggable_mixin.module_eval do
91
+ break if methods.include?(base_tags_method)
92
+
93
+ define_method base_tags_method do
94
+ tag_definition.tags_table.where(taggable_type: tag_definition.owner.name)
95
+ end
96
+ end
97
+ end
98
+
99
+ def define_relations
100
+ # Relations cannot be added for the tags and taggings like they are in ActiveRecord because
101
+ # Mongoid does not allow for a scope like ActiveRecord does.
102
+ #
103
+ # Therefore the relation like actions will have to be defined separately ourselves. If any
104
+ # relation actions are missed, we'll just have to fix it here when we find them.
105
+ # (This is far from ideal, but it is the only way to work around the issue at this time.)
106
+
107
+ add_context_taggings_method
108
+ add_context_tags_method
109
+ end
110
+
111
+ def add_context_taggings_method
112
+ tag_definition = self
113
+ taggings_name = tag_definition.taggings_name
114
+
115
+ owner.taggable_mixin.module_eval do
116
+ define_method "#{tag_definition.single_tag_type}_#{taggings_name}".to_sym do
117
+ public_send(taggings_name).
118
+ order_by(*tag_definition.taggings_order).
119
+ for_tag(tag_definition)
120
+ end
121
+ end
122
+ end
123
+
124
+ def add_context_tags_method
125
+ tag_definition = self
126
+
127
+ owner.taggable_mixin.module_eval do
128
+ define_method tag_definition.tag_type.to_sym do
129
+ public_send("#{tag_definition.single_tag_type}_#{tag_definition.taggings_name}").map(&:tag)
130
+ end
131
+ end
132
+ end
133
+
134
+ def add_tag_list
135
+ add_list_getter
136
+ add_list_setter
137
+ add_all_list_getter
138
+ add_list_exists
139
+ add_list_change
140
+ add_list_changed
141
+ add_changed_from_default?
142
+ add_will_change
143
+ add_get_was
144
+ add_reset_list
145
+ add_reset_to_default
146
+ end
147
+
148
+ def add_list_getter
149
+ tag_definition = self
150
+ tag_list_name = tag_definition.tag_list_name
151
+
152
+ owner.taggable_mixin.module_eval do
153
+ define_method(tag_list_name) do
154
+ tag_list_on tag_definition
155
+ end
156
+
157
+ alias_method "#{tag_list_name}_before_type_cast".to_sym, tag_list_name.to_sym
158
+ end
159
+ end
160
+
161
+ def add_list_setter
162
+ tag_definition = self
163
+
164
+ owner.taggable_mixin.module_eval do
165
+ define_method("#{tag_definition.tag_list_name}=") do |new_tags|
166
+ new_tags = Array.wrap(new_tags)
167
+ options = new_tags.extract_options!
168
+ options[:parse] = true unless options.key?(:parse)
169
+
170
+ new_list = tag_definition.parse(*new_tags, options)
171
+
172
+ mark_tag_list_changed(new_list)
173
+ tag_list_set(new_list)
174
+ end
175
+ end
176
+ end
177
+
178
+ def add_all_list_getter
179
+ tag_definition = self
180
+
181
+ owner.taggable_mixin.module_eval do
182
+ define_method(tag_definition.all_tag_list_name) do
183
+ all_tags_list_on tag_definition
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end