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.
- 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
|
+
[](http://travis-ci.org/digitalplaywright/mongoid-slug) [](https://gemnasium.com/digitalplaywright/mongoid-slug) [](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
|