mongoid-slug 5.3.0 → 6.0.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 (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