acts-as-taggable-on-mongoid 6.0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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