mongoid-slug 6.0.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.
- checksums.yaml +4 -4
- data/LICENSE +20 -20
- data/README.md +361 -336
- data/lib/mongoid/slug.rb +328 -328
- data/lib/mongoid/slug/criteria.rb +107 -107
- data/lib/mongoid/slug/{index.rb → index_builder.rb} +67 -45
- data/lib/mongoid/slug/railtie.rb +9 -9
- data/lib/mongoid/slug/slug_id_strategy.rb +3 -3
- data/lib/mongoid/slug/unique_slug.rb +173 -173
- data/lib/mongoid/slug/version.rb +5 -5
- data/lib/mongoid_slug.rb +2 -2
- data/lib/tasks/mongoid_slug.rake +15 -19
- data/spec/models/alias.rb +6 -6
- data/spec/models/article.rb +9 -9
- data/spec/models/artist.rb +8 -8
- data/spec/models/artwork.rb +10 -10
- data/spec/models/author.rb +15 -15
- data/spec/models/author_polymorphic.rb +15 -15
- data/spec/models/book.rb +12 -12
- data/spec/models/book_polymorphic.rb +12 -12
- data/spec/models/caption.rb +17 -17
- data/spec/models/entity.rb +11 -11
- data/spec/models/friend.rb +7 -7
- data/spec/models/incorrect_slug_persistence.rb +9 -9
- data/spec/models/integer_id.rb +9 -9
- data/spec/models/magazine.rb +7 -7
- data/spec/models/page.rb +9 -9
- data/spec/models/page_localize.rb +9 -9
- data/spec/models/page_slug_localized.rb +9 -9
- data/spec/models/page_slug_localized_custom.rb +10 -10
- data/spec/models/page_slug_localized_history.rb +9 -9
- data/spec/models/partner.rb +7 -7
- data/spec/models/person.rb +12 -12
- data/spec/models/relationship.rb +8 -8
- data/spec/models/string_id.rb +9 -9
- data/spec/models/subject.rb +7 -7
- data/spec/models/without_slug.rb +5 -5
- data/spec/mongoid/criteria_spec.rb +207 -207
- data/spec/mongoid/index_builder_spec.rb +105 -0
- data/spec/mongoid/slug_spec.rb +1175 -1169
- data/spec/shared/indexes.rb +41 -41
- data/spec/spec_helper.rb +61 -61
- data/spec/tasks/mongoid_slug_rake_spec.rb +73 -73
- metadata +31 -32
- data/spec/mongoid/index_spec.rb +0 -33
data/lib/mongoid/slug.rb
CHANGED
@@ -1,328 +1,328 @@
|
|
1
|
-
require 'mongoid'
|
2
|
-
require 'stringex'
|
3
|
-
require 'mongoid/slug/criteria'
|
4
|
-
require 'mongoid/slug/
|
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
|
88
|
-
|
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
|
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
|