mongoid-slug 4.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 +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