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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Models
5
+ # A class representing all tags that have ever been set on a model.
6
+ class Tag
7
+ include Mongoid::Document
8
+ include Mongoid::Timestamps
9
+
10
+ field :name, type: String
11
+ field :taggings_count, type: Integer, default: 0
12
+ field :context, type: String
13
+ field :taggable_type, type: String
14
+
15
+ # field :type, type: String
16
+
17
+ index({ name: 1, taggable_type: 1, context: 1 }, unique: true)
18
+
19
+ ### ASSOCIATIONS:
20
+
21
+ has_many :taggings, dependent: :destroy, class_name: "ActsAsTaggableOnMongoid::Models::Tagging"
22
+
23
+ ### VALIDATIONS:
24
+
25
+ validates :name, presence: true
26
+ validates :context, presence: true
27
+ validates :taggable_type, presence: true
28
+ validates :name, uniqueness: { scope: %i[context taggable_type] }
29
+
30
+ ### SCOPES:
31
+ scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
32
+ scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }
33
+
34
+ scope :named, ->(name) { where(name: as_8bit_ascii(name)) }
35
+ scope :named_any, ->(*names) { where(:name.in => names.map { |name| as_8bit_ascii(name) }) }
36
+ scope :named_like, ->(name) { where(name: /#{as_8bit_ascii(name)}/i) }
37
+ scope :named_like_any, ->(*names) { where(:name.in => names.map { |name| /#{as_8bit_ascii(name)}/i }) }
38
+ scope :for_context, ->(context) { where(context: context) }
39
+ scope :for_taggable_class, ->(taggable_type) { where(taggable_type: taggable_type.name) }
40
+ scope :for_tag, ->(tag_definition) { for_taggable_class(tag_definition.owner).for_context(tag_definition.tag_type) }
41
+
42
+ ### CLASS METHODS:
43
+
44
+ class << self
45
+ def find_or_create_all_with_like_by_name(tag_definition, *list)
46
+ list = ActsAsTaggableOnMongoid::TagList.new(tag_definition, *Array.wrap(list).flatten)
47
+
48
+ return [] if list.empty?
49
+
50
+ list.map do |tag_name|
51
+ begin
52
+ tries ||= 3
53
+
54
+ existing_tag = tag_definition.tags_table.for_tag(tag_definition).named(tag_name).first
55
+
56
+ existing_tag || create_tag(tag_definition, tag_name)
57
+ rescue Mongoid::Errors::Validations
58
+ # :nocov:
59
+ if (tries -= 1).positive?
60
+ retry
61
+ end
62
+
63
+ raise ActsAsTaggableOnMongoid::Errors::DuplicateTagError.new, "'#{tag_name}' has already been taken"
64
+ # :nocov:
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def create_tag(tag_definition, name)
72
+ tag_definition.tags_table.create(name: name,
73
+ context: tag_definition.tag_type,
74
+ taggable_type: tag_definition.owner.name)
75
+ end
76
+
77
+ def as_8bit_ascii(string)
78
+ string = string.to_s
79
+ if defined?(Encoding)
80
+ string.dup.force_encoding("BINARY")
81
+ else
82
+ # :nocov:
83
+ string.mb_chars
84
+ # :nocov:
85
+ end
86
+ end
87
+ end
88
+
89
+ ### INSTANCE METHODS:
90
+
91
+ def ==(other)
92
+ super || (other.class == self.class &&
93
+ name == other.name &&
94
+ context == other.context &&
95
+ taggable_type == other.taggable_type)
96
+ end
97
+
98
+ def to_s
99
+ name
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ module Models
5
+ # A class representing the actual tags assigned to a particular model object
6
+ class Tagging
7
+ include Mongoid::Document
8
+ include Mongoid::Timestamps
9
+
10
+ DEFAULT_CONTEXT = "tags"
11
+
12
+ after_save :tagging_saved
13
+ after_destroy :tagging_destroyed
14
+
15
+ field :tag_name, type: String
16
+ field :context, type: String
17
+
18
+ belongs_to :tag, counter_cache: true, inverse_of: :taggings
19
+ belongs_to :taggable, polymorphic: true
20
+ # belongs_to :tagger, { polymorphic: true, optional: true }
21
+
22
+ # If/when adding the concept of a tagger, this index will need to be changed.
23
+ index({ taggable_id: 1, taggable_type: 1, context: 1, tag_name: 1 }, unique: true, name: "tagging_taggable_context_tag_name")
24
+ index(tag_name: 1)
25
+ index(tag_id: 1, tag_type: 1)
26
+
27
+ # scope :owned_by, ->(owner) { where(tagger: owner) }
28
+ # scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
29
+
30
+ scope :by_contexts, ->(*contexts) { where(:context.in => Array.wrap(contexts.presence || DEFAULT_CONTEXT)) }
31
+ scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
32
+ scope :for_tag, ->(tag_definition) { where(taggable_type: tag_definition.owner.name).by_context(tag_definition.tag_type) }
33
+
34
+ validates :tag_name, presence: true
35
+ validates :context, presence: true
36
+ validates :tag, presence: true
37
+ validates :taggable, presence: true
38
+
39
+ # validates :tag_id, uniqueness: {scope: [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]}
40
+ validates :tag_name, uniqueness: { scope: %i[taggable_type taggable_id context] }
41
+ # validates :tag_id, uniqueness: {scope: [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]}
42
+ validates :tag_id, uniqueness: { scope: %i[taggable_type taggable_id context] }
43
+
44
+ after_destroy :remove_unused_tags
45
+
46
+ private
47
+
48
+ def remove_unused_tags
49
+ return nil unless taggable
50
+
51
+ tag_definition = taggable.tag_types[context]
52
+
53
+ return unless tag_definition&.remove_unused_tags?
54
+
55
+ tag.destroy if tag.reload.taggings_count.zero?
56
+ end
57
+
58
+ def tagging_saved
59
+ tag_definition = taggable.tag_types[context]
60
+
61
+ return unless tag_definition
62
+
63
+ tag_list = taggable.public_send(tag_definition.tag_list_name)
64
+ tag_list.add_tagging(self)
65
+ end
66
+
67
+ def tagging_destroyed
68
+ taggable_was = taggable_type_was.constantize.where(id: taggable_id_was).first
69
+
70
+ return unless taggable_was
71
+
72
+ tag_definition = taggable_was.tag_types[context_was]
73
+
74
+ return unless tag_definition
75
+
76
+ taggable_was.public_send(tag_definition.tag_list_name).remove(tag_name_was)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require "active_support/core_ext/module/delegation"
4
+
5
+ module ActsAsTaggableOnMongoid
6
+ # A list of tags. The TagList must be initialized with a tag definition so that it knows how to clean
7
+ # the list properly and to convert the list to a string.
8
+ #
9
+ # All methods that add objects to the list (initialization, concat, etc.) optionally take an array of values including
10
+ # options to parse the values and to optionally specifiy the parser to use. If no parser is specified, then
11
+ # parser for the tag_definition is used.
12
+ #
13
+ # If the input value(s) are to be parsed, then all values passed in are parsed.
14
+ #
15
+ # Examples:
16
+ # TagList.new(tag_definition, "value 1", "value 2")
17
+ # # > TagList<> ["value 1", "value 2"]
18
+ #
19
+ # TagList.new(tag_definition, "value 1, value 2", parse: true)
20
+ # # > TagList<> ["value 1", "value 2"]
21
+ #
22
+ # TagList.new(tag_definition, "value 1, value 2", "value 3, value 4", parse: true)
23
+ # # > TagList<> ["value 1", "value 2", "value 3", "value 4"]
24
+ #
25
+ # TagList.new(tag_definition, "value 1, value 2", "value 3, value 4", parser: ActsAsTaggableOnMongoid::GenericParser)
26
+ # # > TagList<> ["value 1", "value 2", "value 3", "value 4"]
27
+
28
+ # :reek:MissingSafeMethod
29
+ # :reek:SubclassedFromCoreClass
30
+ class TagList < Array
31
+ # :reek:Attribute
32
+ attr_accessor :taggable
33
+ attr_reader :tag_definition
34
+
35
+ def initialize(tag_definition, *args)
36
+ @tag_definition = tag_definition
37
+
38
+ add(*args)
39
+ end
40
+
41
+ ##
42
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
43
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
44
+ #
45
+ # Example:
46
+ # tag_list.add("Fun", "Happy")
47
+ # tag_list.add("Fun, Happy", :parse => true)
48
+ def add(*names)
49
+ extract_and_apply_options!(names)
50
+ concat(names)
51
+ clean!
52
+
53
+ self
54
+ end
55
+
56
+ # Append---Add the tag to the tag_list. This
57
+ # expression returns the tag_list itself, so several appends
58
+ # may be chained together.
59
+ def <<(obj)
60
+ add(obj)
61
+ end
62
+
63
+ # Concatenation --- Returns a new tag list built by concatenating the
64
+ # two tag lists together to produce a third tag list.
65
+ def +(other)
66
+ TagList.new(tag_definition).add(*self).add(other)
67
+ end
68
+
69
+ # Appends the elements of +other_tag_list+ to +self+.
70
+ def concat(other_tag_list)
71
+ notify_will_change
72
+
73
+ super(other_tag_list).send(:clean!)
74
+
75
+ self
76
+ end
77
+
78
+ ##
79
+ # Remove specific tags from the tag_list.
80
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
81
+ #
82
+ # Example:
83
+ # tag_list.remove("Sad", "Lonely")
84
+ # tag_list.remove("Sad, Lonely", :parse => true)
85
+ def remove(*names)
86
+ remove_list = ActsAsTaggableOnMongoid::TagList.new(tag_definition, *names)
87
+
88
+ notify_will_change
89
+
90
+ delete_if { |name| remove_list.include?(name) }
91
+
92
+ self
93
+ end
94
+
95
+ ##
96
+ # Transform the tag_list into a tag string suitable for editing in a form.
97
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
98
+ #
99
+ # Example:
100
+ # tag_list = TagList.new("Round", "Square,Cube")
101
+ # tag_list.to_s # 'Round, "Square,Cube"'
102
+ def to_s
103
+ tag_definition.parser.new(*self).to_s
104
+ end
105
+
106
+ def notify_will_change
107
+ return unless taggable
108
+
109
+ taggable.tag_list_on_changed tag_definition
110
+ end
111
+
112
+ # :reek:ManualDispatch
113
+ def ==(other)
114
+ if tag_definition.preserve_tag_order?
115
+ super
116
+ elsif other.respond_to?(:sort)
117
+ self&.sort == other.sort
118
+ end
119
+ end
120
+
121
+ def add_tagging(tagging)
122
+ orig_taggable = taggable
123
+ @taggable = nil
124
+
125
+ begin
126
+ tag = tagging.tag_name
127
+
128
+ self << tag unless include?(tag)
129
+ ensure
130
+ @taggable = orig_taggable
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def clean!
137
+ reject!(&:blank?)
138
+
139
+ map!(&:to_s)
140
+ map!(&:strip)
141
+
142
+ conditional_clean_rules
143
+ end
144
+
145
+ def conditional_clean_rules
146
+ map! { |tag| tag.mb_chars.downcase.to_s } if tag_definition.force_lowercase?
147
+ map!(&:parameterize) if tag_definition.force_parameterize?
148
+
149
+ uniq!
150
+
151
+ self
152
+ end
153
+
154
+ # :reek:FeatureEnvy
155
+ # :reek:DuplicateMethodCall
156
+ def extract_and_apply_options!(args)
157
+ options = args.extract_options!
158
+ options.assert_valid_keys :parse, :parser
159
+
160
+ options_parser = options[:parser]
161
+ run_parser = options_parser || tag_definition.parser
162
+
163
+ args.flatten!
164
+ args.map! { |argument| run_parser.new(argument).parse } if options[:parse] || options_parser
165
+
166
+ args.flatten!
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOnMongoid
4
+ # This module defines the class methods to be added to the Mongoid model so that
5
+ # tags can be defined on and added to a model.
6
+ #
7
+ # When a tag is added to a model, additional modules will be included to add methods that
8
+ # are needed only if a tag is actually being used.
9
+ module Taggable
10
+ extend ActiveSupport::Concern
11
+
12
+ # rubocop:disable Metrics/BlockLength
13
+
14
+ class_methods do
15
+ # Options include:
16
+ # * parser
17
+ # The class to be used to parse strings.
18
+ # * preserve_tag_order
19
+ # If true, the _list accessor will save and returns tags in the order they are added to the object.
20
+ # * cached_in_model
21
+ # Not currently supported
22
+ # * force_lowercase
23
+ # If true, values stored for tags will first be downcased to make the values effectively case-insensitive
24
+ # * force_parameterize
25
+ # If true, values stored for tags will be parameterized
26
+ # * remove_unused_tags
27
+ # If true, when there are no more taggings for a tag, the tag will be destroyed
28
+ # * tags_table
29
+ # The class to use for Tags
30
+ # * taggings_table
31
+ # The class to use for Taggings
32
+ # * default
33
+ # A default value. Any value that can be used for list assignment or adding values to a list
34
+ # can be used. If custom options like `parse` or `parser` are to be used for the default, the value
35
+ # must be passed in as an array with a hash as the last value. Like list setters, parsing is assumed.
36
+ # Example: default: ["this, is, a, list", parser: ActsAsTaggableOnMongoid::GenericParser]
37
+
38
+ ##
39
+ # This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
40
+ #
41
+ # Example:
42
+ # class Book < ActiveRecord::Base
43
+ # acts_as_taggable
44
+ # end
45
+ def acts_as_taggable(options = {})
46
+ acts_as_taggable_on :tags, options
47
+ end
48
+
49
+ ##
50
+ # This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
51
+ #
52
+ # Example:
53
+ # class Book < ActiveRecord::Base
54
+ # acts_as_ordered_taggable
55
+ # end
56
+ def acts_as_ordered_taggable(options = {})
57
+ acts_as_ordered_taggable_on :tags, options
58
+ end
59
+
60
+ ##
61
+ # Make a model taggable on specified contexts.
62
+ #
63
+ # @param [Array] tag_types An array of taggable contexts
64
+ #
65
+ # Example:
66
+ # class User < ActiveRecord::Base
67
+ # acts_as_taggable_on :languages, :skills
68
+ # end
69
+ def acts_as_taggable_on(*tag_types)
70
+ taggable_on(*tag_types)
71
+ end
72
+
73
+ ##
74
+ # Make a model taggable on specified contexts
75
+ # and preserves the order in which tags are created.
76
+ #
77
+ # An alias for acts_as_taggable_on *tag_types, preserve_tag_order: true
78
+ #
79
+ # @param [Array] tag_types An array of taggable contexts
80
+ #
81
+ # Example:
82
+ # class User < ActiveRecord::Base
83
+ # acts_as_ordered_taggable_on :languages, :skills
84
+ # end
85
+ def acts_as_ordered_taggable_on(*tag_types)
86
+ options = tag_types.extract_options!
87
+
88
+ taggable_on(*tag_types, options.merge(preserve_tag_order: true))
89
+ end
90
+
91
+ private
92
+
93
+ # Make a model taggable on specified contexts
94
+ # and optionally preserves the order in which tags are created
95
+ #
96
+ # Separate methods used above for backwards compatibility
97
+ # so that the original acts_as_taggable_on method is unaffected
98
+ # as it's not possible to add another argument to the method
99
+ # without the tag_types being enclosed in square brackets
100
+ #
101
+ # NB: method overridden in core module in order to create tag type
102
+ # associations and methods after this logic has executed
103
+ #
104
+ def taggable_on(*tag_types)
105
+ # if we are actually defining tags on a module, add these modules to add hooks and global methods
106
+ # used by tagging. We only add them dynamically like this so that they don't bloat the model
107
+ # and add hooks/callbacks that aren't needed without tags.
108
+ [ActsAsTaggableOnMongoid::Taggable::Core,
109
+ ActsAsTaggableOnMongoid::Taggable::Changeable,
110
+ # include Collection - not sure we will need as done here. Need to think more on this one.
111
+ # include Cache - TODO: Add this.
112
+ # include Ownership - TODO: Add this.
113
+ # include Related - TODO: Add this.
114
+ ActsAsTaggableOnMongoid::Taggable::ListTags].each do |include_module|
115
+ include include_module unless included_modules.include?(include_module)
116
+ end
117
+
118
+ options = tag_types.extract_options!
119
+ tag_types.flatten!
120
+
121
+ tag_types.each do |tag_type|
122
+ next if tag_type.blank?
123
+
124
+ define_tag tag_type, options
125
+ end
126
+ end
127
+ end
128
+
129
+ # rubocop:enable Metrics/BlockLength
130
+ end
131
+ end