mongoid-slug 5.2.0 → 5.3.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.
@@ -6,15 +6,42 @@ module Mongoid
6
6
  #
7
7
  # @return [ Array(Hash, Hash) ] the indexable fields and index options.
8
8
  def self.build_index(scope_key = nil, by_model_type = false)
9
- fields = { _slugs: 1 }
10
- options = {}
11
-
12
- fields.merge!(scope_key => 1) if scope_key
9
+ # The order of field keys is intentional.
10
+ # See: http://docs.mongodb.org/manual/core/index-compound/
11
+ fields = {}
12
+ fields[:_type] = 1 if by_model_type
13
+ fields[scope_key] = 1 if scope_key
14
+ fields[:_slugs] = 1
13
15
 
14
- if by_model_type
15
- fields.merge!(_type: 1)
16
- else
17
- options.merge!(unique: true, sparse: true)
16
+ # By design, we use the unique index constraint when possible to enforce slug uniqueness.
17
+ # When migrating legacy data to Mongoid slug, the _slugs field may be null on many records,
18
+ # hence we set the sparse index option to ignore these from the unique index.
19
+ # See: http://docs.mongodb.org/manual/core/index-sparse/
20
+ #
21
+ # There are three edge cases where the index must not be unique:
22
+ #
23
+ # 1) Legacy tables with `scope_key`. The sparse indexes on compound keys (scope + _slugs) are
24
+ # whenever ANY of the key values are present (e.g. when scope is set and _slugs is unset),
25
+ # and collisions will occur when multiple records have the same scope but null slugs.
26
+ #
27
+ # 2) Single Table Inheritance (`by_model_type`). MongoDB creates indexes on the parent collection,
28
+ # irrespective of how STI is defined in Mongoid, i.e. ANY child index will be applied to EVERY child.
29
+ # This can cause collisions using various combinations of scopes.
30
+ #
31
+ # 3) Paranoid docs rely on sparse indexes to exclude paranoid-deleted records
32
+ # from the unique index constraint (i.e. when _slugs is unset.) However, when
33
+ # using compound keys (`by_model_type` or `scope_key`), paranoid-deleted records
34
+ # can become inadvertently indexed when _slugs is unset, causing duplicates. This
35
+ # is already covered by #1 and #2 above.
36
+ #
37
+ # In the future, MongoDB may implement partial indexes or improve sparse index behavior.
38
+ # See: https://jira.mongodb.org/browse/SERVER-785
39
+ # https://jira.mongodb.org/browse/SERVER-13780
40
+ # https://jira.mongodb.org/browse/SERVER-10403
41
+ options = {}
42
+ unless scope_key || by_model_type
43
+ options[:unique] = true
44
+ options[:sparse] = true
18
45
  end
19
46
 
20
47
  [fields, options]
@@ -62,8 +62,8 @@ module Mongoid
62
62
  attr_reader :model, :_slug
63
63
 
64
64
  def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
65
- :check_against_id, :reserved_words, :url_builder, :collection_name,
66
- :embedded?, :reflect_on_all_associations, :by_model_type, :slug_max_length
65
+ :check_against_id, :slug_reserved_words, :slug_url_builder, :collection_name,
66
+ :embedded?, :reflect_on_all_associations, :slug_by_model_type, :slug_max_length
67
67
 
68
68
  def initialize(model)
69
69
  @model = model
@@ -80,7 +80,7 @@ module Mongoid
80
80
  @_slug = if attempt
81
81
  attempt.to_url
82
82
  else
83
- url_builder.call(model)
83
+ slug_url_builder.call(model)
84
84
  end
85
85
 
86
86
  @_slug = @_slug[0...slug_max_length] if slug_max_length
@@ -100,7 +100,7 @@ module Mongoid
100
100
  where_hash[scope] = model.try(:read_attribute, scope)
101
101
  end
102
102
 
103
- if by_model_type == true
103
+ if slug_by_model_type == true
104
104
  where_hash[:_type] = model.try(:read_attribute, :_type)
105
105
  end
106
106
 
@@ -110,7 +110,7 @@ module Mongoid
110
110
  @state.include_slug unless model.class.look_like_slugs?([@_slug])
111
111
 
112
112
  # make sure that the slug is not equal to a reserved word
113
- @state.include_slug if reserved_words.any? { |word| word === @_slug }
113
+ @state.include_slug if slug_reserved_words.any? { |word| word === @_slug }
114
114
 
115
115
  # only look for a new unique slug if the existing slugs contains the current slug
116
116
  # - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
@@ -1,5 +1,5 @@
1
1
  module Mongoid #:nodoc:
2
2
  module Slug
3
- VERSION = '5.2.0'
3
+ VERSION = '5.3.0'.freeze
4
4
  end
5
5
  end
@@ -3,6 +3,7 @@ namespace :mongoid_slug do
3
3
  task set: :environment do |_, args|
4
4
  ::Rails.application.eager_load! if defined?(Rails)
5
5
  klasses = Module.constants.find_all do |const|
6
+ next if const == :MissingSourceFile
6
7
  const != const.upcase ? Mongoid::Slug > (Object.const_get const) : nil
7
8
  end
8
9
  klasses.map! { |klass| klass.to_s.constantize }
@@ -11,7 +12,7 @@ namespace :mongoid_slug do
11
12
  klasses = (klasses.map(&:to_s) & models.map(&:classify)).map(&:constantize) if models.any?
12
13
  klasses.each do |klass|
13
14
  # set slug for objects having blank slug
14
- klass.each { |object| object.set_slug! unless object.slugs? }
15
+ klass.each { |object| object.set_slug! unless object.slugs? && object.slugs.any? }
15
16
  end
16
17
  end
17
18
  end
@@ -3,9 +3,13 @@ class Author
3
3
  include Mongoid::Slug
4
4
  field :first_name
5
5
  field :last_name
6
- slug :first_name, :last_name, scope: :book, history: false, max_length: 256
7
- belongs_to :book
6
+ if Mongoid::Compatibility::Version.mongoid6?
7
+ belongs_to :book, required: false
8
+ else
9
+ belongs_to :book
10
+ end
8
11
  has_many :characters,
9
12
  class_name: 'Person',
10
13
  foreign_key: :author_id
14
+ slug :first_name, :last_name, scope: :book, history: false, max_length: 256
11
15
  end
@@ -4,7 +4,11 @@ class AuthorPolymorphic
4
4
  field :first_name
5
5
  field :last_name
6
6
  slug :first_name, :last_name, scope: :book_polymorphic
7
- belongs_to :book_polymorphic
7
+ if Mongoid::Compatibility::Version.mongoid6?
8
+ belongs_to :book_polymorphic, required: false
9
+ else
10
+ belongs_to :book_polymorphic
11
+ end
8
12
  has_many :characters,
9
13
  class_name: 'Person',
10
14
  foreign_key: :author_id
@@ -4,5 +4,7 @@ class ParanoidPermanent
4
4
  include Mongoid::Slug
5
5
 
6
6
  field :title
7
- slug :title, permanent: true
7
+ field :foo
8
+
9
+ slug :title, scope: :foo, permanent: true
8
10
  end
@@ -4,5 +4,9 @@ class Person
4
4
  field :name
5
5
  slug :name, permanent: true, scope: :author
6
6
  embeds_many :relationships
7
- belongs_to :author, inverse_of: :characters
7
+ if Mongoid::Compatibility::Version.mongoid6?
8
+ belongs_to :author, inverse_of: :characters, required: false
9
+ else
10
+ belongs_to :author, inverse_of: :characters
11
+ end
8
12
  end
@@ -11,17 +11,19 @@ describe Mongoid::Slug::Index do
11
11
 
12
12
  context 'when by_model_type is true' do
13
13
  let(:by_model_type) { true }
14
+
14
15
  it { is_expected.to eq [{ _slugs: 1, foo: 1, _type: 1 }, {}] }
15
16
  end
16
17
 
17
18
  context 'when by_model_type is false' do
18
- it { is_expected.to eq [{ _slugs: 1, foo: 1 }, { unique: true, sparse: true }] }
19
+ it { is_expected.to eq [{ _slugs: 1, foo: 1 }, {}] }
19
20
  end
20
21
  end
21
22
 
22
23
  context 'when scope_key is not set' do
23
24
  context 'when by_model_type is true' do
24
25
  let(:by_model_type) { true }
26
+
25
27
  it { is_expected.to eq [{ _slugs: 1, _type: 1 }, {}] }
26
28
  end
27
29
 
@@ -2,10 +2,78 @@
2
2
  require 'spec_helper'
3
3
 
4
4
  describe 'Mongoid::Paranoia with Mongoid::Slug' do
5
+ let(:paranoid_doc) { ParanoidDocument.create!(title: 'slug') }
6
+ let(:paranoid_doc_2) { ParanoidDocument.create!(title: 'slug') }
7
+ let(:paranoid_perm) { ParanoidPermanent.create!(title: 'slug') }
8
+ let(:paranoid_perm_2) { ParanoidPermanent.create!(title: 'slug') }
9
+ let(:non_paranoid_doc) { Article.create!(title: 'slug') }
10
+ subject { paranoid_doc }
11
+
12
+ describe '.paranoid?' do
13
+ context 'when Mongoid::Paranoia is included' do
14
+ subject { paranoid_doc.class }
15
+ specify { expect(subject.is_paranoid_doc?).to be true }
16
+ end
17
+
18
+ context 'when Mongoid::Paranoia not included' do
19
+ subject { non_paranoid_doc.class }
20
+ specify { expect(subject.is_paranoid_doc?).to be false }
21
+ end
22
+ end
23
+
24
+ describe '#paranoid_deleted?' do
25
+ context 'when Mongoid::Paranoia is included' do
26
+ context 'when not destroyed' do
27
+ specify { expect(subject.paranoid_deleted?).to be false }
28
+ end
29
+
30
+ context 'when destroyed' do
31
+ before { subject.destroy }
32
+ specify { expect(subject.paranoid_deleted?).to be true }
33
+ end
34
+ end
35
+
36
+ context 'when Mongoid::Paranoia not included' do
37
+ subject { non_paranoid_doc }
38
+ specify { expect(subject.paranoid_deleted?).to be false }
39
+ end
40
+ end
41
+
42
+ describe 'restore callbacks' do
43
+ context 'when Mongoid::Paranoia is included' do
44
+ subject { paranoid_doc.class }
45
+ it { is_expected.to respond_to(:before_restore) }
46
+ it { is_expected.to respond_to(:after_restore) }
47
+ end
48
+
49
+ context 'when Mongoid::Paranoia not included' do
50
+ it { is_expected.to_not respond_to(:before_restore) }
51
+ it { is_expected.to_not respond_to(:after_restore) }
52
+ end
53
+ end
54
+
55
+ describe 'index' do
56
+ context 'simple index' do
57
+ before { ParanoidDocument.create_indexes }
58
+ after { ParanoidDocument.remove_indexes }
59
+ subject { ParanoidDocument }
60
+
61
+ it_should_behave_like 'has an index', { _slugs: 1 }, unique: true, sparse: true
62
+ end
63
+
64
+ context 'compound index' do
65
+ before { ParanoidPermanent.create_indexes }
66
+ after { ParanoidPermanent.remove_indexes }
67
+ subject { ParanoidPermanent }
68
+
69
+ it_should_behave_like 'has an index', { foo: 1, _slugs: 1 }, unique: nil, sparse: nil
70
+ end
71
+ end
72
+
5
73
  shared_examples_for 'paranoid slugs' do
6
74
  context 'querying' do
7
75
  it 'returns paranoid_doc for correct slug' do
8
- expect(subject.class.find(subject.slug)).to eq(subject)
76
+ expect(subject.class.find(subject.slug)).to eq subject
9
77
  end
10
78
  end
11
79
 
@@ -27,8 +95,8 @@ describe 'Mongoid::Paranoia with Mongoid::Slug' do
27
95
  end
28
96
 
29
97
  it 'persists the removed slug' do
30
- expect(subject.reload._slugs).to eq []
31
- expect(subject.reload.slug).to be_nil
98
+ expect(subject.reload._slugs).to be nil
99
+ expect(subject.reload.slug).to eq subject._id.to_s
32
100
  end
33
101
 
34
102
  it 'persists the removed slug in the database' do
@@ -67,7 +135,7 @@ describe 'Mongoid::Paranoia with Mongoid::Slug' do
67
135
  it 'new documents should be able to use the slug of destroyed documents' do
68
136
  expect(subject.slug).to eq 'slug'
69
137
  subject.destroy
70
- expect(subject.reload.slug).to be_nil
138
+ expect(subject.reload.slug).to eq subject._id.to_s
71
139
  expect(other_doc.slug).to eq 'slug'
72
140
  subject.restore
73
141
  expect(subject.slug).to eq 'slug-1'
@@ -77,16 +145,16 @@ describe 'Mongoid::Paranoia with Mongoid::Slug' do
77
145
  it 'should allow multiple documents to be destroyed without index conflict' do
78
146
  expect(subject.slug).to eq 'slug'
79
147
  subject.destroy
80
- expect(subject.reload.slug).to be_nil
148
+ expect(subject.reload.slug).to eq subject._id.to_s
81
149
  expect(other_doc.slug).to eq 'slug'
82
150
  other_doc.destroy
83
- expect(other_doc.reload.slug).to be_nil
151
+ expect(other_doc.reload.slug).to eq other_doc._id.to_s
84
152
  end
85
153
  end
86
154
  end
87
155
 
88
156
  [ParanoidDocument, DocumentParanoid].each do |paranoid_klass|
89
- context "#{paranoid_klass}" do
157
+ context paranoid_klass.to_s do
90
158
  let(:paranoid_doc) { paranoid_klass.create!(title: 'slug') }
91
159
  let(:non_paranoid_doc) { Article.create!(title: 'slug') }
92
160
 
@@ -95,30 +163,30 @@ describe 'Mongoid::Paranoia with Mongoid::Slug' do
95
163
  describe '.paranoid?' do
96
164
  context 'when Mongoid::Paranoia is included' do
97
165
  subject { paranoid_doc.class }
98
- its(:is_paranoid_doc?) { should be_truthy }
166
+ its(:is_paranoid_doc?) { is_expected.to be_truthy }
99
167
  end
100
168
 
101
169
  context 'when Mongoid::Paranoia not included' do
102
170
  subject { non_paranoid_doc.class }
103
- its(:is_paranoid_doc?) { should be_falsey }
171
+ its(:is_paranoid_doc?) { is_expected.to be_falsey }
104
172
  end
105
173
  end
106
174
 
107
175
  describe '#paranoid_deleted?' do
108
176
  context 'when Mongoid::Paranoia is included' do
109
177
  context 'when not destroyed' do
110
- its(:paranoid_deleted?) { should be_falsey }
178
+ its(:paranoid_deleted?) { is_expected.to be_falsey }
111
179
  end
112
180
 
113
181
  context 'when destroyed' do
114
182
  before { subject.destroy }
115
- its(:paranoid_deleted?) { should be_truthy }
183
+ its(:paranoid_deleted?) { is_expected.to be_truthy }
116
184
  end
117
185
  end
118
186
 
119
187
  context 'when Mongoid::Paranoia not included' do
120
188
  subject { non_paranoid_doc }
121
- its(:paranoid_deleted?) { should be_falsey }
189
+ its(:paranoid_deleted?) { is_expected.to be_falsey }
122
190
  end
123
191
  end
124
192
 
@@ -7,8 +7,8 @@ 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
10
+ context 'special cases' do
11
+ it 'slugs are not be generated from invalid documents' do
12
12
  # this will fail now
13
13
  x = IncorrectSlugPersistence.create!(name: 'test')
14
14
  expect(x.slug).to eq('test')
@@ -23,9 +23,33 @@ module Mongoid
23
23
  x.save!
24
24
  end
25
25
 
26
- it "doesn't persist blank strings" do
26
+ it 'has no slugs for blank strings' do
27
27
  book = Book.create!(title: '')
28
- expect(book.reload.slugs).to be_empty
28
+ expect(book.reload.slugs).to be nil
29
+ end
30
+
31
+ it 'has no slugs for dashes' do
32
+ book = Book.create!(title: '-')
33
+ expect(book.reload.slugs).to be nil
34
+ end
35
+
36
+ it 'has no slugs for underscores' do
37
+ book = Book.create!(title: '_')
38
+ expect(book.reload.slugs).to be nil
39
+ end
40
+
41
+ it 'has no slugs for nil strings' do
42
+ book = Book.create!
43
+ expect(book.reload.slugs).to be nil
44
+ end
45
+
46
+ it 'works for multiple nils' do
47
+ expect do
48
+ 2.times do
49
+ Book.create!
50
+ end
51
+ end.to_not raise_error # Mongo::Error::OperationFailure
52
+ expect(Book.all.map(&:slug)).to eq Book.all.map(&:id).map(&:to_s)
29
53
  end
30
54
  end
31
55
 
@@ -271,7 +295,8 @@ module Mongoid
271
295
  let!(:author) do
272
296
  Author.create(
273
297
  first_name: 'Gilles',
274
- last_name: 'Deleuze')
298
+ last_name: 'Deleuze'
299
+ )
275
300
  end
276
301
 
277
302
  it 'generates a slug' do
@@ -288,12 +313,14 @@ module Mongoid
288
313
  it 'generates a unique slug by appending a counter to duplicate text' do
289
314
  dup = Author.create(
290
315
  first_name: author.first_name,
291
- last_name: author.last_name)
316
+ last_name: author.last_name
317
+ )
292
318
  expect(dup.to_param).to eql 'gilles-deleuze-1'
293
319
 
294
320
  dup2 = Author.create(
295
321
  first_name: author.first_name,
296
- last_name: author.last_name)
322
+ last_name: author.last_name
323
+ )
297
324
 
298
325
  dup.save
299
326
  expect(dup2.to_param).to eql 'gilles-deleuze-2'
@@ -412,7 +439,8 @@ module Mongoid
412
439
  it 'generates a unique slug by appending a counter to duplicate text' do
413
440
  dup = book.authors.create(
414
441
  first_name: author.first_name,
415
- last_name: author.last_name)
442
+ last_name: author.last_name
443
+ )
416
444
  expect(dup.to_param).to eql 'gilles-deleuze-1'
417
445
  end
418
446
 
@@ -488,6 +516,54 @@ module Mongoid
488
516
  end
489
517
  end
490
518
 
519
+ context 'when block is configured globally' do
520
+ before do
521
+ Mongoid::Slug.configure do |c|
522
+ c.slug do |cur_obj|
523
+ slug = cur_obj.slug_builder
524
+ "#{slug}-#{cur_obj.id}".to_url
525
+ end
526
+ end
527
+ end
528
+
529
+ after do
530
+ # Remove global configuration to avoid affect on
531
+ # other specs run after this spec
532
+ Mongoid::Slug.default_slug = nil
533
+ expect(Mongoid::Slug.default_slug).to be_nil
534
+ end
535
+
536
+ it 'generates a slug' do
537
+ expect(Mongoid::Slug.default_slug).to be_present
538
+ class Person
539
+ include Mongoid::Document
540
+ include Mongoid::Slug
541
+
542
+ field :name
543
+ slug :name
544
+ end
545
+
546
+ person = Person.create(name: 'John')
547
+ expect(person.to_param).to eql "john-#{person.id}"
548
+ end
549
+
550
+ it 'can be overridden at model level' do
551
+ expect(Mongoid::Slug.default_slug).to be_present
552
+ class Person
553
+ include Mongoid::Document
554
+ include Mongoid::Slug
555
+
556
+ field :name
557
+ slug :name do |cur_object|
558
+ cur_object.slug_builder.to_url
559
+ end
560
+ end
561
+
562
+ person = Person.create(name: 'John')
563
+ expect(person.to_param).to eql 'john'
564
+ end
565
+ end
566
+
491
567
  context 'when slugged field contains non-ASCII characters' do
492
568
  it 'slugs Cyrillic characters' do
493
569
  book.title = 'Капитал'
@@ -514,126 +590,108 @@ module Mongoid
514
590
  end
515
591
  end
516
592
 
517
- context 'when indexes are created' do
518
- before do
519
- Author.create_indexes
520
- Book.create_indexes
593
+ context 'when slug is not scoped by a reference association' do
594
+ subject { Book }
595
+ it_should_behave_like 'has an index', { _slugs: 1 }, unique: true, sparse: true
596
+ end
521
597
 
522
- AuthorPolymorphic.create_indexes
523
- BookPolymorphic.create_indexes
598
+ context 'with a value exceeding mongodb max index key' do
599
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
600
+ it 'errors with a model without a max length' do
601
+ expect do
602
+ Book.create!(title: 't' * 1025)
603
+ end.to raise_error Mongo::Error::OperationFailure, /key too large to index/
604
+ end
605
+ elsif Mongoid::Compatibility::Version.mongoid4?
606
+ it 'errors with a model without a max length' do
607
+ expect do
608
+ Book.create!(title: 't' * 1025)
609
+ end.to raise_error Moped::Errors::OperationFailure, /key too large to index/
610
+ end
524
611
  end
525
-
526
- after do
527
- Author.remove_indexes
528
- Book.remove_indexes
529
-
530
- AuthorPolymorphic.remove_indexes
531
- BookPolymorphic.remove_indexes
612
+ it 'succeeds with a model with a max length' do
613
+ expect do
614
+ author = Author.create!(last_name: 't' * 1025)
615
+ expect(author.slug.length).to eq 256
616
+ end.to_not raise_error
532
617
  end
618
+ end
533
619
 
534
- context 'when slug is not scoped by a reference association' do
535
- subject { Book }
536
- it_should_behave_like 'has an index', { _slugs: 1 }, unique: true, sparse: true
537
- end
620
+ context 'when slug is scoped by a reference association' do
621
+ subject { Author }
622
+ it_should_behave_like 'does not have an index', _slugs: 1
623
+ end
538
624
 
539
- context 'with a value exceeding mongodb max index key' do
540
- if Mongoid::Compatibility::Version.mongoid5?
541
- it 'errors with a model without a max length' do
542
- expect do
543
- Book.create!(title: 't' * 1025)
544
- end.to raise_error Mongo::Error::OperationFailure, /key too large to index/
545
- end
546
- elsif Mongoid::Compatibility::Version.mongoid4?
547
- it 'errors with a model without a max length' do
548
- expect do
549
- Book.create!(title: 't' * 1025)
550
- end.to raise_error Moped::Errors::OperationFailure, /key too large to index/
551
- end
552
- end
553
- it 'succeeds with a model with a max length' do
554
- expect do
555
- author = Author.create!(last_name: 't' * 1025)
556
- expect(author.slug.length).to eq 256
557
- end.to_not raise_error
558
- end
625
+ context 'for subclass scope' do
626
+ context 'when slug is not scoped by a reference association' do
627
+ subject { BookPolymorphic }
628
+ it_should_behave_like 'has an index', { _type: 1, _slugs: 1 }, unique: nil, sparse: nil
559
629
  end
560
630
 
561
631
  context 'when slug is scoped by a reference association' do
562
- subject { Author }
563
- it_should_behave_like 'does not have an index', _slugs: 1
632
+ subject { AuthorPolymorphic }
633
+ it_should_behave_like 'does not have an index', _type: 1, _slugs: 1
564
634
  end
565
635
 
566
- context 'for subclass scope' do
567
- context 'when slug is not scoped by a reference association' do
568
- subject { BookPolymorphic }
569
- it_should_behave_like 'has an index', { _type: 1, _slugs: 1 }, unique: nil, sparse: nil
570
- end
571
-
572
- context 'when slug is scoped by a reference association' do
573
- subject { AuthorPolymorphic }
574
- it_should_behave_like 'does not have an index', _type: 1, _slugs: 1
575
- end
576
-
577
- context 'when the object has STI' do
578
- it 'scopes by the subclass' do
579
- b = BookPolymorphic.create!(title: 'Book')
580
- expect(b.slug).to eq('book')
636
+ context 'when the object has STI' do
637
+ it 'scopes by the subclass' do
638
+ b = BookPolymorphic.create!(title: 'Book')
639
+ expect(b.slug).to eq('book')
581
640
 
582
- b2 = BookPolymorphic.create!(title: 'Book')
583
- expect(b2.slug).to eq('book-1')
641
+ b2 = BookPolymorphic.create!(title: 'Book')
642
+ expect(b2.slug).to eq('book-1')
584
643
 
585
- c = ComicBookPolymorphic.create!(title: 'Book')
586
- expect(c.slug).to eq('book')
644
+ c = ComicBookPolymorphic.create!(title: 'Book')
645
+ expect(c.slug).to eq('book')
587
646
 
588
- c2 = ComicBookPolymorphic.create!(title: 'Book')
589
- expect(c2.slug).to eq('book-1')
647
+ c2 = ComicBookPolymorphic.create!(title: 'Book')
648
+ expect(c2.slug).to eq('book-1')
590
649
 
591
- expect(BookPolymorphic.find('book')).to eq(b)
592
- expect(BookPolymorphic.find('book-1')).to eq(b2)
593
- expect(ComicBookPolymorphic.find('book')).to eq(c)
594
- expect(ComicBookPolymorphic.find('book-1')).to eq(c2)
595
- end
650
+ expect(BookPolymorphic.find('book')).to eq(b)
651
+ expect(BookPolymorphic.find('book-1')).to eq(b2)
652
+ expect(ComicBookPolymorphic.find('book')).to eq(c)
653
+ expect(ComicBookPolymorphic.find('book-1')).to eq(c2)
596
654
  end
597
655
  end
598
656
  end
657
+ end
599
658
 
600
- context 'for reserved words' do
601
- context 'when the :reserve option is used on the model' do
602
- it 'does not use the reserved slugs' do
603
- friend1 = Friend.create(name: 'foo')
604
- expect(friend1.slugs).not_to include('foo')
605
- expect(friend1.slugs).to include('foo-1')
659
+ context 'for reserved words' do
660
+ context 'when the :reserve option is used on the model' do
661
+ it 'does not use the reserved slugs' do
662
+ friend1 = Friend.create(name: 'foo')
663
+ expect(friend1.slugs).not_to include('foo')
664
+ expect(friend1.slugs).to include('foo-1')
606
665
 
607
- friend2 = Friend.create(name: 'bar')
608
- expect(friend2.slugs).not_to include('bar')
609
- expect(friend2.slugs).to include('bar-1')
666
+ friend2 = Friend.create(name: 'bar')
667
+ expect(friend2.slugs).not_to include('bar')
668
+ expect(friend2.slugs).to include('bar-1')
610
669
 
611
- friend3 = Friend.create(name: 'en')
612
- expect(friend3.slugs).not_to include('en')
613
- expect(friend3.slugs).to include('en-1')
614
- end
670
+ friend3 = Friend.create(name: 'en')
671
+ expect(friend3.slugs).not_to include('en')
672
+ expect(friend3.slugs).to include('en-1')
673
+ end
615
674
 
616
- it 'should start with concatenation -1' do
617
- friend1 = Friend.create(name: 'foo')
618
- expect(friend1.slugs).to include('foo-1')
619
- friend2 = Friend.create(name: 'foo')
620
- expect(friend2.slugs).to include('foo-2')
621
- end
675
+ it 'should start with concatenation -1' do
676
+ friend1 = Friend.create(name: 'foo')
677
+ expect(friend1.slugs).to include('foo-1')
678
+ friend2 = Friend.create(name: 'foo')
679
+ expect(friend2.slugs).to include('foo-2')
680
+ end
622
681
 
623
- %w(new edit).each do |word|
624
- it "should overwrite the default reserved words allowing the word '#{word}'" do
625
- friend = Friend.create(name: word)
626
- expect(friend.slugs).to include word
627
- end
682
+ %w(new edit).each do |word|
683
+ it "should overwrite the default reserved words allowing the word '#{word}'" do
684
+ friend = Friend.create(name: word)
685
+ expect(friend.slugs).to include word
628
686
  end
629
687
  end
630
- context 'when the model does not have any reserved words set' do
631
- %w(new edit).each do |word|
632
- it "does not use the default reserved word '#{word}'" do
633
- book = Book.create(title: word)
634
- expect(book.slugs).not_to include word
635
- expect(book.slugs).to include("#{word}-1")
636
- end
688
+ end
689
+ context 'when the model does not have any reserved words set' do
690
+ %w(new edit).each do |word|
691
+ it "does not use the default reserved word '#{word}'" do
692
+ book = Book.create(title: word)
693
+ expect(book.slugs).not_to include word
694
+ expect(book.slugs).to include("#{word}-1")
637
695
  end
638
696
  end
639
697
  end
@@ -667,7 +725,7 @@ module Mongoid
667
725
  let(:book) { Book.new }
668
726
 
669
727
  it 'should return nil' do
670
- expect(book.to_param).to be_nil
728
+ expect(book.to_param).to eq book._id.to_s
671
729
  end
672
730
 
673
731
  it 'should not persist the record' do
@@ -677,24 +735,22 @@ module Mongoid
677
735
  end
678
736
 
679
737
  context 'when called on an existing record with no slug' do
680
- let!(:book_no_title) { Book.create }
681
-
682
- before do
683
- if Mongoid::Compatibility::Version.mongoid5?
738
+ let!(:book_no_slug) do
739
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
684
740
  Book.collection.insert_one(title: 'Proust and Signs')
685
741
  else
686
742
  Book.collection.insert(title: 'Proust and Signs')
687
743
  end
744
+ Book.where(title: 'Proust and Signs').first
688
745
  end
689
746
 
690
747
  it 'should return the id if there is no slug' do
691
- book = Book.first
692
- expect(book.to_param).to eq(book.id.to_s)
693
- expect(book.reload.slugs).to be_empty
748
+ expect(book_no_slug.to_param).to eq(book_no_slug.id.to_s)
749
+ expect(book_no_slug.slugs).to be_nil
694
750
  end
695
751
 
696
752
  it 'should not persist the record' do
697
- expect(book_no_title.to_param).to eq(book_no_title._id.to_s)
753
+ expect(book_no_slug.to_param).to eq(book_no_slug._id.to_s)
698
754
  end
699
755
  end
700
756
  end
@@ -738,9 +794,17 @@ module Mongoid
738
794
  end
739
795
 
740
796
  it 'ensures uniqueness' do
741
- Book.create(title: 'A Thousand Plateaus', slugs: ['not-what-you-expected'])
742
- book2 = Book.create(title: 'A Thousand Plateaus', slugs: ['not-what-you-expected'])
743
- expect(book2.to_param).to eql 'not-what-you-expected-1'
797
+ book1 = Book.create(title: 'A Thousand Plateaus', slugs: ['not-what-you-expected'])
798
+ expect(book1.to_param).to eql 'not-what-you-expected'
799
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
800
+ expect do
801
+ Book.create(title: 'A Thousand Plateaus', slugs: ['not-what-you-expected'])
802
+ end.to raise_error Mongo::Error::OperationFailure, /duplicate/
803
+ elsif Mongoid::Compatibility::Version.mongoid4?
804
+ expect do
805
+ Book.create(title: 'A Thousand Plateaus', slugs: ['not-what-you-expected'])
806
+ end.to raise_error Moped::Errors::OperationFailure, /duplicate/
807
+ end
744
808
  end
745
809
 
746
810
  it 'updates the slug when a new one is passed in' do
@@ -761,8 +825,15 @@ module Mongoid
761
825
  Book.create(title: 'Sleepyhead')
762
826
  book2 = Book.create(title: 'A Thousand Plateaus')
763
827
  book2.slugs.push 'sleepyhead'
764
- book2.save
765
- expect(book2.to_param).to eql 'sleepyhead-1'
828
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
829
+ expect do
830
+ book2.save
831
+ end.to raise_error Mongo::Error::OperationFailure, /duplicate/
832
+ elsif Mongoid::Compatibility::Version.mongoid4?
833
+ expect do
834
+ book2.save
835
+ end.to raise_error Moped::Errors::OperationFailure, /duplicate/
836
+ end
766
837
  end
767
838
  end
768
839
 
@@ -1087,5 +1158,13 @@ module Mongoid
1087
1158
  expect(author3.slug.ends_with?('tt-2')).to be true
1088
1159
  end
1089
1160
  end
1161
+
1162
+ context 'has_many / belongs_to' do
1163
+ let(:book) { Book.create!(title: 'War and Peace') }
1164
+ it 'allows for duplicates with different slugs' do
1165
+ Author.create!(first_name: 'Leo', last_name: 'Tostoy')
1166
+ expect { book.authors.create!(first_name: 'Leo', last_name: 'Tostoy') }.to_not raise_error
1167
+ end
1168
+ end
1090
1169
  end
1091
1170
  end