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,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