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.
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