mongoid-slug 4.0.0

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +333 -0
  4. data/lib/mongoid/slug.rb +333 -0
  5. data/lib/mongoid/slug/criteria.rb +110 -0
  6. data/lib/mongoid/slug/index.rb +27 -0
  7. data/lib/mongoid/slug/paranoia.rb +22 -0
  8. data/lib/mongoid/slug/slug_id_strategy.rb +3 -0
  9. data/lib/mongoid/slug/unique_slug.rb +153 -0
  10. data/lib/mongoid/slug/version.rb +5 -0
  11. data/lib/mongoid_slug.rb +2 -0
  12. data/spec/models/alias.rb +6 -0
  13. data/spec/models/article.rb +9 -0
  14. data/spec/models/author.rb +11 -0
  15. data/spec/models/author_polymorphic.rb +11 -0
  16. data/spec/models/book.rb +12 -0
  17. data/spec/models/book_polymorphic.rb +12 -0
  18. data/spec/models/caption.rb +17 -0
  19. data/spec/models/entity.rb +12 -0
  20. data/spec/models/friend.rb +7 -0
  21. data/spec/models/incorrect_slug_persistence.rb +9 -0
  22. data/spec/models/integer_id.rb +9 -0
  23. data/spec/models/magazine.rb +7 -0
  24. data/spec/models/page.rb +9 -0
  25. data/spec/models/page_localize.rb +9 -0
  26. data/spec/models/page_slug_localized.rb +9 -0
  27. data/spec/models/page_slug_localized_custom.rb +11 -0
  28. data/spec/models/page_slug_localized_history.rb +9 -0
  29. data/spec/models/paranoid_document.rb +8 -0
  30. data/spec/models/paranoid_permanent.rb +8 -0
  31. data/spec/models/partner.rb +7 -0
  32. data/spec/models/person.rb +8 -0
  33. data/spec/models/relationship.rb +8 -0
  34. data/spec/models/string_id.rb +9 -0
  35. data/spec/models/subject.rb +7 -0
  36. data/spec/models/without_slug.rb +5 -0
  37. data/spec/mongoid/criteria_spec.rb +190 -0
  38. data/spec/mongoid/index_spec.rb +34 -0
  39. data/spec/mongoid/paranoia_spec.rb +169 -0
  40. data/spec/mongoid/slug_spec.rb +1022 -0
  41. data/spec/mongoid/slug_spec.rb.b00 +1101 -0
  42. data/spec/shared/indexes.rb +27 -0
  43. data/spec/spec_helper.rb +47 -0
  44. metadata +245 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ae9c5f1b269558e56ea27b2c659cc14d4670b886
4
+ data.tar.gz: bd270a6a2320c369d822dfb7836ea2a13d85db01
5
+ SHA512:
6
+ metadata.gz: 9c662ebc338efef83a34c6d509c2c012e1404b50ddf9337a8570e27c76ef8ee5e8c2e0eae6ba871bcd669042fee6b3f69576213073726a5f45f5be2e84c9eaac
7
+ data.tar.gz: 63d767317c4169410246110ea16942ced927e773c7ec94ea83bef62a1d2004a46fa2ce3c9d65ea94a5ef63fad6b2e0f3fe391e4af162a7a5a67dd112a9593a36
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2012 Hakan Ensari
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,333 @@
1
+ *IMPORTANT:* If you are upgrading to Mongoid Slug 1.0.0 please migrate in accordance with the instructions in https://github.com/digitalplaywright/mongoid-slug/wiki/How-to-upgrade-to-1.0.0-or-newer.
2
+ Mongoid Slug 1.0.0 stores the slugs in a single field _slugs of array type, and all previous slugs must be migrated.
3
+
4
+ Mongoid Slug
5
+ ============
6
+
7
+ Mongoid Slug generates a URL slug or permalink based on one or more fields in a
8
+ Mongoid model. It sits idly on top of [stringex] [1], supporting non-Latin
9
+ characters.
10
+
11
+ [![Build Status](https://secure.travis-ci.org/digitalplaywright/mongoid-slug.png)](http://travis-ci.org/digitalplaywright/mongoid-slug) [![Dependency Status](https://gemnasium.com/digitalplaywright/mongoid-slug.png)](https://gemnasium.com/digitalplaywright/mongoid-slug) [![Code Climate](https://codeclimate.com/github/digitalplaywright/mongoid-slug.png)](https://codeclimate.com/github/digitalplaywright/mongoid-slug)
12
+
13
+ Installation
14
+ ------------
15
+
16
+ Add to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'mongoid-slug'
20
+ ```
21
+
22
+ Usage
23
+ -----
24
+
25
+ Set up a slug:
26
+
27
+ ```ruby
28
+ class Book
29
+ include Mongoid::Document
30
+ include Mongoid::Slug
31
+
32
+ field :title
33
+ slug :title
34
+ end
35
+ ```
36
+
37
+ Find a document by its slug:
38
+
39
+ ```ruby
40
+ # GET /books/a-thousand-plateaus
41
+ book = Book.find params[:book_id]
42
+ ```
43
+
44
+ Mongoid Slug will attempt to determine whether you want to find using the `slugs` field or the `_id` field by inspecting the supplied parameters.
45
+
46
+ * Mongoid Slug will perform a find based on `slugs` only if all arguments passed to `find` are of the type `String`
47
+ * If your document uses `BSON::ObjectId` identifiers, and all arguments look like valid `BSON::ObjectId`, then Mongoid Slug will perform a find based on `_id`.
48
+ * If your document uses any other type of identifiers, and all arguments passed to `find` are of the same type, then Mongoid Slug will perform a find based on `_id`.
49
+ * If your document uses `String` identifiers and you want to be able find by slugs or ids, to get the correct behaviour, you should add a slug_id_strategy option to your _id field definition. This option should return something that responds to `call` (a callable) and takes one string argument, e.g. a lambda. This callable must return true if the string looks like one of your ids.
50
+
51
+
52
+ ```ruby
53
+ Book.fields['_id'].type
54
+ => String
55
+ book = Book.find 'a-thousand-plateaus' # Finds by slugs
56
+ => ...
57
+
58
+ class Post
59
+ include Mongoid::Document
60
+ include Mongoid::Slug
61
+
62
+ field :_id, type: String, slug_id_strategy: lambda {|id| id.start_with?('....')}
63
+
64
+ field :name
65
+ slug :name, :history => true
66
+ end
67
+
68
+ Post.fields['_id'].type
69
+ => String
70
+ post = Post.find 'a-thousand-plateaus' # Finds by slugs
71
+ => ...
72
+ post = Post.find '50b1386a0482939864000001' # Finds by bson ids
73
+ => ...
74
+ ```
75
+ [Read here] [4] for all available options.
76
+
77
+ Custom Slug Generation
78
+ -------
79
+
80
+ By default Mongoid Slug generates slugs with stringex. If this is not desired you can
81
+ define your own slug generator like this:
82
+
83
+ ```ruby
84
+ class Caption
85
+ include Mongoid::Document
86
+ include Mongoid::Slug
87
+
88
+ #create a block that takes the current object as an argument
89
+ #and returns the slug.
90
+ slug do |cur_object|
91
+ cur_object.slug_builder.to_url
92
+ end
93
+ end
94
+ ```
95
+ You can call stringex `to_url` method.
96
+
97
+ Scoping
98
+ -------
99
+
100
+ To scope a slug by a reference association, pass `:scope`:
101
+
102
+ ```ruby
103
+ class Company
104
+ include Mongoid::Document
105
+
106
+ references_many :employees
107
+ end
108
+
109
+ class Employee
110
+ include Mongoid::Document
111
+ include Mongoid::Slug
112
+
113
+ field :name
114
+ referenced_in :company
115
+
116
+ slug :name, :scope => :company
117
+ end
118
+ ```
119
+
120
+ In this example, if you create an employee without associating it with any
121
+ company, the scope will fall back to the root employees collection.
122
+
123
+ Currently, if you have an irregular association name, you **must** specify the
124
+ `:inverse_of` option on the other side of the assocation.
125
+
126
+ Embedded objects are automatically scoped by their parent.
127
+
128
+ The value of `:scope` can alternatively be a field within the model itself:
129
+
130
+ ```ruby
131
+ class Employee
132
+ include Mongoid::Document
133
+ include Mongoid::Slug
134
+
135
+ field :name
136
+ field :company_id
137
+
138
+ slug :name, :scope => :company_id
139
+ end
140
+ ```
141
+
142
+ Optionally find and create slugs per model type
143
+ -------
144
+
145
+ By default when using STI, the scope will be around the super-class.
146
+
147
+ ```ruby
148
+ class Book
149
+ include Mongoid::Document
150
+ include Mongoid::Slug
151
+ field :title
152
+
153
+ slug :title, :history => true
154
+ embeds_many :subjects
155
+ has_many :authors
156
+ end
157
+
158
+ class ComicBook < Book
159
+ end
160
+
161
+ book = Book.create(:title => "Anti Oedipus")
162
+ comic_book = ComicBook.create(:title => "Anti Oedipus")
163
+ comic_book.slugs.should_not eql(book.slugs)
164
+ ```
165
+
166
+ If you want the scope to be around the subclass, then set the option :by_model_type => true.
167
+
168
+ ```ruby
169
+ class Book
170
+ include Mongoid::Document
171
+ include Mongoid::Slug
172
+ field :title
173
+
174
+ slug :title, :history => true, :by_model_type => true
175
+ embeds_many :subjects
176
+ has_many :authors
177
+ end
178
+
179
+ class ComicBook < Book
180
+ end
181
+
182
+ book = Book.create(:title => "Anti Oedipus")
183
+ comic_book = ComicBook.create(:title => "Anti Oedipus")
184
+ comic_book.slugs.should eql(book.slugs)
185
+ ```
186
+
187
+ History
188
+ -------
189
+
190
+ To specify that the history of a document should be kept track of, pass
191
+ `:history` with a value of `true`.
192
+
193
+ ```ruby
194
+ class Page
195
+ include Mongoid::Document
196
+ include Mongoid::Slug
197
+
198
+ field :title
199
+
200
+ slug :title, history: true
201
+ end
202
+ ```
203
+
204
+ The document will then be returned for any of the saved slugs:
205
+
206
+ ```ruby
207
+ page = Page.new title: "Home"
208
+ page.save
209
+ page.update_attributes title: "Welcome"
210
+
211
+ Page.find("welcome") == Page.find("home") #=> true
212
+ ```
213
+
214
+ Reserved Slugs
215
+ --------------
216
+
217
+ Pass words you do not want to be slugged using the `reserve` option:
218
+
219
+ ```ruby
220
+ class Friend
221
+ include Mongoid::Document
222
+
223
+ field :name
224
+ slug :name, reserve: ['admin', 'root']
225
+ end
226
+
227
+ friend = Friend.create name: 'admin'
228
+ Friend.find('admin') # => nil
229
+ friend.slug # => 'admin-1'
230
+ ```
231
+
232
+ When reserved words are not specified, the words 'new' and 'edit' are considered reserved by default.
233
+ Specifying an array of custom reserved words will overwrite these defaults.
234
+
235
+ Localize Slug
236
+ --------------
237
+
238
+ The slug can be localized:
239
+
240
+ ```ruby
241
+ class PageSlugLocalize
242
+ include Mongoid::Document
243
+ include Mongoid::Slug
244
+
245
+ field :title, localize: true
246
+ slug :title, localize: true
247
+ end
248
+ ```
249
+
250
+ This feature is built upon Mongoid localized fields, so fallbacks and localization
251
+ works as documented in the Mongoid manual.
252
+
253
+ PS! A migration is needed to use Mongoid localized fields for documents that was created when this
254
+ feature was off. Anything else will cause errors.
255
+
256
+ Custom Find Strategies
257
+ --------------
258
+
259
+ By default find will search for the document by the id field if the provided id
260
+ looks like a BSON::ObjectId, and it will otherwise find by the _slugs field. However,
261
+ custom strategies can ovveride the default behavior, like e.g:
262
+
263
+ ```ruby
264
+ module Mongoid::Slug::UuidIdStrategy
265
+ def self.call id
266
+ id =~ /\A([0-9a-fA-F]){8}-(([0-9a-fA-F]){4}-){3}([0-9a-fA-F]){12}\z/
267
+ end
268
+ end
269
+ ```
270
+
271
+ Use a custom strategy by adding the slug_id_strategy annotation to the _id field:
272
+
273
+ ```ruby
274
+ class Entity
275
+ include Mongoid::Document
276
+ include Mongoid::Slug
277
+
278
+ field :_id, type: String, slug_id_strategy: UuidIdStrategy
279
+
280
+ field :user_edited_variation
281
+ slug :user_edited_variation, :history => true
282
+ end
283
+ ```
284
+
285
+
286
+ Adhoc checking whether a string is unique on a per Model basis
287
+ --------------------------------------------------------------
288
+
289
+ Lets say you want to have a auto-suggest function on your GUI that could provide a preview of what the url or slug could be before the form to create the record was submitted.
290
+
291
+ You can use the UniqueSlug class in your server side code to do this, e.g.
292
+
293
+ ```ruby
294
+ title = params[:title]
295
+ unique = Mongoid::Slug::UniqueSlug.new(Book.new).find_unique(title)
296
+ ...
297
+ # return some representation of unique
298
+ ```
299
+
300
+
301
+ Mongoid::Paranoia Support
302
+ -------------------------
303
+
304
+ The [Mongoid::Paranoia](http://github.com/simi/mongoid-paranoia) gem adds "soft-destroy" functionality to Mongoid documents.
305
+ Mongoid::Slug contains special handling for Mongoid::Paranoia:
306
+ - When destroying a paranoid document, the slug will be unset from the database.
307
+ - When restoring a paranoid document, the slug will be rebuilt. Note that the new slug may not match the old one.
308
+ - When resaving a destroyed paranoid document, the slug will remain unset in the database.
309
+ - For indexing purposes, sparse unique indexes are used. The sparse condition will ignore any destroyed paranoid documents, since their slug is not set in database.
310
+
311
+ ```ruby
312
+ class Entity
313
+ include Mongoid::Document
314
+ include Mongoid::Slug
315
+ include Mongoid::Paranoia
316
+ end
317
+ ```
318
+
319
+ The following variants of Mongoid Paranoia are officially supported:
320
+ * Mongoid 3 built-in Mongoid::Paranoia
321
+ * Mongoid 4 gem http://github.com/simi/mongoid-paranoia
322
+
323
+ Mongoid 4 gem "mongoid-paranoia" (http://github.com/haihappen/mongoid-paranoia)
324
+ is not officially supported but should also work.
325
+
326
+
327
+ References
328
+ ----------
329
+
330
+ [1]: https://github.com/rsl/stringex/
331
+ [2]: https://secure.travis-ci.org/hakanensari/mongoid-slug.png
332
+ [3]: http://travis-ci.org/hakanensari/mongoid-slug
333
+ [4]: https://github.com/digitalplaywright/mongoid-slug/blob/master/lib/mongoid/slug.rb
@@ -0,0 +1,333 @@
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
+
9
+ module Mongoid
10
+ # Slugs your Mongoid model.
11
+ module Slug
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ cattr_accessor :reserved_words,
16
+ :slug_scope,
17
+ :slugged_attributes,
18
+ :url_builder,
19
+ :history,
20
+ :by_model_type
21
+
22
+ # field :_slugs, type: Array, default: [], localize: false
23
+ # alias_attribute :slugs, :_slugs
24
+ end
25
+
26
+ module ClassMethods
27
+ # @overload slug(*fields)
28
+ # Sets one ore more fields as source of slug.
29
+ # @param [Array] fields One or more fields the slug should be based on.
30
+ # @yield If given, the block is used to build a custom slug.
31
+ #
32
+ # @overload slug(*fields, options)
33
+ # Sets one ore more fields as source of slug.
34
+ # @param [Array] fields One or more fields the slug should be based on.
35
+ # @param [Hash] options
36
+ # @param options [Boolean] :history Whether a history of changes to
37
+ # the slug should be retained. When searched by slug, the document now
38
+ # matches both past and present slugs.
39
+ # @param options [Boolean] :permanent Whether the slug should be
40
+ # immutable. Defaults to `false`.
41
+ # @param options [Array] :reserve` A list of reserved slugs
42
+ # @param options :scope [Symbol] a reference association or field to
43
+ # scope the slug by. Embedded documents are, by default, scoped by
44
+ # their parent.
45
+ # @yield If given, a block is used to build a slug.
46
+ #
47
+ # @example A custom builder
48
+ # class Person
49
+ # include Mongoid::Document
50
+ # include Mongoid::Slug
51
+ #
52
+ # field :names, :type => Array
53
+ # slug :names do |doc|
54
+ # doc.names.join(' ')
55
+ # end
56
+ # end
57
+ #
58
+ def slug(*fields, &block)
59
+ options = fields.extract_options!
60
+
61
+ self.slug_scope = options[:scope]
62
+ self.reserved_words = options[:reserve] || Set.new(["new", "edit"])
63
+ self.slugged_attributes = fields.map &:to_s
64
+ self.history = options[:history]
65
+ self.by_model_type = options[:by_model_type]
66
+
67
+ field :_slugs, type: Array, default: [], localize: options[:localize]
68
+ alias_attribute :slugs, :_slugs
69
+
70
+ # Set index
71
+ unless embedded?
72
+ index(*Mongoid::Slug::Index.build_index(self.slug_scope_key, self.by_model_type))
73
+ end
74
+
75
+ #-- Why is it necessary to customize the slug builder?
76
+ default_url_builder = lambda do |cur_object|
77
+ cur_object.slug_builder.to_url
78
+ end
79
+
80
+ self.url_builder = block_given? ? block : default_url_builder
81
+
82
+ #-- always create slug on create
83
+ #-- do not create new slug on update if the slug is permanent
84
+ if options[:permanent]
85
+ set_callback :create, :before, :build_slug
86
+ else
87
+ set_callback :save, :before, :build_slug, :if => :slug_should_be_rebuilt?
88
+ end
89
+
90
+ # If paranoid document:
91
+ # - include shim to add callbacks for restore method
92
+ # - unset the slugs on destroy
93
+ # - recreate the slug on restore
94
+ # - force reset the slug when saving a destroyed paranoid document, to ensure it stays unset in the database
95
+ if is_paranoid_doc?
96
+ self.send(:include, Mongoid::Slug::Paranoia) unless self.respond_to?(:before_restore)
97
+ set_callback :destroy, :after, :unset_slug!
98
+ set_callback :restore, :before, :set_slug!
99
+ set_callback :save, :before, :reset_slug!, :if => :paranoid_deleted?
100
+ set_callback :save, :after, :clear_slug!, :if => :paranoid_deleted?
101
+ end
102
+ end
103
+
104
+ def look_like_slugs?(*args)
105
+ with_default_scope.look_like_slugs?(*args)
106
+ end
107
+
108
+ # Returns the scope key for indexing, considering associations
109
+ #
110
+ # @return [ Array<Document>, Document ] Whether the document is paranoid
111
+ def slug_scope_key
112
+ return nil unless self.slug_scope
113
+ self.reflect_on_association(self.slug_scope).try(:key) || self.slug_scope
114
+ end
115
+
116
+ # Find documents by slugs.
117
+ #
118
+ # A document matches if any of its slugs match one of the supplied params.
119
+ #
120
+ # A document matching multiple supplied params will be returned only once.
121
+ #
122
+ # If any supplied param does not match a document a Mongoid::Errors::DocumentNotFound will be raised.
123
+ #
124
+ # @example Find by a slug.
125
+ # Model.find_by_slug!('some-slug')
126
+ #
127
+ # @example Find by multiple slugs.
128
+ # Model.find_by_slug!('some-slug', 'some-other-slug')
129
+ #
130
+ # @param [ Array<Object> ] args The slugs to search for.
131
+ #
132
+ # @return [ Array<Document>, Document ] The matching document(s).
133
+ def find_by_slug!(*args)
134
+ with_default_scope.find_by_slug!(*args)
135
+ end
136
+
137
+ def queryable
138
+ scope_stack.last || Criteria.new(self) # Use Mongoid::Slug::Criteria for slugged documents.
139
+ end
140
+
141
+ # Indicates whether or not the document includes Mongoid::Paranoia
142
+ #
143
+ # This can be replaced with .paranoid? method once the following PRs are merged:
144
+ # - https://github.com/simi/mongoid-paranoia/pull/19
145
+ # - https://github.com/haihappen/mongoid-paranoia/pull/3
146
+ #
147
+ # @return [ Array<Document>, Document ] Whether the document is paranoid
148
+ def is_paranoid_doc?
149
+ !!(defined?(::Mongoid::Paranoia) && self < ::Mongoid::Paranoia)
150
+ end
151
+ end
152
+
153
+ # Builds a new slug.
154
+ #
155
+ # @return [true]
156
+ def build_slug
157
+ if localized?
158
+ begin
159
+ orig_locale = I18n.locale
160
+ all_locales.each do |target_locale|
161
+ I18n.locale = target_locale
162
+ apply_slug
163
+ end
164
+ ensure
165
+ I18n.locale = orig_locale
166
+ end
167
+ else
168
+ apply_slug
169
+ end
170
+ true
171
+ end
172
+
173
+ def apply_slug
174
+ _new_slug = find_unique_slug
175
+
176
+ #skip slug generation and use Mongoid id
177
+ #to find document instead
178
+ return true if _new_slug.size == 0
179
+
180
+ # avoid duplicate slugs
181
+ self._slugs.delete(_new_slug) if self._slugs
182
+
183
+ if !!self.history && self._slugs.is_a?(Array)
184
+ append_slug(_new_slug)
185
+ else
186
+ self._slugs = [_new_slug]
187
+ end
188
+ end
189
+
190
+ # Builds slug then atomically sets it in the database.
191
+ # This is used when working with the Mongoid::Paranoia restore callback.
192
+ #
193
+ # This method is adapted to use the :set method variants from both
194
+ # Mongoid 3 (two args) and Mongoid 4 (hash arg)
195
+ def set_slug!
196
+ build_slug
197
+ self.method(:set).arity == 1 ? set({_slugs: self._slugs}) : set(:_slugs, self._slugs)
198
+ end
199
+
200
+ # Atomically unsets the slug field in the database. It is important to unset
201
+ # the field for the sparse index on slugs.
202
+ #
203
+ # This also resets the in-memory value of the slug field to its default (empty array)
204
+ def unset_slug!
205
+ unset(:_slugs)
206
+ clear_slug!
207
+ end
208
+
209
+ # Rolls back the slug value from the Mongoid changeset.
210
+ def reset_slug!
211
+ self.reset__slugs!
212
+ end
213
+
214
+ # Sets the slug to its default value.
215
+ def clear_slug!
216
+ self._slugs = []
217
+ end
218
+
219
+ # Finds a unique slug, were specified string used to generate a slug.
220
+ #
221
+ # Returned slug will the same as the specified string when there are no
222
+ # duplicates.
223
+ #
224
+ # @return [String] A unique slug
225
+ def find_unique_slug
226
+ UniqueSlug.new(self).find_unique
227
+ end
228
+
229
+ # @return [Boolean] Whether the slug requires to be rebuilt
230
+ def slug_should_be_rebuilt?
231
+ (new_record? or _slugs_changed? or slugged_attributes_changed?) and !paranoid_deleted?
232
+ end
233
+
234
+ # Indicates whether or not the document has been deleted in paranoid fashion
235
+ # Always returns false if the document is not paranoid
236
+ #
237
+ # @return [Boolean] Whether or not the document has been deleted in paranoid fashion
238
+ def paranoid_deleted?
239
+ !!(self.class.is_paranoid_doc? and self.deleted_at != nil)
240
+ end
241
+
242
+ def slugged_attributes_changed?
243
+ slugged_attributes.any? { |f| attribute_changed? f.to_s }
244
+ end
245
+
246
+ # @return [String] A string which Action Pack uses for constructing an URL
247
+ # to this record.
248
+ def to_param
249
+ slug || super
250
+ end
251
+
252
+ # @return [String] the slug, or nil if the document does not have a slug.
253
+ def slug
254
+ return _slugs.last if _slugs
255
+ return _id.to_s
256
+ end
257
+
258
+ def slug_builder
259
+ _cur_slug = nil
260
+ if new_with_slugs? or persisted_with_slug_changes?
261
+ #user defined slug
262
+ _cur_slug = _slugs.last
263
+ end
264
+ #generate slug if the slug is not user defined or does not exist
265
+ _cur_slug || pre_slug_string
266
+ end
267
+
268
+ def self.mongoid3?
269
+ ::Mongoid.const_defined? :Observer
270
+ end
271
+
272
+ private
273
+
274
+ def append_slug(_slug)
275
+ if localized?
276
+ # This is necessary for the scenario in which the slugged locale is not yet present
277
+ # but the default locale is. In this situation, self._slugs falls back to the default
278
+ # which is undesired
279
+ current_slugs = self._slugs_translations.fetch(I18n.locale.to_s, [])
280
+ current_slugs << _slug
281
+ self._slugs_translations = self._slugs_translations.merge(I18n.locale.to_s => current_slugs)
282
+ else
283
+ self._slugs << _slug
284
+ end
285
+ end
286
+
287
+ # Returns true if object is a new record and slugs are present
288
+ def new_with_slugs?
289
+ if localized?
290
+ # We need to check if slugs are present for the locale without falling back
291
+ # to a default
292
+ new_record? and _slugs_translations.fetch(I18n.locale.to_s, []).any?
293
+ else
294
+ new_record? and _slugs.present?
295
+ end
296
+ end
297
+
298
+ # Returns true if object has been persisted and has changes in the slug
299
+ def persisted_with_slug_changes?
300
+ if localized?
301
+ changes = self._slugs_change
302
+ return (persisted? and false) if changes.nil?
303
+
304
+ # ensure we check for changes only between the same locale
305
+ original = changes.first.try(:fetch, I18n.locale.to_s, nil)
306
+ compare = changes.last.try(:fetch, I18n.locale.to_s, nil)
307
+ persisted? and original != compare
308
+ else
309
+ persisted? and _slugs_changed?
310
+ end
311
+ end
312
+
313
+ def localized?
314
+ self.fields['_slugs'].options[:localize] rescue 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 = self.slugged_attributes
323
+ .map{|attr| self.send("#{attr}_translations").keys if self.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
+ self.slugged_attributes.map { |f| self.send f }.join ' '
331
+ end
332
+ end
333
+ end