mongoid-slug 5.3.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +20 -20
  3. data/README.md +361 -360
  4. data/lib/mongoid/slug.rb +328 -366
  5. data/lib/mongoid/slug/criteria.rb +107 -107
  6. data/lib/mongoid/slug/{index.rb → index_builder.rb} +67 -51
  7. data/lib/mongoid/slug/railtie.rb +9 -9
  8. data/lib/mongoid/slug/slug_id_strategy.rb +3 -3
  9. data/lib/mongoid/slug/unique_slug.rb +173 -154
  10. data/lib/mongoid/slug/version.rb +5 -5
  11. data/lib/mongoid_slug.rb +2 -2
  12. data/lib/tasks/mongoid_slug.rake +15 -19
  13. data/spec/models/alias.rb +6 -6
  14. data/spec/models/article.rb +9 -9
  15. data/spec/models/artist.rb +8 -8
  16. data/spec/models/artwork.rb +10 -10
  17. data/spec/models/author.rb +15 -15
  18. data/spec/models/author_polymorphic.rb +15 -15
  19. data/spec/models/book.rb +12 -12
  20. data/spec/models/book_polymorphic.rb +12 -12
  21. data/spec/models/caption.rb +17 -17
  22. data/spec/models/entity.rb +11 -12
  23. data/spec/models/friend.rb +7 -7
  24. data/spec/models/incorrect_slug_persistence.rb +9 -9
  25. data/spec/models/integer_id.rb +9 -9
  26. data/spec/models/magazine.rb +7 -7
  27. data/spec/models/page.rb +9 -9
  28. data/spec/models/page_localize.rb +9 -9
  29. data/spec/models/page_slug_localized.rb +9 -9
  30. data/spec/models/page_slug_localized_custom.rb +10 -10
  31. data/spec/models/page_slug_localized_history.rb +9 -9
  32. data/spec/models/partner.rb +7 -7
  33. data/spec/models/person.rb +12 -12
  34. data/spec/models/relationship.rb +8 -8
  35. data/spec/models/string_id.rb +9 -9
  36. data/spec/models/subject.rb +7 -7
  37. data/spec/models/without_slug.rb +5 -5
  38. data/spec/mongoid/criteria_spec.rb +207 -208
  39. data/spec/mongoid/index_builder_spec.rb +105 -0
  40. data/spec/mongoid/slug_spec.rb +1175 -1170
  41. data/spec/shared/indexes.rb +41 -41
  42. data/spec/spec_helper.rb +61 -67
  43. data/spec/tasks/mongoid_slug_rake_spec.rb +73 -73
  44. metadata +14 -24
  45. data/lib/mongoid/slug/paranoia.rb +0 -20
  46. data/spec/models/document_paranoid.rb +0 -9
  47. data/spec/models/paranoid_document.rb +0 -8
  48. data/spec/models/paranoid_permanent.rb +0 -10
  49. data/spec/mongoid/index_spec.rb +0 -34
  50. data/spec/mongoid/paranoia_spec.rb +0 -230
data/lib/mongoid/slug.rb CHANGED
@@ -1,366 +1,328 @@
1
- require 'mongoid'
2
- require 'stringex'
3
- require 'mongoid/slug/criteria'
4
- require 'mongoid/slug/index'
5
- require 'mongoid/slug/paranoia'
6
- require 'mongoid/slug/unique_slug'
7
- require 'mongoid/slug/slug_id_strategy'
8
- require 'mongoid-compatibility'
9
- require 'mongoid/slug/railtie' if defined?(Rails)
10
-
11
- module Mongoid
12
- # Slugs your Mongoid model.
13
- module Slug
14
- extend ActiveSupport::Concern
15
-
16
- MONGO_INDEX_KEY_LIMIT_BYTES = 1024
17
-
18
- included do
19
- cattr_accessor :slug_reserved_words,
20
- :slug_scope,
21
- :slugged_attributes,
22
- :slug_url_builder,
23
- :slug_history,
24
- :slug_by_model_type,
25
- :slug_max_length
26
-
27
- # field :_slugs, type: Array, default: [], localize: false
28
- # alias_attribute :slugs, :_slugs
29
- end
30
-
31
- class << self
32
- attr_accessor :default_slug
33
- def configure(&block)
34
- instance_eval(&block)
35
- end
36
-
37
- def slug(&block)
38
- @default_slug = block if block_given?
39
- end
40
- end
41
-
42
- module ClassMethods
43
- # @overload slug(*fields)
44
- # Sets one ore more fields as source of slug.
45
- # @param [Array] fields One or more fields the slug should be based on.
46
- # @yield If given, the block is used to build a custom slug.
47
- #
48
- # @overload slug(*fields, options)
49
- # Sets one ore more fields as source of slug.
50
- # @param [Array] fields One or more fields the slug should be based on.
51
- # @param [Hash] options
52
- # @param options [Boolean] :history Whether a history of changes to
53
- # the slug should be retained. When searched by slug, the document now
54
- # matches both past and present slugs.
55
- # @param options [Boolean] :permanent Whether the slug should be
56
- # immutable. Defaults to `false`.
57
- # @param options [Array] :reserve` A list of reserved slugs
58
- # @param options :scope [Symbol] a reference association or field to
59
- # scope the slug by. Embedded documents are, by default, scoped by
60
- # their parent.
61
- # @param options :max_length [Integer] the maximum length of the text portion of the slug
62
- # @yield If given, a block is used to build a slug.
63
- #
64
- # @example A custom builder
65
- # class Person
66
- # include Mongoid::Document
67
- # include Mongoid::Slug
68
- #
69
- # field :names, :type => Array
70
- # slug :names do |doc|
71
- # doc.names.join(' ')
72
- # end
73
- # end
74
- #
75
- def slug(*fields, &block)
76
- options = fields.extract_options!
77
-
78
- self.slug_scope = options[:scope]
79
- self.slug_reserved_words = options[:reserve] || Set.new(%w(new edit))
80
- self.slugged_attributes = fields.map(&:to_s)
81
- self.slug_history = options[:history]
82
- self.slug_by_model_type = options[:by_model_type]
83
- self.slug_max_length = options.key?(:max_length) ? options[:max_length] : MONGO_INDEX_KEY_LIMIT_BYTES - 32
84
-
85
- field :_slugs, type: Array, localize: options[:localize]
86
- alias_attribute :slugs, :_slugs
87
-
88
- # Set index
89
- unless embedded?
90
- index(*Mongoid::Slug::Index.build_index(slug_scope_key, slug_by_model_type))
91
- end
92
-
93
- self.slug_url_builder = block_given? ? block : default_slug_url_builder
94
-
95
- #-- always create slug on create
96
- #-- do not create new slug on update if the slug is permanent
97
- if options[:permanent]
98
- set_callback :create, :before, :build_slug
99
- else
100
- set_callback :save, :before, :build_slug, if: :slug_should_be_rebuilt?
101
- end
102
-
103
- # If paranoid document:
104
- # - include shim to add callbacks for restore method
105
- # - unset the slugs on destroy
106
- # - recreate the slug on restore
107
- # - force reset the slug when saving a destroyed paranoid document, to ensure it stays unset in the database
108
- if is_paranoid_doc?
109
- send(:include, Mongoid::Slug::Paranoia) unless respond_to?(:before_restore)
110
- set_callback :destroy, :after, :unset_slug!
111
- set_callback :restore, :before, :set_slug!
112
- set_callback :save, :before, :reset_slug!, if: :paranoid_deleted?
113
- set_callback :save, :after, :clear_slug!, if: :paranoid_deleted?
114
- end
115
- end
116
-
117
- def default_slug_url_builder
118
- Mongoid::Slug.default_slug || ->(cur_object) { cur_object.slug_builder.to_url }
119
- end
120
-
121
- def look_like_slugs?(*args)
122
- with_default_scope.look_like_slugs?(*args)
123
- end
124
-
125
- # Returns the scope key for indexing, considering associations
126
- #
127
- # @return [ Array<Document>, Document ] Whether the document is paranoid
128
- def slug_scope_key
129
- return nil unless slug_scope
130
- reflect_on_association(slug_scope).try(:key) || slug_scope
131
- end
132
-
133
- # Find documents by slugs.
134
- #
135
- # A document matches if any of its slugs match one of the supplied params.
136
- #
137
- # A document matching multiple supplied params will be returned only once.
138
- #
139
- # If any supplied param does not match a document a Mongoid::Errors::DocumentNotFound will be raised.
140
- #
141
- # @example Find by a slug.
142
- # Model.find_by_slug!('some-slug')
143
- #
144
- # @example Find by multiple slugs.
145
- # Model.find_by_slug!('some-slug', 'some-other-slug')
146
- #
147
- # @param [ Array<Object> ] args The slugs to search for.
148
- #
149
- # @return [ Array<Document>, Document ] The matching document(s).
150
- def find_by_slug!(*args)
151
- with_default_scope.find_by_slug!(*args)
152
- end
153
-
154
- def queryable
155
- current_scope || Criteria.new(self) # Use Mongoid::Slug::Criteria for slugged documents.
156
- end
157
-
158
- # Indicates whether or not the document includes Mongoid::Paranoia
159
- #
160
- # This can be replaced with .paranoid? method once the following PRs are merged:
161
- # - https://github.com/simi/mongoid-paranoia/pull/19
162
- # - https://github.com/haihappen/mongoid-paranoia/pull/3
163
- #
164
- # @return [ Array<Document>, Document ] Whether the document is paranoid
165
- def is_paranoid_doc?
166
- !!(defined?(::Mongoid::Paranoia) && self < ::Mongoid::Paranoia)
167
- end
168
-
169
- private
170
-
171
- if Mongoid::Compatibility::Version.mongoid5? ||
172
- Mongoid::Compatibility::Version.mongoid6? &&
173
- Threaded.method(:current_scope).arity == -1
174
- def current_scope
175
- Threaded.current_scope(self)
176
- end
177
- elsif Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
178
- def current_scope
179
- Threaded.current_scope
180
- end
181
- else
182
- def current_scope
183
- scope_stack.last
184
- end
185
- end
186
- end
187
-
188
- # Builds a new slug.
189
- #
190
- # @return [true]
191
- def build_slug
192
- if localized?
193
- begin
194
- orig_locale = I18n.locale
195
- all_locales.each do |target_locale|
196
- I18n.locale = target_locale
197
- apply_slug
198
- end
199
- ensure
200
- I18n.locale = orig_locale
201
- end
202
- else
203
- apply_slug
204
- end
205
- true
206
- end
207
-
208
- def apply_slug
209
- new_slug = find_unique_slug
210
-
211
- # skip slug generation and use Mongoid id
212
- # to find document instead
213
- return true if new_slug.size.zero?
214
-
215
- # avoid duplicate slugs
216
- _slugs.delete(new_slug) if _slugs
217
-
218
- if !!slug_history && _slugs.is_a?(Array)
219
- append_slug(new_slug)
220
- else
221
- self._slugs = [new_slug]
222
- end
223
- end
224
-
225
- # Builds slug then atomically sets it in the database.
226
- # This is used when working with the Mongoid::Paranoia restore callback.
227
- #
228
- # This method is adapted to use the :set method variants from both
229
- # Mongoid 3 (two args) and Mongoid 4 (hash arg)
230
- def set_slug!
231
- build_slug
232
- method(:set).arity == 1 ? set(_slugs: _slugs) : set(:_slugs, _slugs)
233
- end
234
-
235
- # Atomically unsets the slug field in the database. It is important to unset
236
- # the field for the sparse index on slugs.
237
- #
238
- # This also resets the in-memory value of the slug field to its default (empty array)
239
- def unset_slug!
240
- unset(:_slugs)
241
- clear_slug!
242
- end
243
-
244
- # Rolls back the slug value from the Mongoid changeset.
245
- def reset_slug!
246
- reset__slugs!
247
- end
248
-
249
- # Sets the slug to its default value.
250
- def clear_slug!
251
- self._slugs = []
252
- end
253
-
254
- # Finds a unique slug, were specified string used to generate a slug.
255
- #
256
- # Returned slug will the same as the specified string when there are no
257
- # duplicates.
258
- #
259
- # @return [String] A unique slug
260
- def find_unique_slug
261
- UniqueSlug.new(self).find_unique
262
- end
263
-
264
- # @return [Boolean] Whether the slug requires to be rebuilt
265
- def slug_should_be_rebuilt?
266
- (new_record? || _slugs_changed? || slugged_attributes_changed?) && !paranoid_deleted?
267
- end
268
-
269
- # Indicates whether or not the document has been deleted in paranoid fashion
270
- # Always returns false if the document is not paranoid
271
- #
272
- # @return [Boolean] Whether or not the document has been deleted in paranoid fashion
273
- def paranoid_deleted?
274
- !!(self.class.is_paranoid_doc? && !deleted_at.nil?)
275
- end
276
-
277
- def slugged_attributes_changed?
278
- slugged_attributes.any? { |f| attribute_changed? f.to_s }
279
- end
280
-
281
- # @return [String] A string which Action Pack uses for constructing an URL
282
- # to this record.
283
- def to_param
284
- slug || super
285
- end
286
-
287
- # @return [String] the slug, or nil if the document does not have a slug.
288
- def slug
289
- return _slugs.last if _slugs
290
- _id.to_s
291
- end
292
-
293
- def slug_builder
294
- cur_slug = nil
295
- if new_with_slugs? || persisted_with_slug_changes?
296
- # user defined slug
297
- cur_slug = _slugs.last
298
- end
299
- # generate slug if the slug is not user defined or does not exist
300
- cur_slug || pre_slug_string
301
- end
302
-
303
- private
304
-
305
- def append_slug(value)
306
- if localized?
307
- # This is necessary for the scenario in which the slugged locale is not yet present
308
- # but the default locale is. In this situation, self._slugs falls back to the default
309
- # which is undesired
310
- current_slugs = _slugs_translations.fetch(I18n.locale.to_s, [])
311
- current_slugs << value
312
- self._slugs_translations = _slugs_translations.merge(I18n.locale.to_s => current_slugs)
313
- else
314
- _slugs << value
315
- end
316
- end
317
-
318
- # Returns true if object is a new record and slugs are present
319
- def new_with_slugs?
320
- if localized?
321
- # We need to check if slugs are present for the locale without falling back
322
- # to a default
323
- new_record? && _slugs_translations.fetch(I18n.locale.to_s, []).any?
324
- else
325
- new_record? && _slugs.present?
326
- end
327
- end
328
-
329
- # Returns true if object has been persisted and has changes in the slug
330
- def persisted_with_slug_changes?
331
- if localized?
332
- changes = _slugs_change
333
- return (persisted? && false) if changes.nil?
334
-
335
- # ensure we check for changes only between the same locale
336
- original = changes.first.try(:fetch, I18n.locale.to_s, nil)
337
- compare = changes.last.try(:fetch, I18n.locale.to_s, nil)
338
- persisted? && original != compare
339
- else
340
- persisted? && _slugs_changed?
341
- end
342
- end
343
-
344
- def localized?
345
- fields['_slugs'].options[:localize]
346
- rescue
347
- false
348
- end
349
-
350
- # Return all possible locales for model
351
- # Avoiding usage of I18n.available_locales in case the user hasn't set it properly, or is
352
- # doing something crazy, but at the same time we need a fallback in case the model doesn't
353
- # have any localized attributes at all (extreme edge case).
354
- def all_locales
355
- locales = slugged_attributes
356
- .map { |attr| send("#{attr}_translations").keys if respond_to?("#{attr}_translations") }
357
- .flatten.compact.uniq
358
- locales = I18n.available_locales if locales.empty?
359
- locales
360
- end
361
-
362
- def pre_slug_string
363
- slugged_attributes.map { |f| send f }.join ' '
364
- end
365
- end
366
- end
1
+ require 'mongoid'
2
+ require 'stringex'
3
+ require 'mongoid/slug/criteria'
4
+ require 'mongoid/slug/index_builder'
5
+ require 'mongoid/slug/unique_slug'
6
+ require 'mongoid/slug/slug_id_strategy'
7
+ require 'mongoid-compatibility'
8
+ require 'mongoid/slug/railtie' if defined?(Rails)
9
+
10
+ module Mongoid
11
+ # Slugs your Mongoid model.
12
+ module Slug
13
+ extend ActiveSupport::Concern
14
+
15
+ MONGO_INDEX_KEY_LIMIT_BYTES = 1024
16
+
17
+ included do
18
+ cattr_accessor :slug_reserved_words,
19
+ :slug_scope,
20
+ :slugged_attributes,
21
+ :slug_url_builder,
22
+ :slug_history,
23
+ :slug_by_model_type,
24
+ :slug_max_length
25
+
26
+ # field :_slugs, type: Array, default: [], localize: false
27
+ # alias_attribute :slugs, :_slugs
28
+ end
29
+
30
+ class << self
31
+ attr_accessor :default_slug
32
+ def configure(&block)
33
+ instance_eval(&block)
34
+ end
35
+
36
+ def slug(&block)
37
+ @default_slug = block if block_given?
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+ # @overload slug(*fields)
43
+ # Sets one ore more fields as source of slug.
44
+ # @param [Array] fields One or more fields the slug should be based on.
45
+ # @yield If given, the block is used to build a custom slug.
46
+ #
47
+ # @overload slug(*fields, options)
48
+ # Sets one ore more fields as source of slug.
49
+ # @param [Array] fields One or more fields the slug should be based on.
50
+ # @param [Hash] options
51
+ # @param options [Boolean] :history Whether a history of changes to
52
+ # the slug should be retained. When searched by slug, the document now
53
+ # matches both past and present slugs.
54
+ # @param options [Boolean] :permanent Whether the slug should be
55
+ # immutable. Defaults to `false`.
56
+ # @param options [Array] :reserve` A list of reserved slugs
57
+ # @param options :scope [Symbol] a reference association or field to
58
+ # scope the slug by. Embedded documents are, by default, scoped by
59
+ # their parent.
60
+ # @param options :max_length [Integer] the maximum length of the text portion of the slug
61
+ # @yield If given, a block is used to build a slug.
62
+ #
63
+ # @example A custom builder
64
+ # class Person
65
+ # include Mongoid::Document
66
+ # include Mongoid::Slug
67
+ #
68
+ # field :names, :type => Array
69
+ # slug :names do |doc|
70
+ # doc.names.join(' ')
71
+ # end
72
+ # end
73
+ #
74
+ def slug(*fields, &block)
75
+ options = fields.extract_options!
76
+
77
+ self.slug_scope = options[:scope]
78
+ self.slug_reserved_words = options[:reserve] || Set.new(%w[new edit])
79
+ self.slugged_attributes = fields.map(&:to_s)
80
+ self.slug_history = options[:history]
81
+ self.slug_by_model_type = options[:by_model_type]
82
+ self.slug_max_length = options.key?(:max_length) ? options[:max_length] : MONGO_INDEX_KEY_LIMIT_BYTES - 32
83
+
84
+ field :_slugs, type: Array, localize: options[:localize]
85
+ alias_attribute :slugs, :_slugs
86
+
87
+ # Set indexes
88
+ Mongoid::Slug::IndexBuilder.build_indexes(self, slug_scope_key, slug_by_model_type, options[:localize]) unless embedded?
89
+
90
+ self.slug_url_builder = block_given? ? block : default_slug_url_builder
91
+
92
+ #-- always create slug on create
93
+ #-- do not create new slug on update if the slug is permanent
94
+ if options[:permanent]
95
+ set_callback :create, :before, :build_slug
96
+ else
97
+ set_callback :save, :before, :build_slug, if: :slug_should_be_rebuilt?
98
+ end
99
+ end
100
+
101
+ def default_slug_url_builder
102
+ Mongoid::Slug.default_slug || ->(cur_object) { cur_object.slug_builder.to_url }
103
+ end
104
+
105
+ def look_like_slugs?(*args)
106
+ with_default_scope.look_like_slugs?(*args)
107
+ end
108
+
109
+ # Returns the scope key for indexing, considering associations
110
+ #
111
+ # @return [ Array<Document>, Document ]
112
+ def slug_scope_key
113
+ return nil unless slug_scope
114
+ reflect_on_association(slug_scope).try(:key) || slug_scope
115
+ end
116
+
117
+ # Find documents by slugs.
118
+ #
119
+ # A document matches if any of its slugs match one of the supplied params.
120
+ #
121
+ # A document matching multiple supplied params will be returned only once.
122
+ #
123
+ # If any supplied param does not match a document a Mongoid::Errors::DocumentNotFound will be raised.
124
+ #
125
+ # @example Find by a slug.
126
+ # Model.find_by_slug!('some-slug')
127
+ #
128
+ # @example Find by multiple slugs.
129
+ # Model.find_by_slug!('some-slug', 'some-other-slug')
130
+ #
131
+ # @param [ Array<Object> ] args The slugs to search for.
132
+ #
133
+ # @return [ Array<Document>, Document ] The matching document(s).
134
+ def find_by_slug!(*args)
135
+ with_default_scope.find_by_slug!(*args)
136
+ end
137
+
138
+ def queryable
139
+ current_scope || Criteria.new(self) # Use Mongoid::Slug::Criteria for slugged documents.
140
+ end
141
+
142
+ private
143
+
144
+ if Mongoid::Compatibility::Version.mongoid5_or_newer? && Threaded.method(:current_scope).arity == -1
145
+ def current_scope
146
+ Threaded.current_scope(self)
147
+ end
148
+ elsif Mongoid::Compatibility::Version.mongoid5_or_newer?
149
+ def current_scope
150
+ Threaded.current_scope
151
+ end
152
+ else
153
+ def current_scope
154
+ scope_stack.last
155
+ end
156
+ end
157
+ end
158
+
159
+ # Builds a new slug.
160
+ #
161
+ # @return [true]
162
+ def build_slug
163
+ if localized?
164
+ begin
165
+ orig_locale = I18n.locale
166
+ all_locales.each do |target_locale|
167
+ I18n.locale = target_locale
168
+ apply_slug
169
+ end
170
+ ensure
171
+ I18n.locale = orig_locale
172
+ end
173
+ else
174
+ apply_slug
175
+ end
176
+ true
177
+ end
178
+
179
+ def apply_slug
180
+ new_slug = find_unique_slug
181
+
182
+ # skip slug generation and use Mongoid id
183
+ # to find document instead
184
+ return true if new_slug.size.zero?
185
+
186
+ # avoid duplicate slugs
187
+ _slugs.delete(new_slug) if _slugs
188
+
189
+ if !!slug_history && _slugs.is_a?(Array)
190
+ append_slug(new_slug)
191
+ else
192
+ self._slugs = [new_slug]
193
+ end
194
+ end
195
+
196
+ # Builds slug then atomically sets it in the database.
197
+ #
198
+ # This method is adapted to use the :set method variants from both
199
+ # Mongoid 3 (two args) and Mongoid 4 (hash arg)
200
+ def set_slug!
201
+ build_slug
202
+ method(:set).arity == 1 ? set(_slugs: _slugs) : set(:_slugs, _slugs)
203
+ end
204
+
205
+ # Atomically unsets the slug field in the database. It is important to unset
206
+ # the field for the sparse index on slugs.
207
+ #
208
+ # This also resets the in-memory value of the slug field to its default (empty array)
209
+ def unset_slug!
210
+ unset(:_slugs)
211
+ clear_slug!
212
+ end
213
+
214
+ # Rolls back the slug value from the Mongoid changeset.
215
+ def reset_slug!
216
+ reset__slugs!
217
+ end
218
+
219
+ # Sets the slug to its default value.
220
+ def clear_slug!
221
+ self._slugs = []
222
+ end
223
+
224
+ # Finds a unique slug, were specified string used to generate a slug.
225
+ #
226
+ # Returned slug will the same as the specified string when there are no
227
+ # duplicates.
228
+ #
229
+ # @return [String] A unique slug
230
+ def find_unique_slug
231
+ UniqueSlug.new(self).find_unique
232
+ end
233
+
234
+ # @return [Boolean] Whether the slug requires to be rebuilt
235
+ def slug_should_be_rebuilt?
236
+ new_record? || _slugs_changed? || slugged_attributes_changed?
237
+ end
238
+
239
+ def slugged_attributes_changed?
240
+ slugged_attributes.any? { |f| attribute_changed? f.to_s }
241
+ end
242
+
243
+ # @return [String] A string which Action Pack uses for constructing an URL
244
+ # to this record.
245
+ def to_param
246
+ slug || super
247
+ end
248
+
249
+ # @return [String] the slug, or nil if the document does not have a slug.
250
+ def slug
251
+ return _slugs.last if _slugs
252
+ _id.to_s
253
+ end
254
+
255
+ def slug_builder
256
+ cur_slug = nil
257
+ if new_with_slugs? || persisted_with_slug_changes?
258
+ # user defined slug
259
+ cur_slug = _slugs.last
260
+ end
261
+ # generate slug if the slug is not user defined or does not exist
262
+ cur_slug || pre_slug_string
263
+ end
264
+
265
+ private
266
+
267
+ def append_slug(value)
268
+ if localized?
269
+ # This is necessary for the scenario in which the slugged locale is not yet present
270
+ # but the default locale is. In this situation, self._slugs falls back to the default
271
+ # which is undesired
272
+ current_slugs = _slugs_translations.fetch(I18n.locale.to_s, [])
273
+ current_slugs << value
274
+ self._slugs_translations = _slugs_translations.merge(I18n.locale.to_s => current_slugs)
275
+ else
276
+ _slugs << value
277
+ end
278
+ end
279
+
280
+ # Returns true if object is a new record and slugs are present
281
+ def new_with_slugs?
282
+ if localized?
283
+ # We need to check if slugs are present for the locale without falling back
284
+ # to a default
285
+ new_record? && _slugs_translations.fetch(I18n.locale.to_s, []).any?
286
+ else
287
+ new_record? && _slugs.present?
288
+ end
289
+ end
290
+
291
+ # Returns true if object has been persisted and has changes in the slug
292
+ def persisted_with_slug_changes?
293
+ if localized?
294
+ changes = _slugs_change
295
+ return (persisted? && false) if changes.nil?
296
+
297
+ # ensure we check for changes only between the same locale
298
+ original = changes.first.try(:fetch, I18n.locale.to_s, nil)
299
+ compare = changes.last.try(:fetch, I18n.locale.to_s, nil)
300
+ persisted? && original != compare
301
+ else
302
+ persisted? && _slugs_changed?
303
+ end
304
+ end
305
+
306
+ def localized?
307
+ fields['_slugs'].options[:localize]
308
+ rescue StandardError
309
+ false
310
+ end
311
+
312
+ # Return all possible locales for model
313
+ # Avoiding usage of I18n.available_locales in case the user hasn't set it properly, or is
314
+ # doing something crazy, but at the same time we need a fallback in case the model doesn't
315
+ # have any localized attributes at all (extreme edge case).
316
+ def all_locales
317
+ locales = slugged_attributes
318
+ .map { |attr| send("#{attr}_translations").keys if respond_to?("#{attr}_translations") }
319
+ .flatten.compact.uniq
320
+ locales = I18n.available_locales if locales.empty?
321
+ locales
322
+ end
323
+
324
+ def pre_slug_string
325
+ slugged_attributes.map { |f| send f }.join ' '
326
+ end
327
+ end
328
+ end