mongoid-slug 6.0.0 → 7.0.0

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