mongoid-slug 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +333 -0
- data/lib/mongoid/slug.rb +333 -0
- data/lib/mongoid/slug/criteria.rb +110 -0
- data/lib/mongoid/slug/index.rb +27 -0
- data/lib/mongoid/slug/paranoia.rb +22 -0
- data/lib/mongoid/slug/slug_id_strategy.rb +3 -0
- data/lib/mongoid/slug/unique_slug.rb +153 -0
- data/lib/mongoid/slug/version.rb +5 -0
- data/lib/mongoid_slug.rb +2 -0
- data/spec/models/alias.rb +6 -0
- data/spec/models/article.rb +9 -0
- data/spec/models/author.rb +11 -0
- data/spec/models/author_polymorphic.rb +11 -0
- data/spec/models/book.rb +12 -0
- data/spec/models/book_polymorphic.rb +12 -0
- data/spec/models/caption.rb +17 -0
- data/spec/models/entity.rb +12 -0
- data/spec/models/friend.rb +7 -0
- data/spec/models/incorrect_slug_persistence.rb +9 -0
- data/spec/models/integer_id.rb +9 -0
- data/spec/models/magazine.rb +7 -0
- data/spec/models/page.rb +9 -0
- data/spec/models/page_localize.rb +9 -0
- data/spec/models/page_slug_localized.rb +9 -0
- data/spec/models/page_slug_localized_custom.rb +11 -0
- data/spec/models/page_slug_localized_history.rb +9 -0
- data/spec/models/paranoid_document.rb +8 -0
- data/spec/models/paranoid_permanent.rb +8 -0
- data/spec/models/partner.rb +7 -0
- data/spec/models/person.rb +8 -0
- data/spec/models/relationship.rb +8 -0
- data/spec/models/string_id.rb +9 -0
- data/spec/models/subject.rb +7 -0
- data/spec/models/without_slug.rb +5 -0
- data/spec/mongoid/criteria_spec.rb +190 -0
- data/spec/mongoid/index_spec.rb +34 -0
- data/spec/mongoid/paranoia_spec.rb +169 -0
- data/spec/mongoid/slug_spec.rb +1022 -0
- data/spec/mongoid/slug_spec.rb.b00 +1101 -0
- data/spec/shared/indexes.rb +27 -0
- data/spec/spec_helper.rb +47 -0
- metadata +245 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/lib/mongoid/slug.rb
ADDED
@@ -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
|