mongoid_slug 3.0.0 → 3.1.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 +6 -6
- data/README.md +66 -0
- data/lib/mongoid/slug.rb +16 -3
- data/lib/mongoid/slug/unique_slug.rb +6 -2
- data/lib/mongoid/slug/version.rb +1 -1
- data/spec/models/author_polymorphic.rb +11 -0
- data/spec/models/book_polymorphic.rb +12 -0
- data/spec/models/incorrect_slug_persistence.rb +9 -0
- data/spec/mongoid/slug_spec.rb +74 -0
- data/spec/mongoid/slug_spec.rb.b00 +1101 -0
- metadata +27 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
5
|
-
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7f02a3cbeacca56a521287827c0a7650b8d0b07e
|
4
|
+
data.tar.gz: 8b9d08075c34fabf26e1f5a27c635626520bf649
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 175e6e37b7072bc78fe66959bc3990e7c095091d14ea100df80ace7d1312ff1b019503818797b5ae9a82451e261cf75976917fdfa583f9a5d8929cafb16ed744
|
7
|
+
data.tar.gz: 77e501773a14a495ae9103623ca5375ea33e93f0973f50523c7161958b0c5fd1a0d4526a405fe137cf4260a02a708422669baba17860c350f2e780ca952ae912
|
data/README.md
CHANGED
@@ -139,6 +139,51 @@ class Employee
|
|
139
139
|
end
|
140
140
|
```
|
141
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
|
+
|
142
187
|
History
|
143
188
|
-------
|
144
189
|
|
@@ -187,6 +232,27 @@ friend.slug # => 'admin-1'
|
|
187
232
|
When reserved words are not specified, the words 'new' and 'edit' are considered reserved by default.
|
188
233
|
Specifying an array of custom reserved words will overwrite these defaults.
|
189
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
|
+
|
190
256
|
Custom Find Strategies
|
191
257
|
--------------
|
192
258
|
|
data/lib/mongoid/slug.rb
CHANGED
@@ -8,7 +8,8 @@ module Mongoid
|
|
8
8
|
:slug_scope,
|
9
9
|
:slugged_attributes,
|
10
10
|
:url_builder,
|
11
|
-
:history
|
11
|
+
:history,
|
12
|
+
:by_model_type
|
12
13
|
|
13
14
|
# field :_slugs, type: Array, default: [], localize: false
|
14
15
|
# alias_attribute :slugs, :_slugs
|
@@ -53,6 +54,7 @@ module Mongoid
|
|
53
54
|
self.reserved_words = options[:reserve] || Set.new(["new", "edit"])
|
54
55
|
self.slugged_attributes = fields.map &:to_s
|
55
56
|
self.history = options[:history]
|
57
|
+
self.by_model_type = options[:by_model_type]
|
56
58
|
|
57
59
|
field :_slugs, type: Array, default: [], localize: options[:localize]
|
58
60
|
alias_attribute :slugs, :_slugs
|
@@ -60,9 +62,20 @@ module Mongoid
|
|
60
62
|
unless embedded?
|
61
63
|
if slug_scope
|
62
64
|
scope_key = (metadata = self.reflect_on_association(slug_scope)) ? metadata.key : slug_scope
|
63
|
-
|
65
|
+
if options[:by_model_type] == true
|
66
|
+
# Add _type to the index to fix polymorphism
|
67
|
+
index({ _type: 1, scope_key => 1, _slugs: 1}, {unique: true})
|
68
|
+
else
|
69
|
+
index({scope_key => 1, _slugs: 1}, {unique: true})
|
70
|
+
end
|
71
|
+
|
64
72
|
else
|
65
|
-
index
|
73
|
+
# Add _type to the index to fix polymorphism
|
74
|
+
if options[:by_model_type] == true
|
75
|
+
index({_type: 1, _slugs: 1}, {unique: true})
|
76
|
+
else
|
77
|
+
index({_slugs: 1}, {unique: true})
|
78
|
+
end
|
66
79
|
end
|
67
80
|
end
|
68
81
|
|
@@ -63,7 +63,7 @@ module Mongoid
|
|
63
63
|
|
64
64
|
def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
|
65
65
|
:check_against_id, :reserved_words, :url_builder, :metadata,
|
66
|
-
:collection_name, :embedded?, :reflect_on_all_associations
|
66
|
+
:collection_name, :embedded?, :reflect_on_all_associations, :by_model_type
|
67
67
|
|
68
68
|
def initialize model
|
69
69
|
@model = model
|
@@ -92,7 +92,11 @@ module Mongoid
|
|
92
92
|
where_hash[scope] = model.try(:read_attribute, scope)
|
93
93
|
end
|
94
94
|
|
95
|
-
|
95
|
+
if by_model_type == true
|
96
|
+
where_hash[:_type] = model.try(:read_attribute, :_type)
|
97
|
+
end
|
98
|
+
|
99
|
+
@state = SlugState.new _slug, uniqueness_scope.unscoped.where(where_hash), pattern
|
96
100
|
|
97
101
|
# do not allow a slug that can be interpreted as the current document id
|
98
102
|
@state.include_slug unless model.class.look_like_slugs?([_slug])
|
data/lib/mongoid/slug/version.rb
CHANGED
@@ -0,0 +1,11 @@
|
|
1
|
+
class AuthorPolymorphic
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Slug
|
4
|
+
field :first_name
|
5
|
+
field :last_name
|
6
|
+
slug :first_name, :last_name, :scope => :book_polymorphic
|
7
|
+
belongs_to :book_polymorphic
|
8
|
+
has_many :characters,
|
9
|
+
:class_name => 'Person',
|
10
|
+
:foreign_key => :author_id
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class BookPolymorphic
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Slug
|
4
|
+
field :title
|
5
|
+
|
6
|
+
slug :title, :history => true, :by_model_type => true
|
7
|
+
embeds_many :subjects
|
8
|
+
has_many :author_polymorphics
|
9
|
+
end
|
10
|
+
|
11
|
+
class ComicBookPolymorphic < BookPolymorphic
|
12
|
+
end
|
data/spec/mongoid/slug_spec.rb
CHANGED
@@ -7,6 +7,26 @@ module Mongoid
|
|
7
7
|
Book.create(:title => "A Thousand Plateaus")
|
8
8
|
end
|
9
9
|
|
10
|
+
context "should not persist incorrect slugs" do
|
11
|
+
it "slugs should not be generated from invalid documents" do
|
12
|
+
|
13
|
+
#this will fail now
|
14
|
+
x = IncorrectSlugPersistence.create!(name: "test")
|
15
|
+
x.slug.should == 'test'
|
16
|
+
|
17
|
+
#I believe this will now fail
|
18
|
+
x.name = 'te'
|
19
|
+
x.valid?
|
20
|
+
x.slug.should_not == 'te'
|
21
|
+
|
22
|
+
#I believe this will persist the 'te'
|
23
|
+
x.name = 'testb'
|
24
|
+
x.save!
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
10
30
|
context "when option skip_id_check is used with UUID _id " do
|
11
31
|
let(:entity0) do
|
12
32
|
Entity.create(:_id => UUID.generate, :name => 'Pelham 1 2 3', :user_edited_variation => 'pelham-1-2-3')
|
@@ -483,11 +503,17 @@ module Mongoid
|
|
483
503
|
before do
|
484
504
|
Author.create_indexes
|
485
505
|
Book.create_indexes
|
506
|
+
|
507
|
+
AuthorPolymorphic.create_indexes
|
508
|
+
BookPolymorphic.create_indexes
|
486
509
|
end
|
487
510
|
|
488
511
|
after do
|
489
512
|
Author.remove_indexes
|
490
513
|
Book.remove_indexes
|
514
|
+
|
515
|
+
AuthorPolymorphic.remove_indexes
|
516
|
+
BookPolymorphic.remove_indexes
|
491
517
|
end
|
492
518
|
|
493
519
|
context "when slug is not scoped by a reference association" do
|
@@ -505,6 +531,45 @@ module Mongoid
|
|
505
531
|
Author.index_options.should_not have_key(:_slugs => 1 )
|
506
532
|
end
|
507
533
|
end
|
534
|
+
|
535
|
+
context "for subclass scope" do
|
536
|
+
context "when slug is not scoped by a reference association" do
|
537
|
+
it "defines an index on the slug" do
|
538
|
+
BookPolymorphic.index_options.should have_key( :_type => 1, :_slugs => 1 )
|
539
|
+
end
|
540
|
+
|
541
|
+
it "defines a unique index" do
|
542
|
+
BookPolymorphic.index_options[ :_type => 1, :_slugs => 1 ][:unique].should be_true
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
context "when slug is scoped by a reference association" do
|
547
|
+
it "does not define an index on the slug" do
|
548
|
+
AuthorPolymorphic.index_options.should_not have_key(:_type => 1, :_slugs => 1 )
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
context "when the object has STI" do
|
553
|
+
it "scopes by the subclass" do
|
554
|
+
b = BookPolymorphic.create!(title: 'Book')
|
555
|
+
b.slug.should == 'book'
|
556
|
+
|
557
|
+
b2 = BookPolymorphic.create!(title: 'Book')
|
558
|
+
b2.slug.should == 'book-1'
|
559
|
+
|
560
|
+
c = ComicBookPolymorphic.create!(title: 'Book')
|
561
|
+
c.slug.should == 'book'
|
562
|
+
|
563
|
+
c2 = ComicBookPolymorphic.create!(title: 'Book')
|
564
|
+
c2.slug.should == 'book-1'
|
565
|
+
|
566
|
+
BookPolymorphic.find('book').should == b
|
567
|
+
BookPolymorphic.find('book-1').should == b2
|
568
|
+
ComicBookPolymorphic.find('book').should == c
|
569
|
+
ComicBookPolymorphic.find('book-1').should == c2
|
570
|
+
end
|
571
|
+
end
|
572
|
+
end
|
508
573
|
end
|
509
574
|
|
510
575
|
context "for reserved words" do
|
@@ -554,6 +619,15 @@ module Mongoid
|
|
554
619
|
comic_book = ComicBook.create(:title => "Anti Oedipus")
|
555
620
|
comic_book.slugs.should_not eql(book.slugs)
|
556
621
|
end
|
622
|
+
|
623
|
+
it "scopes by the subclass" do
|
624
|
+
book = BookPolymorphic.create(:title => "Anti Oedipus")
|
625
|
+
comic_book = ComicBookPolymorphic.create(:title => "Anti Oedipus")
|
626
|
+
comic_book.slugs.should eql(book.slugs)
|
627
|
+
|
628
|
+
BookPolymorphic.find(book.slug).should == book
|
629
|
+
ComicBookPolymorphic.find(comic_book.slug).should == comic_book
|
630
|
+
end
|
557
631
|
end
|
558
632
|
|
559
633
|
context "when slug defined on alias of field" do
|
@@ -0,0 +1,1101 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
module Mongoid
|
5
|
+
describe Slug do
|
6
|
+
let(:book) do
|
7
|
+
Book.create(:title => "A Thousand Plateaus")
|
8
|
+
end
|
9
|
+
|
10
|
+
context "should not persist incorrect slugs" do
|
11
|
+
it "slugs should not be generated from invalid documents" do
|
12
|
+
|
13
|
+
#this will fail now
|
14
|
+
x = IncorrectSlugPersistence.create!(name: "test")
|
15
|
+
x.slug.should == 'test'
|
16
|
+
|
17
|
+
#I believe this will now fail
|
18
|
+
x.name = 'te'
|
19
|
+
x.valid?
|
20
|
+
x.slug.should_not == 'te'
|
21
|
+
|
22
|
+
#I believe this will persist the 'te'
|
23
|
+
x.name = 'testb'
|
24
|
+
x.save!
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when option skip_id_check is used with UUID _id " do
|
31
|
+
let(:entity0) do
|
32
|
+
Entity.create(:_id => UUID.generate, :name => 'Pelham 1 2 3', :user_edited_variation => 'pelham-1-2-3')
|
33
|
+
end
|
34
|
+
let(:entity1) do
|
35
|
+
Entity.create(:_id => UUID.generate, :name => 'Jackson 5', :user_edited_variation => 'jackson-5')
|
36
|
+
end
|
37
|
+
let(:entity2) do
|
38
|
+
Entity.create(:_id => UUID.generate, :name => 'Jackson 5', :user_edited_variation => 'jackson-5')
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
42
|
+
entity0.to_param.should eql "pelham-1-2-3"
|
43
|
+
|
44
|
+
5.times{ |x|
|
45
|
+
dup = Entity.create(:_id => UUID.generate, :name => entity0.name, :user_edited_variation => entity0.user_edited_variation)
|
46
|
+
dup.to_param.should eql "pelham-1-2-3-#{x.succ}"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
it "allows the user to edit the sluggable field" do
|
51
|
+
entity1.to_param.should eql "jackson-5"
|
52
|
+
entity2.to_param.should eql "jackson-5-1"
|
53
|
+
entity2.user_edited_variation = "jackson-5-indiana"
|
54
|
+
entity2.save
|
55
|
+
entity2.to_param.should eql "jackson-5-indiana"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "allows users to edit the sluggable field" do
|
59
|
+
entity1.to_param.should eql "jackson-5"
|
60
|
+
entity2.to_param.should eql "jackson-5-1"
|
61
|
+
entity2.user_edited_variation = "jackson-5-indiana"
|
62
|
+
entity2.save
|
63
|
+
entity2.to_param.should eql "jackson-5-indiana"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "it restores the slug if the editing user tries to use an existing slug" do
|
67
|
+
entity1.to_param.should eql "jackson-5"
|
68
|
+
entity2.to_param.should eql "jackson-5-1"
|
69
|
+
entity2.user_edited_variation = "jackson-5"
|
70
|
+
entity2.save
|
71
|
+
entity2.to_param.should eql "jackson-5-1"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "does not force an appended counter on a plain string" do
|
75
|
+
entity = Entity.create(:_id => UUID.generate, :name => 'Adele', :user_edited_variation => 'adele')
|
76
|
+
entity.to_param.should eql "adele"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "when the object is top-level" do
|
81
|
+
|
82
|
+
it "generates a slug" do
|
83
|
+
book.to_param.should eql "a-thousand-plateaus"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "updates the slug" do
|
87
|
+
book.title = "Anti Oedipus"
|
88
|
+
book.save
|
89
|
+
book.to_param.should eql "anti-oedipus"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
93
|
+
15.times{ |x|
|
94
|
+
dup = Book.create(:title => book.title)
|
95
|
+
dup.to_param.should eql "a-thousand-plateaus-#{x+1}"
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
100
|
+
bson_id = Moped::BSON::ObjectId.new.to_s
|
101
|
+
bad = Book.create(:title => bson_id)
|
102
|
+
bad.slugs.should_not include(bson_id)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "does not update slug if slugged fields have not changed" do
|
106
|
+
book.save
|
107
|
+
book.to_param.should eql "a-thousand-plateaus"
|
108
|
+
end
|
109
|
+
|
110
|
+
it "does not change slug if slugged fields have changed but generated slug is identical" do
|
111
|
+
book.title = "a thousand plateaus"
|
112
|
+
book.save
|
113
|
+
book.to_param.should eql "a-thousand-plateaus"
|
114
|
+
end
|
115
|
+
|
116
|
+
context "using find" do
|
117
|
+
it "finds by id as string" do
|
118
|
+
Book.find(book.id.to_s).should eql book
|
119
|
+
end
|
120
|
+
|
121
|
+
it "finds by id as array of strings" do
|
122
|
+
Book.find([book.id.to_s]).should eql [book]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "finds by id as Moped::BSON::ObjectId" do
|
126
|
+
Book.find(book.id).should eql book
|
127
|
+
end
|
128
|
+
|
129
|
+
it "finds by id as an array of Moped::BSON::ObjectIds" do
|
130
|
+
Book.find([book.id]).should eql [book]
|
131
|
+
end
|
132
|
+
|
133
|
+
it "returns an empty array if given an empty array" do
|
134
|
+
Book.find([]).should eql []
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "when the object is embedded" do
|
140
|
+
let(:subject) do
|
141
|
+
book.subjects.create(:name => "Psychoanalysis")
|
142
|
+
end
|
143
|
+
|
144
|
+
it "generates a slug" do
|
145
|
+
subject.to_param.should eql "psychoanalysis"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "updates the slug" do
|
149
|
+
subject.name = "Schizoanalysis"
|
150
|
+
subject.save
|
151
|
+
subject.to_param.should eql "schizoanalysis"
|
152
|
+
end
|
153
|
+
|
154
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
155
|
+
dup = book.subjects.create(:name => subject.name)
|
156
|
+
dup.to_param.should eql "psychoanalysis-1"
|
157
|
+
end
|
158
|
+
|
159
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
160
|
+
bad = book.subjects.create(:name => "4ea0389f0364313d79104fb3")
|
161
|
+
bad.slugs.should_not eql "4ea0389f0364313d79104fb3"
|
162
|
+
end
|
163
|
+
|
164
|
+
it "does not update slug if slugged fields have not changed" do
|
165
|
+
subject.save
|
166
|
+
subject.to_param.should eql "psychoanalysis"
|
167
|
+
end
|
168
|
+
|
169
|
+
it "does not change slug if slugged fields have changed but generated slug is identical" do
|
170
|
+
subject.name = "PSYCHOANALYSIS"
|
171
|
+
subject.to_param.should eql "psychoanalysis"
|
172
|
+
end
|
173
|
+
|
174
|
+
context "using find" do
|
175
|
+
it "finds by id as string" do
|
176
|
+
book.subjects.find(subject.id.to_s).should eql subject
|
177
|
+
end
|
178
|
+
|
179
|
+
it "finds by id as array of strings" do
|
180
|
+
book.subjects.find([subject.id.to_s]).should eql [subject]
|
181
|
+
end
|
182
|
+
|
183
|
+
it "finds by id as Moped::BSON::ObjectId" do
|
184
|
+
book.subjects.find(subject.id).should eql subject
|
185
|
+
end
|
186
|
+
|
187
|
+
it "finds by id as an array of Moped::BSON::ObjectIds" do
|
188
|
+
book.subjects.find([subject.id]).should eql [subject]
|
189
|
+
end
|
190
|
+
|
191
|
+
it "returns an empty array if given an empty array" do
|
192
|
+
book.subjects.find([]).should eql []
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
context "when the object is embedded in another embedded object" do
|
199
|
+
let(:person) do
|
200
|
+
Person.create(:name => "John Doe")
|
201
|
+
end
|
202
|
+
|
203
|
+
let(:relationship) do
|
204
|
+
person.relationships.create(:name => "Engagement")
|
205
|
+
end
|
206
|
+
|
207
|
+
let(:partner) do
|
208
|
+
relationship.partners.create(:name => "Jane Smith")
|
209
|
+
end
|
210
|
+
|
211
|
+
it "generates a slug" do
|
212
|
+
partner.to_param.should eql "jane-smith"
|
213
|
+
end
|
214
|
+
|
215
|
+
it "updates the slug" do
|
216
|
+
partner.name = "Jane Doe"
|
217
|
+
partner.save
|
218
|
+
partner.to_param.should eql "jane-doe"
|
219
|
+
end
|
220
|
+
|
221
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
222
|
+
dup = relationship.partners.create(:name => partner.name)
|
223
|
+
dup.to_param.should eql "jane-smith-1"
|
224
|
+
end
|
225
|
+
|
226
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
227
|
+
bad = relationship.partners.create(:name => "4ea0389f0364313d79104fb3")
|
228
|
+
bad.slugs.should_not eql "4ea0389f0364313d79104fb3"
|
229
|
+
end
|
230
|
+
|
231
|
+
it "does not update slug if slugged fields have not changed" do
|
232
|
+
partner.save
|
233
|
+
partner.to_param.should eql "jane-smith"
|
234
|
+
end
|
235
|
+
|
236
|
+
it "does not change slug if slugged fields have changed but generated slug is identical" do
|
237
|
+
partner.name = "JANE SMITH"
|
238
|
+
partner.to_param.should eql "jane-smith"
|
239
|
+
end
|
240
|
+
|
241
|
+
it "scopes by parent object" do
|
242
|
+
affair = person.relationships.create(:name => "Affair")
|
243
|
+
lover = affair.partners.create(:name => partner.name)
|
244
|
+
lover.to_param.should eql partner.to_param
|
245
|
+
end
|
246
|
+
|
247
|
+
context "using find" do
|
248
|
+
it "finds by id as string" do
|
249
|
+
relationship.partners.find(partner.id.to_s).should eql partner
|
250
|
+
end
|
251
|
+
|
252
|
+
it "finds by id as array of strings" do
|
253
|
+
relationship.partners.find([partner.id.to_s]).should eql [partner]
|
254
|
+
end
|
255
|
+
|
256
|
+
it "finds by id as Moped::BSON::ObjectId" do
|
257
|
+
relationship.partners.find(partner.id).should eql partner
|
258
|
+
end
|
259
|
+
|
260
|
+
it "finds by id as an array of Moped::BSON::ObjectIds" do
|
261
|
+
relationship.partners.find([partner.id]).should eql [partner]
|
262
|
+
end
|
263
|
+
|
264
|
+
it "returns an empty array if given an empty array" do
|
265
|
+
relationship.partners.find([]).should eql []
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
context "when the slug is composed of multiple fields" do
|
272
|
+
let!(:author) do
|
273
|
+
Author.create(
|
274
|
+
:first_name => "Gilles",
|
275
|
+
:last_name => "Deleuze")
|
276
|
+
end
|
277
|
+
|
278
|
+
it "generates a slug" do
|
279
|
+
author.to_param.should eql "gilles-deleuze"
|
280
|
+
end
|
281
|
+
|
282
|
+
it "updates the slug" do
|
283
|
+
author.first_name = "Félix"
|
284
|
+
author.last_name = "Guattari"
|
285
|
+
author.save
|
286
|
+
author.to_param.should eql "felix-guattari"
|
287
|
+
end
|
288
|
+
|
289
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
290
|
+
dup = Author.create(
|
291
|
+
:first_name => author.first_name,
|
292
|
+
:last_name => author.last_name)
|
293
|
+
dup.to_param.should eql "gilles-deleuze-1"
|
294
|
+
|
295
|
+
dup2 = Author.create(
|
296
|
+
:first_name => author.first_name,
|
297
|
+
:last_name => author.last_name)
|
298
|
+
|
299
|
+
dup.save
|
300
|
+
dup2.to_param.should eql "gilles-deleuze-2"
|
301
|
+
end
|
302
|
+
|
303
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
304
|
+
bad = Author.create(:first_name => "4ea0389f0364",
|
305
|
+
:last_name => "313d79104fb3")
|
306
|
+
bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
|
307
|
+
end
|
308
|
+
|
309
|
+
it "does not update slug if slugged fields have changed but generated slug is identical" do
|
310
|
+
author.last_name = "DELEUZE"
|
311
|
+
author.save
|
312
|
+
author.to_param.should eql "gilles-deleuze"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
context "when :as is passed as an argument" do
|
317
|
+
let!(:person) do
|
318
|
+
Person.create(:name => "John Doe")
|
319
|
+
end
|
320
|
+
|
321
|
+
it "sets an alternative slug field name" do
|
322
|
+
person.should respond_to(:_slugs)
|
323
|
+
person.slugs.should eql ["john-doe"]
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'defines #slug' do
|
327
|
+
person.should respond_to :slugs
|
328
|
+
end
|
329
|
+
|
330
|
+
it 'defines #slug_changed?' do
|
331
|
+
person.should respond_to :_slugs_changed?
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'defines #slug_was' do
|
335
|
+
person.should respond_to :_slugs_was
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context "when :permanent is passed as an argument" do
|
340
|
+
let(:person) do
|
341
|
+
Person.create(:name => "John Doe")
|
342
|
+
end
|
343
|
+
|
344
|
+
it "does not update the slug when the slugged fields change" do
|
345
|
+
person.name = "Jane Doe"
|
346
|
+
person.save
|
347
|
+
person.to_param.should eql "john-doe"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
context "when :history is passed as an argument" do
|
352
|
+
let(:book) do
|
353
|
+
Book.create(:title => "Book Title")
|
354
|
+
end
|
355
|
+
|
356
|
+
before(:each) do
|
357
|
+
book.title = "Other Book Title"
|
358
|
+
book.save
|
359
|
+
end
|
360
|
+
|
361
|
+
it "saves the old slug in the owner's history" do
|
362
|
+
book.slugs.should include("book-title")
|
363
|
+
end
|
364
|
+
|
365
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
366
|
+
dup = Book.create(:title => "Book Title")
|
367
|
+
dup.to_param.should eql "book-title-1"
|
368
|
+
end
|
369
|
+
|
370
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
371
|
+
bad = Book.create(:title => "4ea0389f0364313d79104fb3")
|
372
|
+
bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
|
373
|
+
end
|
374
|
+
|
375
|
+
it "ensures no duplicate values are stored in history" do
|
376
|
+
book.update_attributes :title => 'Book Title'
|
377
|
+
book.update_attributes :title => 'Foo'
|
378
|
+
book.slugs.find_all { |slug| slug == 'book-title' }.size.should eql 1
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
context "when slug is scoped by a reference association" do
|
383
|
+
let(:author) do
|
384
|
+
book.authors.create(:first_name => "Gilles", :last_name => "Deleuze")
|
385
|
+
end
|
386
|
+
|
387
|
+
it "scopes by parent object" do
|
388
|
+
book2 = Book.create(:title => "Anti Oedipus")
|
389
|
+
dup = book2.authors.create(
|
390
|
+
:first_name => author.first_name,
|
391
|
+
:last_name => author.last_name
|
392
|
+
)
|
393
|
+
dup.to_param.should eql author.to_param
|
394
|
+
end
|
395
|
+
|
396
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
397
|
+
dup = book.authors.create(
|
398
|
+
:first_name => author.first_name,
|
399
|
+
:last_name => author.last_name)
|
400
|
+
dup.to_param.should eql "gilles-deleuze-1"
|
401
|
+
end
|
402
|
+
|
403
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
404
|
+
bad = book.authors.create(:first_name => "4ea0389f0364",
|
405
|
+
:last_name => "313d79104fb3")
|
406
|
+
bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
|
407
|
+
end
|
408
|
+
|
409
|
+
context "with an irregular association name" do
|
410
|
+
let(:character) do
|
411
|
+
# well we've got to make up something... :-)
|
412
|
+
author.characters.create(:name => "Oedipus")
|
413
|
+
end
|
414
|
+
|
415
|
+
let!(:author2) do
|
416
|
+
Author.create(
|
417
|
+
:first_name => "Sophocles",
|
418
|
+
:last_name => "son of Sophilos"
|
419
|
+
)
|
420
|
+
end
|
421
|
+
|
422
|
+
it "scopes by parent object provided that inverse_of is specified" do
|
423
|
+
dup = author2.characters.create(:name => character.name)
|
424
|
+
dup.to_param.should eql character.to_param
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
context "when slug is scoped by one of the class's own fields" do
|
430
|
+
let!(:magazine) do
|
431
|
+
Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
|
432
|
+
end
|
433
|
+
|
434
|
+
it "should scope by local field" do
|
435
|
+
magazine.to_param.should eql "big-weekly"
|
436
|
+
magazine2 = Magazine.create(:title => "Big Weekly", :publisher_id => "def456")
|
437
|
+
magazine2.to_param.should eql magazine.to_param
|
438
|
+
end
|
439
|
+
|
440
|
+
it "should generate a unique slug by appending a counter to duplicate text" do
|
441
|
+
dup = Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
|
442
|
+
dup.to_param.should eql "big-weekly-1"
|
443
|
+
end
|
444
|
+
|
445
|
+
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
446
|
+
bad = Magazine.create(:title => "4ea0389f0364313d79104fb3", :publisher_id => "abc123")
|
447
|
+
bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
|
448
|
+
end
|
449
|
+
|
450
|
+
end
|
451
|
+
|
452
|
+
context "when #slug is given a block" do
|
453
|
+
let(:caption) do
|
454
|
+
Caption.create(:my_identity => "Edward Hopper (American, 1882-1967)",
|
455
|
+
:title => "Soir Bleu, 1914",
|
456
|
+
:medium => "Oil on Canvas")
|
457
|
+
end
|
458
|
+
|
459
|
+
it "generates a slug" do
|
460
|
+
caption.to_param.should eql "edward-hopper-soir-bleu-1914"
|
461
|
+
end
|
462
|
+
|
463
|
+
it "updates the slug" do
|
464
|
+
caption.title = "Road in Maine, 1914"
|
465
|
+
caption.save
|
466
|
+
caption.to_param.should eql "edward-hopper-road-in-maine-1914"
|
467
|
+
end
|
468
|
+
|
469
|
+
it "does not change slug if slugged fields have changed but generated slug is identical" do
|
470
|
+
caption.my_identity = "Edward Hopper"
|
471
|
+
caption.save
|
472
|
+
caption.to_param.should eql "edward-hopper-soir-bleu-1914"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
context "when slugged field contains non-ASCII characters" do
|
477
|
+
it "slugs Cyrillic characters" do
|
478
|
+
book.title = "Капитал"
|
479
|
+
book.save
|
480
|
+
book.to_param.should eql "kapital"
|
481
|
+
end
|
482
|
+
|
483
|
+
it "slugs Greek characters" do
|
484
|
+
book.title = "Ελλάδα"
|
485
|
+
book.save
|
486
|
+
book.to_param.should eql "ellada"
|
487
|
+
end
|
488
|
+
|
489
|
+
it "slugs Chinese characters" do
|
490
|
+
book.title = "中文"
|
491
|
+
book.save
|
492
|
+
book.to_param.should eql "zhong-wen"
|
493
|
+
end
|
494
|
+
|
495
|
+
it "slugs non-ASCII Latin characters" do
|
496
|
+
book.title = "Paul Cézanne"
|
497
|
+
book.save
|
498
|
+
book.to_param.should eql "paul-cezanne"
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
context "when indexes are created" do
|
503
|
+
before do
|
504
|
+
Author.create_indexes
|
505
|
+
Book.create_indexes
|
506
|
+
|
507
|
+
AuthorPolymorphic.create_indexes
|
508
|
+
BookPolymorphic.create_indexes
|
509
|
+
end
|
510
|
+
|
511
|
+
after do
|
512
|
+
Author.remove_indexes
|
513
|
+
Book.remove_indexes
|
514
|
+
|
515
|
+
AuthorPolymorphic.remove_indexes
|
516
|
+
BookPolymorphic.remove_indexes
|
517
|
+
end
|
518
|
+
|
519
|
+
context "when slug is not scoped by a reference association" do
|
520
|
+
it "defines an index on the slug" do
|
521
|
+
Book.index_options.should have_key( :_slugs => 1 )
|
522
|
+
end
|
523
|
+
|
524
|
+
it "defines a unique index" do
|
525
|
+
Book.index_options[ :_slugs => 1 ][:unique].should be_true
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
context "when slug is scoped by a reference association" do
|
530
|
+
it "does not define an index on the slug" do
|
531
|
+
Author.index_options.should_not have_key(:_slugs => 1 )
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
context "for subclass scope" do
|
536
|
+
context "when slug is not scoped by a reference association" do
|
537
|
+
it "defines an index on the slug" do
|
538
|
+
BookPolymorphic.index_options.should have_key( :_type => 1, :_slugs => 1 )
|
539
|
+
end
|
540
|
+
|
541
|
+
it "defines a unique index" do
|
542
|
+
BookPolymorphic.index_options[ :_type => 1, :_slugs => 1 ][:unique].should be_true
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
context "when slug is scoped by a reference association" do
|
547
|
+
it "does not define an index on the slug" do
|
548
|
+
AuthorPolymorphic.index_options.should_not have_key(:_type => 1, :_slugs => 1 )
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
context "when the object has STI" do
|
553
|
+
it "scopes by the subclass" do
|
554
|
+
b = BookPolymorphic.create!(title: 'Book')
|
555
|
+
b.slug.should == 'book'
|
556
|
+
|
557
|
+
b2 = BookPolymorphic.create!(title: 'Book')
|
558
|
+
b2.slug.should == 'book-1'
|
559
|
+
|
560
|
+
c = ComicBookPolymorphic.create!(title: 'Book')
|
561
|
+
c.slug.should == 'book'
|
562
|
+
|
563
|
+
c2 = ComicBookPolymorphic.create!(title: 'Book')
|
564
|
+
c2.slug.should == 'book-1'
|
565
|
+
|
566
|
+
BookPolymorphic.find('book').should == b
|
567
|
+
BookPolymorphic.find('book-1').should == b2
|
568
|
+
ComicBookPolymorphic.find('book').should == c
|
569
|
+
ComicBookPolymorphic.find('book-1').should == c2
|
570
|
+
end
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
context "for reserved words" do
|
576
|
+
context "when the :reserve option is used on the model" do
|
577
|
+
it "does not use the reserved slugs" do
|
578
|
+
friend1 = Friend.create(:name => "foo")
|
579
|
+
friend1.slugs.should_not include("foo")
|
580
|
+
friend1.slugs.should include("foo-1")
|
581
|
+
|
582
|
+
friend2 = Friend.create(:name => "bar")
|
583
|
+
friend2.slugs.should_not include("bar")
|
584
|
+
friend2.slugs.should include("bar-1")
|
585
|
+
|
586
|
+
friend3 = Friend.create(:name => "en")
|
587
|
+
friend3.slugs.should_not include("en")
|
588
|
+
friend3.slugs.should include("en-1")
|
589
|
+
end
|
590
|
+
|
591
|
+
it "should start with concatenation -1" do
|
592
|
+
friend1 = Friend.create(:name => "foo")
|
593
|
+
friend1.slugs.should include("foo-1")
|
594
|
+
friend2 = Friend.create(:name => "foo")
|
595
|
+
friend2.slugs.should include("foo-2")
|
596
|
+
end
|
597
|
+
|
598
|
+
["new", "edit"].each do |word|
|
599
|
+
it "should overwrite the default reserved words allowing the word '#{word}'" do
|
600
|
+
friend = Friend.create(:name => word)
|
601
|
+
friend.slugs.should include word
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|
605
|
+
context "when the model does not have any reserved words set" do
|
606
|
+
["new", "edit"].each do |word|
|
607
|
+
it "does not use the default reserved word '#{word}'" do
|
608
|
+
book = Book.create(:title => word)
|
609
|
+
book.slugs.should_not include word
|
610
|
+
book.slugs.should include("#{word}-1")
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
context "when the object has STI" do
|
617
|
+
it "scopes by the superclass" do
|
618
|
+
book = Book.create(:title => "Anti Oedipus")
|
619
|
+
comic_book = ComicBook.create(:title => "Anti Oedipus")
|
620
|
+
comic_book.slugs.should_not eql(book.slugs)
|
621
|
+
end
|
622
|
+
|
623
|
+
it "scopes by the subclass" do
|
624
|
+
book = BookPolymorphic.create(:title => "Anti Oedipus")
|
625
|
+
comic_book = ComicBookPolymorphic.create(:title => "Anti Oedipus")
|
626
|
+
comic_book.slugs.should eql(book.slugs)
|
627
|
+
|
628
|
+
BookPolymorphic.find(book.slug).should == book
|
629
|
+
ComicBookPolymorphic.find(comic_book.slug).should == comic_book
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
context "when slug defined on alias of field" do
|
634
|
+
it "should use accessor, not alias" do
|
635
|
+
pseudonim = Alias.create(:author_name => "Max Stirner")
|
636
|
+
pseudonim.slugs.should include("max-stirner")
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
describe ".find" do
|
641
|
+
let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
|
642
|
+
let!(:book2) { Book.create(:title => "Difference and Repetition") }
|
643
|
+
let!(:friend) { Friend.create(:name => "Jim Bob") }
|
644
|
+
let!(:friend2) { Friend.create(:name => friend.id.to_s) }
|
645
|
+
let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
|
646
|
+
let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
|
647
|
+
let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
|
648
|
+
let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
|
649
|
+
let!(:subject) { Subject.create(:title => "A Subject", :book => book) }
|
650
|
+
let!(:subject2) { Subject.create(:title => "A Subject", :book => book2) }
|
651
|
+
let!(:without_slug) { WithoutSlug.new().tap { |d| d.id = 456; d.save } }
|
652
|
+
|
653
|
+
context "when the model does not use mongoid slugs" do
|
654
|
+
it "should not use mongoid slug's custom find methods" do
|
655
|
+
Mongoid::Slug::Criteria.any_instance.should_not_receive(:find)
|
656
|
+
WithoutSlug.find(without_slug.id.to_s).should == without_slug
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
context "using slugs" do
|
661
|
+
context "(single)" do
|
662
|
+
context "and a document is found" do
|
663
|
+
it "returns the document as an object" do
|
664
|
+
Book.find(book.slugs.first).should == book
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
context "but no document is found" do
|
669
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
670
|
+
lambda {
|
671
|
+
Book.find("Anti Oedipus")
|
672
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
context "(multiple)" do
|
678
|
+
context "and all documents are found" do
|
679
|
+
it "returns the documents as an array without duplication" do
|
680
|
+
Book.find(book.slugs + book2.slugs).should =~ [book, book2]
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
context "but not all documents are found" do
|
685
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
686
|
+
lambda {
|
687
|
+
Book.find(book.slugs + ['something-nonexistent'])
|
688
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
context "when no documents match" do
|
694
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
695
|
+
lambda {
|
696
|
+
Book.find("Anti Oedipus")
|
697
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
context "when ids are BSON::ObjectIds and the supplied argument looks like a BSON::ObjectId" do
|
702
|
+
it "it should find based on ids not slugs" do # i.e. it should type cast the argument
|
703
|
+
Friend.find(friend.id.to_s).should == friend
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
context "when ids are Strings" do
|
708
|
+
it "it should find based on ids not slugs" do # i.e. string ids should take precedence over string slugs
|
709
|
+
StringId.find(string_id.id.to_s).should == string_id
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
context "when ids are Integers and the supplied arguments looks like an Integer" do
|
714
|
+
it "it should find based on slugs not ids" do # i.e. it should not type cast the argument
|
715
|
+
IntegerId.find(integer_id.id.to_s).should == integer_id2
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
context "models that does not use slugs, should find using the original find" do
|
720
|
+
it "it should find based on ids" do # i.e. it should not type cast the argument
|
721
|
+
WithoutSlug.find(without_slug.id.to_s).should == without_slug
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
context "when scoped" do
|
726
|
+
context "and a document is found" do
|
727
|
+
it "returns the document as an object" do
|
728
|
+
book.subjects.find(subject.slugs.first).should == subject
|
729
|
+
book2.subjects.find(subject.slugs.first).should == subject2
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
context "but no document is found" do
|
734
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
735
|
+
lambda {
|
736
|
+
book.subjects.find('Another Subject')
|
737
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
context "using ids" do
|
744
|
+
it "raises a Mongoid::Errors::DocumentNotFound error if no document is found" do
|
745
|
+
lambda {
|
746
|
+
Book.find(friend.id)
|
747
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
748
|
+
end
|
749
|
+
|
750
|
+
context "given a single document" do
|
751
|
+
it "returns the document" do
|
752
|
+
Friend.find(friend.id).should == friend
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
context "given multiple documents" do
|
757
|
+
it "returns the documents" do
|
758
|
+
Book.find([book.id, book2.id]).should =~ [book, book2]
|
759
|
+
end
|
760
|
+
end
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
describe ".find_by_slug!" do
|
765
|
+
let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
|
766
|
+
let!(:book2) { Book.create(:title => "Difference and Repetition") }
|
767
|
+
let!(:friend) { Friend.create(:name => "Jim Bob") }
|
768
|
+
let!(:friend2) { Friend.create(:name => friend.id.to_s) }
|
769
|
+
let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
|
770
|
+
let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
|
771
|
+
let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
|
772
|
+
let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
|
773
|
+
let!(:subject) { Subject.create(:title => "A Subject", :book => book) }
|
774
|
+
let!(:subject2) { Subject.create(:title => "A Subject", :book => book2) }
|
775
|
+
|
776
|
+
context "(single)" do
|
777
|
+
context "and a document is found" do
|
778
|
+
it "returns the document as an object" do
|
779
|
+
Book.find_by_slug!(book.slugs.first).should == book
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
context "but no document is found" do
|
784
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
785
|
+
lambda {
|
786
|
+
Book.find_by_slug!("Anti Oedipus")
|
787
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
788
|
+
end
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
context "(multiple)" do
|
793
|
+
context "and all documents are found" do
|
794
|
+
it "returns the documents as an array without duplication" do
|
795
|
+
Book.find_by_slug!(book.slugs + book2.slugs).should =~ [book, book2]
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
context "but not all documents are found" do
|
800
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
801
|
+
lambda {
|
802
|
+
Book.find_by_slug!(book.slugs + ['something-nonexistent'])
|
803
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
context "when scoped" do
|
809
|
+
context "and a document is found" do
|
810
|
+
it "returns the document as an object" do
|
811
|
+
book.subjects.find_by_slug!(subject.slugs.first).should == subject
|
812
|
+
book2.subjects.find_by_slug!(subject.slugs.first).should == subject2
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
context "but no document is found" do
|
817
|
+
it "raises a Mongoid::Errors::DocumentNotFound error" do
|
818
|
+
lambda {
|
819
|
+
book.subjects.find_by_slug!('Another Subject')
|
820
|
+
}.should raise_error(Mongoid::Errors::DocumentNotFound)
|
821
|
+
end
|
822
|
+
end
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
describe "#to_param" do
|
827
|
+
context "when called on a new record" do
|
828
|
+
let(:book) { Book.new }
|
829
|
+
|
830
|
+
it "should return nil" do
|
831
|
+
book.to_param.should be_nil
|
832
|
+
end
|
833
|
+
|
834
|
+
it "should not persist the record" do
|
835
|
+
book.to_param
|
836
|
+
book.should_not be_persisted
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
context "when called on an existing record with no slug" do
|
841
|
+
before do
|
842
|
+
Book.collection.insert(:title => "Proust and Signs")
|
843
|
+
end
|
844
|
+
|
845
|
+
it "should return the id" do
|
846
|
+
book = Book.first
|
847
|
+
book.to_param.should == book.id.to_s
|
848
|
+
book.reload.slugs.should be_empty
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
describe "#_slugs_changed?" do
|
854
|
+
before do
|
855
|
+
Book.create(:title => "A Thousand Plateaus")
|
856
|
+
end
|
857
|
+
|
858
|
+
let(:book) { Book.first }
|
859
|
+
|
860
|
+
it "is initially unchanged" do
|
861
|
+
book._slugs_changed?.should be_false
|
862
|
+
end
|
863
|
+
|
864
|
+
it "tracks changes" do
|
865
|
+
book.slugs = ["Anti Oedipus"]
|
866
|
+
book._slugs_changed?.should be_true
|
867
|
+
end
|
868
|
+
end
|
869
|
+
|
870
|
+
describe "when regular expression matches, but document does not" do
|
871
|
+
let!(:book_1) { Book.create(:title => "book-1") }
|
872
|
+
let!(:book_2) { Book.create(:title => "book") }
|
873
|
+
let!(:book_3) { Book.create(:title => "book") }
|
874
|
+
|
875
|
+
it "book_2 should have the user supplied title without -1 after it" do
|
876
|
+
book_2.to_param.should eql "book"
|
877
|
+
end
|
878
|
+
|
879
|
+
it "book_3 should have a generated slug" do
|
880
|
+
book_3.to_param.should eql "book-2"
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
context "when the slugged field is set manually" do
|
885
|
+
context "when it set to a non-empty string" do
|
886
|
+
it "respects the provided slug" do
|
887
|
+
book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
888
|
+
book.to_param.should eql "not-what-you-expected"
|
889
|
+
end
|
890
|
+
|
891
|
+
it "ensures uniqueness" do
|
892
|
+
book1 = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
893
|
+
book2 = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
894
|
+
book2.to_param.should eql "not-what-you-expected-1"
|
895
|
+
end
|
896
|
+
|
897
|
+
it "updates the slug when a new one is passed in" do
|
898
|
+
book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
899
|
+
book.slugs = ["not-it-either"]
|
900
|
+
book.save
|
901
|
+
book.to_param.should eql "not-it-either"
|
902
|
+
end
|
903
|
+
|
904
|
+
it "updates the slug when a new one is appended" do
|
905
|
+
book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
906
|
+
book.slugs.push "not-it-either"
|
907
|
+
book.save
|
908
|
+
book.to_param.should eql "not-it-either"
|
909
|
+
end
|
910
|
+
|
911
|
+
it "updates the slug to a unique slug when a new one is appended" do
|
912
|
+
book1 = Book.create(:title => "Sleepyhead")
|
913
|
+
book2 = Book.create(:title => "A Thousand Plateaus")
|
914
|
+
book2.slugs.push "sleepyhead"
|
915
|
+
book2.save
|
916
|
+
book2.to_param.should eql "sleepyhead-1"
|
917
|
+
end
|
918
|
+
end
|
919
|
+
|
920
|
+
context "when it is set to an empty string" do
|
921
|
+
it "generate a new one" do
|
922
|
+
book = Book.create(:title => "A Thousand Plateaus")
|
923
|
+
book.to_param.should eql "a-thousand-plateaus"
|
924
|
+
end
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
context "slug can be localized" do
|
929
|
+
it "generate a new slug for each localization" do
|
930
|
+
old_locale = I18n.locale
|
931
|
+
|
932
|
+
# Using a default locale of en.
|
933
|
+
page = PageSlugLocalize.new
|
934
|
+
page.title = "Title on English"
|
935
|
+
page.save
|
936
|
+
page.slug.should eql "title-on-english"
|
937
|
+
I18n.locale = :nl
|
938
|
+
page.title = "Title on Netherlands"
|
939
|
+
page.save
|
940
|
+
page.slug.should eql "title-on-netherlands"
|
941
|
+
|
942
|
+
# Set locale back to english
|
943
|
+
I18n.locale = old_locale
|
944
|
+
end
|
945
|
+
|
946
|
+
it "returns _id if no slug" do
|
947
|
+
old_locale = I18n.locale
|
948
|
+
|
949
|
+
# Using a default locale of en.
|
950
|
+
page = PageSlugLocalize.new
|
951
|
+
page.title = "Title on English"
|
952
|
+
page.save
|
953
|
+
page.slug.should eql "title-on-english"
|
954
|
+
I18n.locale = :nl
|
955
|
+
page.slug.should eql page._id.to_s
|
956
|
+
|
957
|
+
# Set locale back to english
|
958
|
+
I18n.locale = old_locale
|
959
|
+
end
|
960
|
+
|
961
|
+
it "fallbacks if slug not localized yet" do
|
962
|
+
old_locale = I18n.locale
|
963
|
+
|
964
|
+
# Using a default locale of en.
|
965
|
+
page = PageSlugLocalize.new
|
966
|
+
page.title = "Title on English"
|
967
|
+
page.save
|
968
|
+
page.slug.should eql "title-on-english"
|
969
|
+
I18n.locale = :nl
|
970
|
+
page.slug.should eql page._id.to_s
|
971
|
+
|
972
|
+
# Turn on i18n fallback
|
973
|
+
require "i18n/backend/fallbacks"
|
974
|
+
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
975
|
+
::I18n.fallbacks[:nl] = [ :nl, :en ]
|
976
|
+
page.slug.should eql "title-on-english"
|
977
|
+
fallback_slug = page.slug
|
978
|
+
|
979
|
+
fallback_page = PageSlugLocalize.find(fallback_slug) rescue nil
|
980
|
+
fallback_page.should eq(page)
|
981
|
+
|
982
|
+
# Set locale back to english
|
983
|
+
I18n.locale = old_locale
|
984
|
+
|
985
|
+
# Restore fallback for next tests
|
986
|
+
::I18n.fallbacks[:nl] = [ :nl ]
|
987
|
+
end
|
988
|
+
|
989
|
+
it "returns default slug if not localized" do
|
990
|
+
old_locale = I18n.locale
|
991
|
+
|
992
|
+
# Using a default locale of en.
|
993
|
+
page = PageLocalize.new
|
994
|
+
page.title = "Title on English"
|
995
|
+
page.save
|
996
|
+
page.slug.should eql "title-on-english"
|
997
|
+
I18n.locale = :nl
|
998
|
+
page.title = "Title on Netherlands"
|
999
|
+
page.slug.should eql "title-on-english"
|
1000
|
+
page.save
|
1001
|
+
page.slug.should eql "title-on-netherlands"
|
1002
|
+
|
1003
|
+
|
1004
|
+
# Set locale back to english
|
1005
|
+
I18n.locale = old_locale
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
context "slug can be localized when using history" do
|
1010
|
+
it "generate a new slug for each localization and keep history" do
|
1011
|
+
old_locale = I18n.locale
|
1012
|
+
|
1013
|
+
# Using a default locale of en.
|
1014
|
+
page = PageSlugLocalizeHistory.new
|
1015
|
+
page.title = "Title on English"
|
1016
|
+
page.save
|
1017
|
+
page.slug.should eql "title-on-english"
|
1018
|
+
I18n.locale = :nl
|
1019
|
+
page.title = "Title on Netherlands"
|
1020
|
+
page.save
|
1021
|
+
page.slug.should eql "title-on-netherlands"
|
1022
|
+
I18n.locale = old_locale
|
1023
|
+
page.title = "Modified title on English"
|
1024
|
+
page.save
|
1025
|
+
page.slug.should eql "modified-title-on-english"
|
1026
|
+
page.slug.should include("title-on-english")
|
1027
|
+
I18n.locale = :nl
|
1028
|
+
page.title = "Modified title on Netherlands"
|
1029
|
+
page.save
|
1030
|
+
page.slug.should eql "modified-title-on-netherlands"
|
1031
|
+
page.slug.should include("title-on-netherlands")
|
1032
|
+
|
1033
|
+
# Set locale back to english
|
1034
|
+
I18n.locale = old_locale
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
it "returns _id if no slug" do
|
1038
|
+
old_locale = I18n.locale
|
1039
|
+
|
1040
|
+
# Using a default locale of en.
|
1041
|
+
page = PageSlugLocalizeHistory.new
|
1042
|
+
page.title = "Title on English"
|
1043
|
+
page.save
|
1044
|
+
page.slug.should eql "title-on-english"
|
1045
|
+
I18n.locale = :nl
|
1046
|
+
page.slug.should eql page._id.to_s
|
1047
|
+
|
1048
|
+
# Set locale back to english
|
1049
|
+
I18n.locale = old_locale
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
it "fallbacks if slug not localized yet" do
|
1053
|
+
old_locale = I18n.locale
|
1054
|
+
|
1055
|
+
# Using a default locale of en.
|
1056
|
+
page = PageSlugLocalizeHistory.new
|
1057
|
+
page.title = "Title on English"
|
1058
|
+
page.save
|
1059
|
+
page.slug.should eql "title-on-english"
|
1060
|
+
I18n.locale = :nl
|
1061
|
+
page.slug.should eql page._id.to_s
|
1062
|
+
|
1063
|
+
# Turn on i18n fallback
|
1064
|
+
require "i18n/backend/fallbacks"
|
1065
|
+
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
1066
|
+
::I18n.fallbacks[:nl] = [ :nl, :en ]
|
1067
|
+
page.slug.should eql "title-on-english"
|
1068
|
+
fallback_slug = page.slug
|
1069
|
+
|
1070
|
+
fallback_page = PageSlugLocalizeHistory.find(fallback_slug) rescue nil
|
1071
|
+
fallback_page.should eq(page)
|
1072
|
+
|
1073
|
+
# Set locale back to english
|
1074
|
+
I18n.locale = old_locale
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
context "Mongoid paranoia with mongoid slug model" do
|
1079
|
+
|
1080
|
+
let(:paranoid_doc) {ParanoidDocument.create!(:title => "slug")}
|
1081
|
+
|
1082
|
+
it "returns paranoid_doc for correct slug" do
|
1083
|
+
expect(ParanoidDocument.find(paranoid_doc.slug)).to eq(paranoid_doc)
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
it "raises for deleted slug" do
|
1087
|
+
paranoid_doc.delete
|
1088
|
+
expect{ParanoidDocument.find(paranoid_doc.slug)}.to raise_error(Mongoid::Errors::DocumentNotFound)
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
it "returns paranoid_doc for correct restored slug" do
|
1092
|
+
paranoid_doc.delete
|
1093
|
+
ParanoidDocument.deleted.first.restore
|
1094
|
+
expect(ParanoidDocument.find(paranoid_doc.slug)).to eq(paranoid_doc)
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
|
1098
|
+
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
end
|