mongoid-slug 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +333 -0
  4. data/lib/mongoid/slug.rb +333 -0
  5. data/lib/mongoid/slug/criteria.rb +110 -0
  6. data/lib/mongoid/slug/index.rb +27 -0
  7. data/lib/mongoid/slug/paranoia.rb +22 -0
  8. data/lib/mongoid/slug/slug_id_strategy.rb +3 -0
  9. data/lib/mongoid/slug/unique_slug.rb +153 -0
  10. data/lib/mongoid/slug/version.rb +5 -0
  11. data/lib/mongoid_slug.rb +2 -0
  12. data/spec/models/alias.rb +6 -0
  13. data/spec/models/article.rb +9 -0
  14. data/spec/models/author.rb +11 -0
  15. data/spec/models/author_polymorphic.rb +11 -0
  16. data/spec/models/book.rb +12 -0
  17. data/spec/models/book_polymorphic.rb +12 -0
  18. data/spec/models/caption.rb +17 -0
  19. data/spec/models/entity.rb +12 -0
  20. data/spec/models/friend.rb +7 -0
  21. data/spec/models/incorrect_slug_persistence.rb +9 -0
  22. data/spec/models/integer_id.rb +9 -0
  23. data/spec/models/magazine.rb +7 -0
  24. data/spec/models/page.rb +9 -0
  25. data/spec/models/page_localize.rb +9 -0
  26. data/spec/models/page_slug_localized.rb +9 -0
  27. data/spec/models/page_slug_localized_custom.rb +11 -0
  28. data/spec/models/page_slug_localized_history.rb +9 -0
  29. data/spec/models/paranoid_document.rb +8 -0
  30. data/spec/models/paranoid_permanent.rb +8 -0
  31. data/spec/models/partner.rb +7 -0
  32. data/spec/models/person.rb +8 -0
  33. data/spec/models/relationship.rb +8 -0
  34. data/spec/models/string_id.rb +9 -0
  35. data/spec/models/subject.rb +7 -0
  36. data/spec/models/without_slug.rb +5 -0
  37. data/spec/mongoid/criteria_spec.rb +190 -0
  38. data/spec/mongoid/index_spec.rb +34 -0
  39. data/spec/mongoid/paranoia_spec.rb +169 -0
  40. data/spec/mongoid/slug_spec.rb +1022 -0
  41. data/spec/mongoid/slug_spec.rb.b00 +1101 -0
  42. data/spec/shared/indexes.rb +27 -0
  43. data/spec/spec_helper.rb +47 -0
  44. metadata +245 -0
@@ -0,0 +1,9 @@
1
+ class IntegerId
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+
5
+ field :_id, type: Integer
6
+ field :name, type: String
7
+
8
+ slug :name, history: true
9
+ end
@@ -0,0 +1,7 @@
1
+ class Magazine
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :title
5
+ field :publisher_id
6
+ slug :title, :scope => :publisher_id
7
+ end
@@ -0,0 +1,9 @@
1
+ class Page
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :title
5
+ field :content
6
+ field :order, :type => Integer
7
+ slug :title
8
+ default_scope ->{ asc(:order) }
9
+ end
@@ -0,0 +1,9 @@
1
+ class PageLocalize
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :title, localize: true
5
+ field :content
6
+ field :order, :type => Integer
7
+ slug :title
8
+ default_scope ->{ asc(:order) }
9
+ end
@@ -0,0 +1,9 @@
1
+ class PageSlugLocalized
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :title, localize: true
5
+ field :content
6
+ field :order, :type => Integer
7
+ slug :title, localize: true
8
+ default_scope ->{ asc(:order) }
9
+ end
@@ -0,0 +1,11 @@
1
+ class PageSlugLocalizedCustom
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+
5
+ attr_accessor :title
6
+
7
+ slug :title, localize: true do |obj|
8
+ obj.title.to_url
9
+ end
10
+
11
+ end
@@ -0,0 +1,9 @@
1
+ class PageSlugLocalizedHistory
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :title, localize: true
5
+ field :content
6
+ field :order, :type => Integer
7
+ slug :title, localize: true, history: true
8
+ default_scope ->{ asc(:order) }
9
+ end
@@ -0,0 +1,8 @@
1
+ class ParanoidDocument
2
+ include Mongoid::Document
3
+ include Mongoid::Paranoia
4
+ include Mongoid::Slug
5
+
6
+ field :title
7
+ slug :title
8
+ end
@@ -0,0 +1,8 @@
1
+ class ParanoidPermanent
2
+ include Mongoid::Document
3
+ include Mongoid::Paranoia
4
+ include Mongoid::Slug
5
+
6
+ field :title
7
+ slug :title, permanent: true
8
+ end
@@ -0,0 +1,7 @@
1
+ class Partner
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name
5
+ slug :name
6
+ embedded_in :relationship
7
+ end
@@ -0,0 +1,8 @@
1
+ class Person
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name
5
+ slug :name, :permanent => true, :scope => :author
6
+ embeds_many :relationships
7
+ belongs_to :author, :inverse_of => :characters
8
+ end
@@ -0,0 +1,8 @@
1
+ class Relationship
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name
5
+ slug :name
6
+ embeds_many :partners
7
+ embedded_in :person
8
+ end
@@ -0,0 +1,9 @@
1
+ class StringId
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+
5
+ field :_id, type: String
6
+ field :name, type: String
7
+
8
+ slug :name, history: true
9
+ end
@@ -0,0 +1,7 @@
1
+ class Subject
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name
5
+ slug :name, :scope => :book, :history => true
6
+ embedded_in :book
7
+ end
@@ -0,0 +1,5 @@
1
+ class WithoutSlug
2
+ include Mongoid::Document
3
+
4
+ field :_id, type: Integer
5
+ end
@@ -0,0 +1,190 @@
1
+ #encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Mongoid::Slug::Criteria do
5
+ describe ".find" do
6
+ let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
7
+ let!(:book2) { Book.create(:title => "Difference and Repetition") }
8
+ let!(:friend) { Friend.create(:name => "Jim Bob") }
9
+ let!(:friend2) { Friend.create(:name => friend.id.to_s) }
10
+ let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
11
+ let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
12
+ let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
13
+ let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
14
+ let!(:subject) { Subject.create(:name => "A Subject", :book => book) }
15
+ let!(:subject2) { Subject.create(:name => "A Subject", :book => book2) }
16
+ let!(:without_slug) { WithoutSlug.new().tap { |d| d.id = 456; d.save } }
17
+
18
+ context "when the model does not use mongoid slugs" do
19
+ it "should not use mongoid slug's custom find methods" do
20
+ Mongoid::Slug::Criteria.any_instance.should_not_receive(:find)
21
+ WithoutSlug.find(without_slug.id.to_s).should == without_slug
22
+ end
23
+ end
24
+
25
+ context "using slugs" do
26
+ context "(single)" do
27
+ context "and a document is found" do
28
+ it "returns the document as an object" do
29
+ Book.find(book.slugs.first).should == book
30
+ end
31
+ end
32
+
33
+ context "but no document is found" do
34
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
35
+ lambda {
36
+ Book.find("Anti Oedipus")
37
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
38
+ end
39
+ end
40
+ end
41
+
42
+ context "(multiple)" do
43
+ context "and all documents are found" do
44
+ it "returns the documents as an array without duplication" do
45
+ Book.find(book.slugs + book2.slugs).should =~ [book, book2]
46
+ end
47
+ end
48
+
49
+ context "but not all documents are found" do
50
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
51
+ lambda {
52
+ Book.find(book.slugs + ['something-nonexistent'])
53
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
54
+ end
55
+ end
56
+ end
57
+
58
+ context "when no documents match" do
59
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
60
+ lambda {
61
+ Book.find("Anti Oedipus")
62
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
63
+ end
64
+ end
65
+
66
+ context "when ids are BSON::ObjectIds and the supplied argument looks like a BSON::ObjectId" do
67
+ it "it should find based on ids not slugs" do # i.e. it should type cast the argument
68
+ Friend.find(friend.id.to_s).should == friend
69
+ end
70
+ end
71
+
72
+ context "when ids are Strings" do
73
+ it "it should find based on ids not slugs" do # i.e. string ids should take precedence over string slugs
74
+ StringId.find(string_id.id.to_s).should == string_id
75
+ end
76
+ end
77
+
78
+ context "when ids are Integers and the supplied arguments looks like an Integer" do
79
+ it "it should find based on slugs not ids" do # i.e. it should not type cast the argument
80
+ IntegerId.find(integer_id.id.to_s).should == integer_id2
81
+ end
82
+ end
83
+
84
+ context "models that does not use slugs, should find using the original find" do
85
+ it "it should find based on ids" do # i.e. it should not type cast the argument
86
+ WithoutSlug.find(without_slug.id.to_s).should == without_slug
87
+ end
88
+ end
89
+
90
+ context "when scoped" do
91
+ context "and a document is found" do
92
+ it "returns the document as an object" do
93
+ book.subjects.find(subject.slugs.first).should == subject
94
+ book2.subjects.find(subject.slugs.first).should == subject2
95
+ end
96
+ end
97
+
98
+ context "but no document is found" do
99
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
100
+ lambda {
101
+ book.subjects.find('Another Subject')
102
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ context "using ids" do
109
+ it "raises a Mongoid::Errors::DocumentNotFound error if no document is found" do
110
+ lambda {
111
+ Book.find(friend.id)
112
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
113
+ end
114
+
115
+ context "given a single document" do
116
+ it "returns the document" do
117
+ Friend.find(friend.id).should == friend
118
+ end
119
+ end
120
+
121
+ context "given multiple documents" do
122
+ it "returns the documents" do
123
+ Book.find([book.id, book2.id]).should =~ [book, book2]
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ describe ".find_by_slug!" do
130
+ let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
131
+ let!(:book2) { Book.create(:title => "Difference and Repetition") }
132
+ let!(:friend) { Friend.create(:name => "Jim Bob") }
133
+ let!(:friend2) { Friend.create(:name => friend.id.to_s) }
134
+ let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
135
+ let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
136
+ let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
137
+ let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
138
+ let!(:subject) { Subject.create(:name => "A Subject", :book => book) }
139
+ let!(:subject2) { Subject.create(:name => "A Subject", :book => book2) }
140
+
141
+ context "(single)" do
142
+ context "and a document is found" do
143
+ it "returns the document as an object" do
144
+ Book.find_by_slug!(book.slugs.first).should == book
145
+ end
146
+ end
147
+
148
+ context "but no document is found" do
149
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
150
+ lambda {
151
+ Book.find_by_slug!("Anti Oedipus")
152
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
153
+ end
154
+ end
155
+ end
156
+
157
+ context "(multiple)" do
158
+ context "and all documents are found" do
159
+ it "returns the documents as an array without duplication" do
160
+ Book.find_by_slug!(book.slugs + book2.slugs).should =~ [book, book2]
161
+ end
162
+ end
163
+
164
+ context "but not all documents are found" do
165
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
166
+ lambda {
167
+ Book.find_by_slug!(book.slugs + ['something-nonexistent'])
168
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
169
+ end
170
+ end
171
+ end
172
+
173
+ context "when scoped" do
174
+ context "and a document is found" do
175
+ it "returns the document as an object" do
176
+ book.subjects.find_by_slug!(subject.slugs.first).should == subject
177
+ book2.subjects.find_by_slug!(subject.slugs.first).should == subject2
178
+ end
179
+ end
180
+
181
+ context "but no document is found" do
182
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
183
+ lambda {
184
+ book.subjects.find_by_slug!('Another Subject')
185
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,34 @@
1
+ #encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Mongoid::Slug::Index do
5
+
6
+ let(:scope_key) { nil }
7
+ let(:by_model_type) { false }
8
+ subject { Mongoid::Slug::Index.build_index(scope_key, by_model_type) }
9
+
10
+ context "when scope_key is set" do
11
+ let(:scope_key) { :foo }
12
+
13
+ context "when by_model_type is true" do
14
+ let(:by_model_type) { true }
15
+ it { should eq [{:_slugs=>1, :foo=>1, :_type=>1}, {}] }
16
+ end
17
+
18
+ context "when by_model_type is false" do
19
+ it { should eq [{:_slugs=>1, :foo=>1}, {:unique=>true, :sparse=>true}] }
20
+ end
21
+ end
22
+
23
+ context "when scope_key is not set" do
24
+
25
+ context "when by_model_type is true" do
26
+ let(:by_model_type) { true }
27
+ it { should eq [{:_slugs=>1, :_type=>1}, {}] }
28
+ end
29
+
30
+ context "when by_model_type is false" do
31
+ it { should eq [{:_slugs=>1}, {:unique=>true, :sparse=>true}] }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,169 @@
1
+ #encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe "Mongoid::Paranoia with Mongoid::Slug" do
5
+
6
+ let(:paranoid_doc) { ParanoidDocument.create!(:title => "slug") }
7
+ let(:paranoid_doc_2) { ParanoidDocument.create!(:title => "slug") }
8
+ let(:paranoid_perm) { ParanoidPermanent.create!(:title => "slug") }
9
+ let(:paranoid_perm_2) { ParanoidPermanent.create!(:title => "slug") }
10
+ let(:non_paranoid_doc){ Article.create!(:title => "slug") }
11
+ subject{ paranoid_doc }
12
+
13
+ describe ".paranoid?" do
14
+
15
+ context "when Mongoid::Paranoia is included" do
16
+ subject { paranoid_doc.class }
17
+ its(:is_paranoid_doc?){ should be_truthy }
18
+ end
19
+
20
+ context "when Mongoid::Paranoia not included" do
21
+ subject { non_paranoid_doc.class }
22
+ its(:is_paranoid_doc?){ should be_falsey }
23
+ end
24
+ end
25
+
26
+ describe "#paranoid_deleted?" do
27
+
28
+ context "when Mongoid::Paranoia is included" do
29
+
30
+ context "when not destroyed" do
31
+ its(:paranoid_deleted?){ should be_falsey }
32
+ end
33
+
34
+ context "when destroyed" do
35
+ before { subject.destroy }
36
+ its(:paranoid_deleted?){ should be_truthy }
37
+ end
38
+ end
39
+
40
+ context "when Mongoid::Paranoia not included" do
41
+ subject { non_paranoid_doc }
42
+ its(:paranoid_deleted?){ should be_falsey }
43
+ end
44
+ end
45
+
46
+ describe "restore callbacks" do
47
+
48
+ context "when Mongoid::Paranoia is included" do
49
+ subject { paranoid_doc.class }
50
+ it { should respond_to(:before_restore) }
51
+ it { should respond_to(:after_restore) }
52
+ end
53
+
54
+ context "when Mongoid::Paranoia not included" do
55
+ it { should_not respond_to(:before_restore) }
56
+ it { should_not respond_to(:after_restore) }
57
+ end
58
+ end
59
+
60
+ describe "index" do
61
+ before { ParanoidDocument.create_indexes }
62
+ after { ParanoidDocument.remove_indexes }
63
+ subject { ParanoidDocument }
64
+
65
+ it_should_behave_like "has an index", { _slugs: 1 }, { unique: true, sparse: true }
66
+ end
67
+
68
+ shared_examples_for "paranoid slugs" do
69
+
70
+ context "querying" do
71
+
72
+ it "returns paranoid_doc for correct slug" do
73
+ subject.class.find(subject.slug).should eq(subject)
74
+ end
75
+ end
76
+
77
+ context "delete (callbacks not fired)" do
78
+
79
+ before { subject.delete }
80
+
81
+ it "retains slug value" do
82
+ subject.slug.should eq "slug"
83
+ subject.class.unscoped.find("slug").should eq subject
84
+ end
85
+ end
86
+
87
+ context "destroy" do
88
+
89
+ before { subject.destroy }
90
+
91
+ it "unsets slug value when destroyed" do
92
+ subject._slugs.should eq []
93
+ subject.slug.should be_nil
94
+ end
95
+
96
+ it "persists the removed slug" do
97
+ subject.reload._slugs.should eq []
98
+ subject.reload.slug.should be_nil
99
+ end
100
+
101
+ it "persists the removed slug in the database" do
102
+ subject.class.unscoped.exists(_slugs: false).first.should eq subject
103
+ expect{subject.class.unscoped.find("slug")}.to raise_error(Mongoid::Errors::DocumentNotFound)
104
+ end
105
+
106
+ context "when saving the doc again" do
107
+
108
+ before { subject.save }
109
+
110
+ it "should have the default slug value" do
111
+ subject._slugs.should eq []
112
+ subject.slug.should be_nil
113
+ end
114
+
115
+ it "the slug remains unset in the database" do
116
+ subject.class.unscoped.exists(_slugs: false).first.should eq subject
117
+ expect{subject.class.unscoped.find("slug")}.to raise_error(Mongoid::Errors::DocumentNotFound)
118
+ end
119
+ end
120
+ end
121
+
122
+ context "restore" do
123
+
124
+ before do
125
+ subject.destroy
126
+ subject.restore
127
+ end
128
+
129
+ it "resets slug value when restored" do
130
+ subject.slug.should eq "slug"
131
+ subject.reload.slug.should eq "slug"
132
+ end
133
+ end
134
+
135
+ context "multiple documents" do
136
+
137
+ it "new documents should be able to use the slug of destroyed documents" do
138
+ subject.slug.should eq "slug"
139
+ subject.destroy
140
+ subject.reload.slug.should be_nil
141
+ other_doc.slug.should eq "slug"
142
+ subject.restore
143
+ subject.slug.should eq "slug-1"
144
+ subject.reload.slug.should eq "slug-1"
145
+ end
146
+
147
+ it "should allow multiple documents to be destroyed without index conflict" do
148
+ subject.slug.should eq "slug"
149
+ subject.destroy
150
+ subject.reload.slug.should be_nil
151
+ other_doc.slug.should eq "slug"
152
+ other_doc.destroy
153
+ other_doc.reload.slug.should be_nil
154
+ end
155
+ end
156
+ end
157
+
158
+ context "non-permanent slug" do
159
+ subject { paranoid_doc }
160
+ let(:other_doc) { paranoid_doc_2 }
161
+ it_behaves_like "paranoid slugs"
162
+ end
163
+
164
+ context "permanent slug" do
165
+ subject { paranoid_perm }
166
+ let(:other_doc) { paranoid_perm_2 }
167
+ it_behaves_like "paranoid slugs"
168
+ end
169
+ end