mongoid_slug 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.md +83 -44
- data/lib/mongoid/slug.rb +295 -155
- data/lib/mongoid/slug/version.rb +1 -1
- data/spec/models/alias.rb +6 -0
- data/spec/models/book.rb +1 -1
- data/spec/models/friend.rb +6 -0
- data/spec/models/subject.rb +1 -1
- data/spec/mongoid/slug_spec.rb +175 -25
- data/spec/spec_helper.rb +6 -5
- metadata +42 -17
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,22 +1,25 @@
|
|
1
1
|
Mongoid Slug
|
2
2
|
============
|
3
3
|
|
4
|
-
Mongoid Slug generates a URL slug or permalink based on one or more
|
5
|
-
|
6
|
-
|
4
|
+
Mongoid Slug generates a URL slug or permalink based on one or more fields in a
|
5
|
+
Mongoid model. It sits idly on top of [stringex] [1], supporting non-Latin
|
6
|
+
characters.
|
7
7
|
|
8
|
-
[![travis]
|
8
|
+
[![travis] [2]] [3]
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
Installation
|
11
|
+
------------
|
12
12
|
|
13
|
-
Add
|
13
|
+
Add to your Gemfile:
|
14
14
|
|
15
15
|
```ruby
|
16
16
|
gem 'mongoid_slug'
|
17
17
|
```
|
18
18
|
|
19
|
-
|
19
|
+
Usage
|
20
|
+
-----
|
21
|
+
|
22
|
+
Set up a slug:
|
20
23
|
|
21
24
|
```ruby
|
22
25
|
class Book
|
@@ -24,34 +27,18 @@ class Book
|
|
24
27
|
include Mongoid::Slug
|
25
28
|
|
26
29
|
field :title
|
27
|
-
embeds_many :authors
|
28
|
-
|
29
30
|
slug :title
|
30
31
|
end
|
31
|
-
|
32
|
-
class Author
|
33
|
-
include Mongoid::Document
|
34
|
-
include Mongoid::Slug
|
35
|
-
|
36
|
-
field :first
|
37
|
-
field :last
|
38
|
-
embedded_in :book, :inverse_of => :authors
|
39
|
-
|
40
|
-
slug :first, :last, :as => :name
|
41
|
-
end
|
42
32
|
```
|
43
33
|
|
44
|
-
|
34
|
+
Find a record by its slug:
|
45
35
|
|
46
36
|
```ruby
|
47
|
-
# GET /books/a-thousand-plateaus
|
48
|
-
|
49
|
-
authors.
|
50
|
-
find_by_name(params[:id])
|
37
|
+
# GET /books/a-thousand-plateaus
|
38
|
+
book = Book.find_by_slug params[:book_id]
|
51
39
|
```
|
52
40
|
|
53
|
-
[Read here] [
|
54
|
-
for all available options.
|
41
|
+
[Read here] [3] for all available options.
|
55
42
|
|
56
43
|
Scoping
|
57
44
|
-------
|
@@ -61,37 +48,89 @@ To scope a slug by a reference association, pass `:scope`:
|
|
61
48
|
```ruby
|
62
49
|
class Company
|
63
50
|
include Mongoid::Document
|
51
|
+
|
64
52
|
references_many :employees
|
65
53
|
end
|
66
54
|
|
67
55
|
class Employee
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
56
|
+
include Mongoid::Document
|
57
|
+
include Mongoid::Slug
|
58
|
+
|
59
|
+
field :name
|
60
|
+
referenced_in :company
|
61
|
+
|
62
|
+
slug :name, :scope => :company
|
73
63
|
end
|
74
64
|
```
|
75
65
|
|
76
|
-
In this example, if you create an employee without associating it with
|
77
|
-
|
66
|
+
In this example, if you create an employee without associating it with any
|
67
|
+
company, the scope will fall back to the root employees collection.
|
78
68
|
|
79
|
-
Currently, if you have an irregular association name, you **must**
|
80
|
-
|
69
|
+
Currently, if you have an irregular association name, you **must** specify the
|
70
|
+
`:inverse_of` option on the other side of the assocation.
|
81
71
|
|
82
72
|
Embedded objects are automatically scoped by their parent.
|
83
73
|
|
84
|
-
|
74
|
+
The value of `:scope` can alternatively be a field within the model itself:
|
85
75
|
|
86
76
|
```ruby
|
87
77
|
class Employee
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
78
|
+
include Mongoid::Document
|
79
|
+
include Mongoid::Slug
|
80
|
+
|
81
|
+
field :name
|
82
|
+
field :company_id
|
83
|
+
|
84
|
+
slug :name, :scope => :company_id
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
History
|
89
|
+
-------
|
90
|
+
|
91
|
+
To specify that the history of a document should be kept track of, pass
|
92
|
+
`:history` with a value of `true`.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class Page
|
96
|
+
include Mongoid::Document
|
97
|
+
include Mongoid::Slug
|
98
|
+
|
99
|
+
field :title
|
100
|
+
|
101
|
+
slug :title, history: true
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
The document will then be returned for any of the saved slugs:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
page = Page.new title: "Home"
|
109
|
+
page.save
|
110
|
+
page.update_attributes title: "Welcome"
|
111
|
+
|
112
|
+
Page.find_by_slug("welcome") == Page.find_by_slug("home") #=> true
|
113
|
+
```
|
114
|
+
|
115
|
+
Reserved Slugs
|
116
|
+
--------------
|
117
|
+
|
118
|
+
Pass words you do not want to be slugged using the `reserve` option:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class Friend
|
122
|
+
include Mongoid::Document
|
123
|
+
|
124
|
+
field :name
|
125
|
+
slug :name, reserve: ['admin', 'root']
|
93
126
|
end
|
127
|
+
|
128
|
+
friend = Friend.create name: 'admin'
|
129
|
+
Friend.find_by_slug('admin') # => nil
|
130
|
+
friend.slug # => 'admin-1'
|
94
131
|
```
|
95
132
|
|
96
133
|
[1]: https://github.com/rsl/stringex/
|
97
|
-
[2]: https://
|
134
|
+
[2]: https://secure.travis-ci.org/hakanensari/mongoid-slug.png
|
135
|
+
[3]: http://travis-ci.org/hakanensari/mongoid-slug
|
136
|
+
[4]: https://github.com/hakanensari/mongoid-slug/blob/master/lib/mongoid/slug.rb
|
data/lib/mongoid/slug.rb
CHANGED
@@ -1,213 +1,353 @@
|
|
1
1
|
require 'mongoid'
|
2
2
|
require 'stringex'
|
3
3
|
|
4
|
-
module Mongoid
|
5
|
-
|
6
|
-
#
|
7
|
-
# one or more fields in a Mongoid model.
|
8
|
-
#
|
9
|
-
# class Person
|
10
|
-
# include Mongoid::Document
|
11
|
-
# include Mongoid::Slug
|
12
|
-
#
|
13
|
-
# field :name
|
14
|
-
# slug :name
|
15
|
-
# end
|
16
|
-
#
|
4
|
+
module Mongoid
|
5
|
+
# The Slug module helps you generate a URL slug or permalink based on one or
|
6
|
+
# more fields in a Mongoid model.
|
17
7
|
module Slug
|
18
8
|
extend ActiveSupport::Concern
|
19
9
|
|
20
10
|
included do
|
21
11
|
cattr_accessor :slug_builder,
|
22
|
-
:slugged_fields,
|
23
12
|
:slug_name,
|
24
|
-
:
|
13
|
+
:slug_history_name,
|
14
|
+
:slug_scope,
|
15
|
+
:reserved_words_in_slug,
|
16
|
+
:slugged_attributes
|
25
17
|
end
|
26
18
|
|
27
19
|
module ClassMethods
|
28
|
-
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
# The options hash respects the following members:
|
34
|
-
#
|
35
|
-
# * `:as`, which specifies name of the field that stores the
|
36
|
-
# slug. Defaults to `slug`.
|
37
|
-
#
|
38
|
-
# * `:scope`, which specifies a reference association to scope
|
39
|
-
# the slug by. Embedded documents are by default scoped by their
|
40
|
-
# parent.
|
41
|
-
#
|
42
|
-
# * `:permanent`, which specifies whether the slug should be
|
43
|
-
# immutable once created. Defaults to `false`.
|
20
|
+
# @overload slug(*fields)
|
21
|
+
# Sets one ore more fields as source of slug.
|
22
|
+
# @param [Array] fields One or more fields the slug should be based on.
|
23
|
+
# @yield If given, the block is used to build a custom slug.
|
44
24
|
#
|
45
|
-
# *
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
25
|
+
# @overload slug(*fields, options)
|
26
|
+
# Sets one ore more fields as source of slug.
|
27
|
+
# @param [Array] fields One or more fields the slug should be based on.
|
28
|
+
# @param [Hash] options
|
29
|
+
# @param options [String] :as The name of the field that stores the
|
30
|
+
# slug. Defaults to `slug`.
|
31
|
+
# @param options [Boolean] :history Whether a history of changes to
|
32
|
+
# the slug should be retained. When searched by slug, the document now
|
33
|
+
# matches both past and present slugs.
|
34
|
+
# @param options [Boolean] :index Whether an index should be defined
|
35
|
+
# on the slug field. Defaults to `false` and has no effect if the
|
36
|
+
# document is embedded.
|
37
|
+
# Make sure you have a unique index on the slugs of root documents to
|
38
|
+
# avoid race conditions.
|
39
|
+
# @param options [Boolean] :permanent Whether the slug should be
|
40
|
+
# immutable. Defaults to `false`.
|
41
|
+
# @param options [Array] :reserve` A list of reserved slugs
|
42
|
+
# @param options :scope [Symbol] a reference association or field to
|
43
|
+
# scope the slug by. Embedded documents are, by default, scoped by
|
44
|
+
# their parent.
|
45
|
+
# @yield If given, a block is used to build a slug.
|
51
46
|
#
|
52
|
-
#
|
53
|
-
#
|
47
|
+
# @example A custom builder
|
48
|
+
# class Person
|
49
|
+
# include Mongoid::Document
|
50
|
+
# include Mongoid::Slug
|
54
51
|
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
# class Person
|
61
|
-
# include Mongoid::Document
|
62
|
-
# include Mongoid::Slug
|
63
|
-
#
|
64
|
-
# field :names, :type => Array
|
65
|
-
# slug :names do |doc|
|
66
|
-
# doc.names.join(' ')
|
67
|
-
# end
|
52
|
+
# field :names, :type => Array
|
53
|
+
# slug :names do |doc|
|
54
|
+
# doc.names.join(' ')
|
55
|
+
# end
|
56
|
+
# end
|
68
57
|
#
|
69
58
|
def slug(*fields, &block)
|
70
|
-
options
|
71
|
-
|
72
|
-
|
73
|
-
self.
|
74
|
-
|
75
|
-
self.
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
59
|
+
options = fields.extract_options!
|
60
|
+
options[:history] = false if options[:permanent]
|
61
|
+
|
62
|
+
self.slug_scope = options[:scope]
|
63
|
+
self.reserved_words_in_slug = options[:reserve] || []
|
64
|
+
self.slug_name = options[:as] || :slug
|
65
|
+
self.slugged_attributes = fields.map(&:to_s)
|
66
|
+
if options[:history] && !options[:permanent]
|
67
|
+
self.slug_history_name = "#{self.slug_name}_history".to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
default_builder = lambda do |doc|
|
71
|
+
slugged_attributes.map { |f| doc.send f }.join ' '
|
72
|
+
end
|
73
|
+
self.slug_builder = block_given? ? block : default_builder
|
84
74
|
|
85
75
|
field slug_name
|
86
76
|
|
77
|
+
if slug_history_name
|
78
|
+
field slug_history_name, :type => Array, :default => []
|
79
|
+
end
|
80
|
+
|
87
81
|
if options[:index]
|
88
|
-
index
|
82
|
+
index slug_name, :unique => !slug_scope
|
83
|
+
index slug_history_name if slug_history_name
|
89
84
|
end
|
90
85
|
|
91
|
-
|
92
|
-
|
93
|
-
else
|
94
|
-
before_save :generate_slug
|
86
|
+
set_callback options[:permanent] ? :create : :save, :before do |doc|
|
87
|
+
doc.build_slug if doc.slug_should_be_rebuilt?
|
95
88
|
end
|
96
89
|
|
97
|
-
# Build a finder
|
90
|
+
# Build a finder for slug.
|
98
91
|
#
|
99
92
|
# Defaults to `find_by_slug`.
|
100
93
|
instance_eval <<-CODE
|
101
94
|
def self.find_by_#{slug_name}(slug)
|
102
|
-
|
95
|
+
if slug_history_name
|
96
|
+
any_of({ slug_name => slug }, { slug_history_name => slug })
|
97
|
+
else
|
98
|
+
where(slug_name => slug)
|
99
|
+
end.first
|
103
100
|
end
|
104
101
|
|
105
102
|
def self.find_by_#{slug_name}!(slug)
|
106
|
-
|
107
|
-
raise(Mongoid::Errors::DocumentNotFound.new
|
103
|
+
self.find_by_#{slug_name}(slug) ||
|
104
|
+
raise(Mongoid::Errors::DocumentNotFound.new self, slug)
|
108
105
|
end
|
109
106
|
CODE
|
110
|
-
end
|
111
|
-
end
|
112
107
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
108
|
+
# Build a scope based on the slug name.
|
109
|
+
#
|
110
|
+
# Defaults to `by_slug`.
|
111
|
+
scope "by_#{slug_name}".to_sym, lambda { |slug|
|
112
|
+
if slug_history_name
|
113
|
+
any_of({ slug_name => slug }, { slug_history_name => slug })
|
114
|
+
else
|
115
|
+
where(slug_name => slug)
|
116
|
+
end
|
117
|
+
}
|
119
118
|
end
|
120
|
-
|
119
|
+
|
120
|
+
# Finds a unique slug, were specified string used to generate a slug.
|
121
|
+
#
|
122
|
+
# Returned slug will the same as the specified string when there are no
|
123
|
+
# duplicates.
|
124
|
+
#
|
125
|
+
# @param [String] desired_slug
|
126
|
+
# @param [Hash] options
|
127
|
+
# @param options [Symbol] :scope The scope that should be used to
|
128
|
+
# generate the slug, if the class creates scoped slugs. Defaults to
|
129
|
+
# `nil`.
|
130
|
+
# @param options [Constant] :model The model that the slug should be
|
131
|
+
# generated for. This option overrides `:scope`, as the scope can now
|
132
|
+
# be extracted from the model. Defaults to `nil`.
|
133
|
+
# @return [String] A unique slug
|
134
|
+
def find_unique_slug_for(desired_slug, options = {})
|
135
|
+
if slug_scope && self.reflect_on_association(slug_scope).nil?
|
136
|
+
scope_object = uniqueness_scope(options[:model])
|
137
|
+
scope_attribute = options[:scope] || options[:model].try(:read_attribute, slug_scope)
|
138
|
+
else
|
139
|
+
scope_object = options[:scope] || uniqueness_scope(options[:model])
|
140
|
+
scope_attribute = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
excluded_id = options[:model]._id if options[:model]
|
144
|
+
|
145
|
+
slug = desired_slug.to_url
|
146
|
+
|
147
|
+
# Regular expression that matches slug, slug-1, ... slug-n
|
148
|
+
# If slug_name field was indexed, MongoDB will utilize that
|
149
|
+
# index to match /^.../ pattern.
|
150
|
+
pattern = /^#{Regexp.escape(slug)}(?:-(\d+))?$/
|
121
151
|
|
122
|
-
|
152
|
+
if slug_scope &&
|
153
|
+
self.reflect_on_association(slug_scope).nil?
|
154
|
+
# scope is not an association, so it's scoped to a local field
|
155
|
+
# (e.g. an association id in a denormalized db design)
|
123
156
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# Regular expression that matches slug, slug-1, ... slug-n
|
129
|
-
# If slug_name field was indexed, MongoDB will utilize that
|
130
|
-
# index to match /^.../ pattern.
|
131
|
-
pattern = /^#{Regexp.escape(slug)}(?:-(\d+))?$/
|
132
|
-
|
133
|
-
if slug_scope &&
|
134
|
-
self.class.reflect_on_association(slug_scope).nil?
|
135
|
-
# scope is not an association, so it's scoped to a local field
|
136
|
-
# (e.g. an association id in a denormalized db design)
|
137
|
-
existing_slugs =
|
138
|
-
self.class.
|
139
|
-
only(slug_name).
|
140
|
-
where(slug_name => pattern,
|
141
|
-
:_id.ne => _id,
|
142
|
-
slug_scope => self[slug_scope])
|
143
|
-
else
|
144
|
-
existing_slugs =
|
145
|
-
uniqueness_scope.
|
146
|
-
only(slug_name).
|
147
|
-
where(slug_name => pattern, :_id.ne => _id)
|
148
|
-
end
|
157
|
+
where_hash = {}
|
158
|
+
where_hash[slug_name] = pattern
|
159
|
+
where_hash[:_id.ne] = excluded_id if excluded_id
|
160
|
+
where_hash[slug_scope] = scope_attribute
|
149
161
|
|
150
|
-
|
151
|
-
|
152
|
-
|
162
|
+
existing_slugs =
|
163
|
+
deepest_document_superclass.
|
164
|
+
only(slug_name).
|
165
|
+
where(where_hash)
|
166
|
+
else
|
167
|
+
where_hash = {}
|
168
|
+
where_hash[slug_name] = pattern
|
169
|
+
where_hash[:_id.ne] = excluded_id if excluded_id
|
170
|
+
|
171
|
+
existing_slugs =
|
172
|
+
scope_object.
|
173
|
+
only(slug_name).
|
174
|
+
where(where_hash)
|
175
|
+
end
|
176
|
+
|
177
|
+
existing_slugs = existing_slugs.map do |doc|
|
178
|
+
doc.slug
|
179
|
+
end
|
180
|
+
|
181
|
+
if slug_history_name
|
182
|
+
if slug_scope &&
|
183
|
+
self.reflect_on_association(slug_scope).nil?
|
184
|
+
# scope is not an association, so it's scoped to a local field
|
185
|
+
# (e.g. an association id in a denormalized db design)
|
186
|
+
|
187
|
+
where_hash = {}
|
188
|
+
where_hash[slug_history_name.all] = [pattern]
|
189
|
+
where_hash[:_id.ne] = excluded_id if excluded_id
|
190
|
+
where_hash[slug_scope] = scope_attribute
|
191
|
+
|
192
|
+
history_slugged_documents =
|
193
|
+
deepest_document_superclass.
|
194
|
+
where(where_hash)
|
195
|
+
else
|
196
|
+
where_hash = {}
|
197
|
+
where_hash[slug_history_name.all] = [pattern]
|
198
|
+
where_hash[:_id.ne] = excluded_id if excluded_id
|
199
|
+
|
200
|
+
history_slugged_documents =
|
201
|
+
scope_object.
|
202
|
+
where(where_hash)
|
203
|
+
end
|
153
204
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
205
|
+
existing_history_slugs = []
|
206
|
+
history_slugged_documents.each do |doc|
|
207
|
+
history_slugs = doc.read_attribute(slug_history_name)
|
208
|
+
next if history_slugs.nil?
|
209
|
+
existing_history_slugs.push(*history_slugs.find_all { |slug| slug =~ pattern })
|
210
|
+
end
|
211
|
+
|
212
|
+
# If the only conflict is in the history of a document in the same scope,
|
213
|
+
# transfer the slug
|
214
|
+
if slug_scope && existing_slugs.count == 0 && existing_history_slugs.count > 0
|
215
|
+
history_slugged_documents.each do |doc|
|
216
|
+
doc_history_slugs = doc.read_attribute(slug_history_name)
|
217
|
+
next if doc_history_slugs.nil?
|
218
|
+
doc_history_slugs -= existing_history_slugs
|
219
|
+
doc.write_attribute(slug_history_name, doc_history_slugs)
|
220
|
+
doc.save
|
221
|
+
end
|
222
|
+
existing_history_slugs = []
|
223
|
+
end
|
224
|
+
|
225
|
+
existing_slugs += existing_history_slugs
|
226
|
+
end
|
227
|
+
|
228
|
+
if reserved_words_in_slug.any? { |word| word === slug }
|
229
|
+
existing_slugs << slug
|
161
230
|
end
|
162
|
-
max = existing_slugs.last.match(/-(\d+)$/).try(:[], 1).to_i
|
163
231
|
|
164
|
-
|
232
|
+
if existing_slugs.count > 0
|
233
|
+
# Sort the existing_slugs in increasing order by comparing the
|
234
|
+
# suffix numbers:
|
235
|
+
# slug, slug-1, slug-2, ..., slug-n
|
236
|
+
existing_slugs.sort! do |a, b|
|
237
|
+
(pattern.match(a)[1] || -1).to_i <=>
|
238
|
+
(pattern.match(b)[1] || -1).to_i
|
239
|
+
end
|
240
|
+
max = existing_slugs.last.match(/-(\d+)$/).try(:[], 1).to_i
|
241
|
+
|
242
|
+
slug += "-#{max + 1}"
|
243
|
+
end
|
244
|
+
|
245
|
+
slug
|
165
246
|
end
|
247
|
+
|
248
|
+
private
|
249
|
+
|
250
|
+
def uniqueness_scope(model = nil)
|
251
|
+
if model
|
252
|
+
if slug_scope && (metadata = self.reflect_on_association(slug_scope))
|
253
|
+
parent = model.send(metadata.name)
|
166
254
|
|
167
|
-
|
255
|
+
# Make sure doc is actually associated with something, and that
|
256
|
+
# some referenced docs have been persisted to the parent
|
257
|
+
#
|
258
|
+
# TODO: we need better reflection for reference associations,
|
259
|
+
# like association_name instead of forcing collection_name here
|
260
|
+
# -- maybe in the forthcoming Mongoid refactorings?
|
261
|
+
inverse = metadata.inverse_of || collection_name
|
262
|
+
return parent.respond_to?(inverse) ? parent.send(inverse) : self
|
263
|
+
end
|
264
|
+
if embedded?
|
265
|
+
parent_metadata = reflect_on_all_associations(:embedded_in)[0]
|
266
|
+
return model._parent.send(parent_metadata.inverse_of || model.metadata.name)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
deepest_document_superclass
|
270
|
+
end
|
271
|
+
|
272
|
+
def deepest_document_superclass
|
273
|
+
appropriate_class = self
|
274
|
+
while appropriate_class.superclass.include?(Mongoid::Document)
|
275
|
+
appropriate_class = appropriate_class.superclass
|
276
|
+
end
|
277
|
+
appropriate_class
|
278
|
+
end
|
168
279
|
end
|
169
280
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
281
|
+
# Builds a new slug.
|
282
|
+
#
|
283
|
+
# @return [true]
|
284
|
+
def build_slug
|
285
|
+
old = slug
|
286
|
+
write_attribute slug_name, find_unique_slug
|
287
|
+
|
288
|
+
# @note I find it odd that we can't use `slug_was`, `slug_changed?`, or
|
289
|
+
# `read_attribute (slug_history_name)` here.
|
290
|
+
|
291
|
+
if slug_history_name && old && old != slug
|
292
|
+
self.send(slug_history_name).<<(old).uniq!
|
177
293
|
end
|
294
|
+
|
295
|
+
true
|
178
296
|
end
|
179
297
|
|
180
|
-
|
181
|
-
|
298
|
+
# Finds a unique slug, were specified string used to generate a slug.
|
299
|
+
#
|
300
|
+
# Returned slug will the same as the specified string when there are no
|
301
|
+
# duplicates.
|
302
|
+
#
|
303
|
+
# @param [String] Desired slug
|
304
|
+
# @return [String] A unique slug
|
305
|
+
def find_unique_slug_for(desired_slug)
|
306
|
+
self.class.find_unique_slug_for desired_slug, :model => self
|
182
307
|
end
|
183
308
|
|
184
|
-
|
185
|
-
|
309
|
+
# @return [Boolean] Whether the slug requires to be rebuilt
|
310
|
+
def slug_should_be_rebuilt?
|
311
|
+
new_record? or slug_changed? or slugged_attributes_changed?
|
186
312
|
end
|
187
313
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
314
|
+
unless self.respond_to? :slug
|
315
|
+
def slug
|
316
|
+
read_attribute slug_name
|
317
|
+
end
|
192
318
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
319
|
+
def slug_changed?
|
320
|
+
attribute_changed? slug_name
|
321
|
+
end
|
322
|
+
|
323
|
+
def slug_was
|
324
|
+
attribute_was slug_name
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def slugged_attributes_changed?
|
329
|
+
slugged_attributes.any? { |f| attribute_changed? f }
|
330
|
+
end
|
331
|
+
|
332
|
+
# @return [String] A string which Action Pack uses for constructing an URL
|
333
|
+
# to this record.
|
334
|
+
def to_param
|
335
|
+
unless slug
|
336
|
+
build_slug
|
337
|
+
save
|
210
338
|
end
|
339
|
+
|
340
|
+
slug
|
341
|
+
end
|
342
|
+
|
343
|
+
private
|
344
|
+
|
345
|
+
def find_unique_slug
|
346
|
+
find_unique_slug_for user_defined_slug || slug_builder.call(self)
|
347
|
+
end
|
348
|
+
|
349
|
+
def user_defined_slug
|
350
|
+
slug if new_record? and slug.present? or slug_changed?
|
211
351
|
end
|
212
352
|
end
|
213
353
|
end
|
data/lib/mongoid/slug/version.rb
CHANGED
data/spec/models/book.rb
CHANGED
data/spec/models/subject.rb
CHANGED
data/spec/mongoid/slug_spec.rb
CHANGED
@@ -58,7 +58,7 @@ module Mongoid
|
|
58
58
|
|
59
59
|
it "generates a unique slug by appending a counter to duplicate text" do
|
60
60
|
dup = book.subjects.create(:name => subject.name)
|
61
|
-
dup.to_param.should eql
|
61
|
+
dup.to_param.should eql "psychoanalysis-1"
|
62
62
|
end
|
63
63
|
|
64
64
|
it "does not update slug if slugged fields have not changed" do
|
@@ -147,26 +147,25 @@ module Mongoid
|
|
147
147
|
dup = Author.create(
|
148
148
|
:first_name => author.first_name,
|
149
149
|
:last_name => author.last_name)
|
150
|
-
dup.to_param.should eql
|
150
|
+
dup.to_param.should eql "gilles-deleuze-1"
|
151
151
|
|
152
152
|
dup2 = Author.create(
|
153
153
|
:first_name => author.first_name,
|
154
154
|
:last_name => author.last_name)
|
155
155
|
|
156
156
|
dup.save
|
157
|
-
dup2.to_param.should eql
|
157
|
+
dup2.to_param.should eql "gilles-deleuze-2"
|
158
158
|
end
|
159
159
|
|
160
160
|
it "does not update slug if slugged fields have changed but generated slug is identical" do
|
161
161
|
author.last_name = "DELEUZE"
|
162
162
|
author.save
|
163
|
-
author.to_param.should eql
|
163
|
+
author.to_param.should eql "gilles-deleuze"
|
164
164
|
end
|
165
165
|
|
166
166
|
it "finds by slug" do
|
167
167
|
Author.find_by_slug("gilles-deleuze").should eql author
|
168
168
|
end
|
169
|
-
|
170
169
|
end
|
171
170
|
|
172
171
|
context "when :as is passed as an argument" do
|
@@ -196,6 +195,40 @@ module Mongoid
|
|
196
195
|
end
|
197
196
|
end
|
198
197
|
|
198
|
+
context "when :history is passed as an argument" do
|
199
|
+
let(:book) do
|
200
|
+
Book.create(:title => "Book Title")
|
201
|
+
end
|
202
|
+
|
203
|
+
before(:each) do
|
204
|
+
book.title = "Other Book Title"
|
205
|
+
book.save
|
206
|
+
end
|
207
|
+
|
208
|
+
it "saves the old slug in the owner's history" do
|
209
|
+
book.slug_history.should include("book-title")
|
210
|
+
end
|
211
|
+
|
212
|
+
it "returns the document for the old slug" do
|
213
|
+
Book.find_by_slug("book-title").should == book
|
214
|
+
end
|
215
|
+
|
216
|
+
it "returns the document for the new slug" do
|
217
|
+
Book.find_by_slug("other-book-title").should == book
|
218
|
+
end
|
219
|
+
|
220
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
221
|
+
dup = Book.create(:title => "Book Title")
|
222
|
+
dup.to_param.should eql "book-title-1"
|
223
|
+
end
|
224
|
+
|
225
|
+
it "ensures no duplicate values are stored in history" do
|
226
|
+
book.update_attributes :title => 'Book Title'
|
227
|
+
book.update_attributes :title => 'Foo'
|
228
|
+
book.slug_history.find_all { |slug| slug == 'book-title' }.size.should eql 1
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
199
232
|
context "when slug is scoped by a reference association" do
|
200
233
|
let(:author) do
|
201
234
|
book.authors.create(:first_name => "Gilles", :last_name => "Deleuze")
|
@@ -214,7 +247,7 @@ module Mongoid
|
|
214
247
|
dup = book.authors.create(
|
215
248
|
:first_name => author.first_name,
|
216
249
|
:last_name => author.last_name)
|
217
|
-
dup.to_param.should eql
|
250
|
+
dup.to_param.should eql "gilles-deleuze-1"
|
218
251
|
end
|
219
252
|
|
220
253
|
context "with an irregular association name" do
|
@@ -235,46 +268,70 @@ module Mongoid
|
|
235
268
|
dup.to_param.should eql character.to_param
|
236
269
|
end
|
237
270
|
end
|
271
|
+
|
272
|
+
context "when using history and reusing a slug within the scope" do
|
273
|
+
let!(:subject1) do
|
274
|
+
book.subjects.create(:name => "A Subject")
|
275
|
+
end
|
276
|
+
let!(:subject2) do
|
277
|
+
book.subjects.create(:name => "Another Subject")
|
278
|
+
end
|
279
|
+
|
280
|
+
before(:each) do
|
281
|
+
subject1.name = "Something Else Entirely"
|
282
|
+
subject1.save
|
283
|
+
subject2.name = "A Subject"
|
284
|
+
subject2.save
|
285
|
+
end
|
286
|
+
|
287
|
+
it "allows using the slug" do
|
288
|
+
subject2.slug.should == "a-subject"
|
289
|
+
end
|
290
|
+
|
291
|
+
it "removes the slug from the old owner's history" do
|
292
|
+
subject1.slug_history.should_not include("a-subject")
|
293
|
+
end
|
294
|
+
end
|
238
295
|
end
|
239
|
-
|
296
|
+
|
240
297
|
context "when slug is scoped by one of the class's own fields" do
|
241
298
|
let!(:magazine) do
|
242
299
|
Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
|
243
300
|
end
|
244
301
|
|
245
302
|
it "should scope by local field" do
|
246
|
-
magazine.to_param.should eql
|
303
|
+
magazine.to_param.should eql "big-weekly"
|
247
304
|
magazine2 = Magazine.create(:title => "Big Weekly", :publisher_id => "def456")
|
248
305
|
magazine2.to_param.should eql magazine.to_param
|
249
306
|
end
|
250
307
|
|
251
308
|
it "should generate a unique slug by appending a counter to duplicate text" do
|
252
309
|
dup = Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
|
253
|
-
dup.to_param.should eql
|
310
|
+
dup.to_param.should eql "big-weekly-1"
|
254
311
|
end
|
255
312
|
end
|
256
313
|
|
257
|
-
context "when
|
314
|
+
context "when #slug is given a block" do
|
258
315
|
let(:caption) do
|
259
|
-
Caption.create(:identity =>
|
260
|
-
:title =>
|
261
|
-
:medium =>
|
316
|
+
Caption.create(:identity => "Edward Hopper (American, 1882-1967)",
|
317
|
+
:title => "Soir Bleu, 1914",
|
318
|
+
:medium => "Oil on Canvas")
|
262
319
|
end
|
263
320
|
|
264
321
|
it "generates a slug" do
|
265
|
-
caption.to_param.should eql
|
322
|
+
caption.to_param.should eql "edward-hopper-soir-bleu-1914"
|
266
323
|
end
|
267
324
|
|
268
325
|
it "updates the slug" do
|
269
|
-
caption.title =
|
326
|
+
caption.title = "Road in Maine, 1914"
|
270
327
|
caption.save
|
271
328
|
caption.to_param.should eql "edward-hopper-road-in-maine-1914"
|
272
329
|
end
|
273
330
|
|
274
331
|
it "does not change slug if slugged fields have changed but generated slug is identical" do
|
275
|
-
caption.identity =
|
332
|
+
caption.identity = "Edward Hopper"
|
276
333
|
caption.save
|
277
|
-
caption.to_param.should eql
|
334
|
+
caption.to_param.should eql "edward-hopper-soir-bleu-1914"
|
278
335
|
end
|
279
336
|
|
280
337
|
it "finds by slug" do
|
@@ -298,13 +355,13 @@ module Mongoid
|
|
298
355
|
it "slugs Chinese characters" do
|
299
356
|
book.title = "中文"
|
300
357
|
book.save
|
301
|
-
book.to_param.should eql
|
358
|
+
book.to_param.should eql "zhong-wen"
|
302
359
|
end
|
303
360
|
|
304
361
|
it "slugs non-ASCII Latin characters" do
|
305
|
-
book.title =
|
362
|
+
book.title = "Paul Cézanne"
|
306
363
|
book.save
|
307
|
-
book.to_param.should eql
|
364
|
+
book.to_param.should eql "paul-cezanne"
|
308
365
|
end
|
309
366
|
end
|
310
367
|
|
@@ -340,6 +397,29 @@ module Mongoid
|
|
340
397
|
Person.collection.index_information.should_not have_key "permalink_1"
|
341
398
|
end
|
342
399
|
end
|
400
|
+
|
401
|
+
context "when :reserve is passed" do
|
402
|
+
it "does not use the the reserved slugs" do
|
403
|
+
friend1 = Friend.create(:name => "foo")
|
404
|
+
friend1.slug.should_not eql("foo")
|
405
|
+
friend1.slug.should eql("foo-1")
|
406
|
+
|
407
|
+
friend2 = Friend.create(:name => "bar")
|
408
|
+
friend2.slug.should_not eql("bar")
|
409
|
+
friend2.slug.should eql("bar-1")
|
410
|
+
|
411
|
+
friend3 = Friend.create(:name => "en")
|
412
|
+
friend3.slug.should_not eql("en")
|
413
|
+
friend3.slug.should eql("en-1")
|
414
|
+
end
|
415
|
+
|
416
|
+
it "should start with concatenation -1" do
|
417
|
+
friend1 = Friend.create(:name => "foo")
|
418
|
+
friend1.slug.should eql("foo-1")
|
419
|
+
friend2 = Friend.create(:name => "foo")
|
420
|
+
friend2.slug.should eql("foo-2")
|
421
|
+
end
|
422
|
+
end
|
343
423
|
|
344
424
|
context "when the object has STI" do
|
345
425
|
it "scopes by the superclass" do
|
@@ -349,6 +429,25 @@ module Mongoid
|
|
349
429
|
end
|
350
430
|
end
|
351
431
|
|
432
|
+
context "when slug defined on alias of field" do
|
433
|
+
it "should use accessor, not alias" do
|
434
|
+
pseudonim = Alias.create(:author_name => "Max Stirner")
|
435
|
+
pseudonim.slug.should eql("max-stirner")
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
describe ".by_slug scope" do
|
440
|
+
let!(:author) { book.authors.create(:first_name => "Gilles", :last_name => "Deleuze") }
|
441
|
+
|
442
|
+
it "returns an empty array if no document is found" do
|
443
|
+
book.authors.by_slug("never-heard-of").should == []
|
444
|
+
end
|
445
|
+
|
446
|
+
it "returns an array containing the document if it is found" do
|
447
|
+
book.authors.by_slug(author.slug).should == [author]
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
352
451
|
describe ".find_by_slug" do
|
353
452
|
let!(:book) { Book.create(:title => "A Thousand Plateaus") }
|
354
453
|
|
@@ -386,11 +485,62 @@ module Mongoid
|
|
386
485
|
book.reload.slug.should eql "proust-and-signs"
|
387
486
|
end
|
388
487
|
end
|
389
|
-
|
390
|
-
|
391
|
-
it "
|
392
|
-
|
393
|
-
|
488
|
+
|
489
|
+
describe ".for_unique_slug_for" do
|
490
|
+
it "returns the unique slug" do
|
491
|
+
Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus")
|
492
|
+
end
|
493
|
+
|
494
|
+
it "returns the unique slug with a counter if necessary" do
|
495
|
+
Book.create(:title => "A Thousand Plateaus")
|
496
|
+
Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus-1")
|
497
|
+
end
|
498
|
+
|
499
|
+
it "returns the unique slug as if it were the provided object" do
|
500
|
+
book = Book.create(:title => "A Thousand Plateaus")
|
501
|
+
Book.find_unique_slug_for("A Thousand Plateaus", :model => book).should eq("a-thousand-plateaus")
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
describe "#find_unique_slug_for" do
|
506
|
+
let!(:book) { Book.create(:title => "A Thousand Plateaus") }
|
507
|
+
|
508
|
+
it "returns the unique slug" do
|
509
|
+
book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus")
|
510
|
+
end
|
511
|
+
|
512
|
+
it "returns the unique slug with a counter if necessary" do
|
513
|
+
Book.create(:title => "Anti Oedipus")
|
514
|
+
book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus-1")
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
context "when the slugged field is set manually" do
|
519
|
+
context "when it set to a non-empty string" do
|
520
|
+
it "respects the provided slug" do
|
521
|
+
book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
|
522
|
+
book.to_param.should eql "not-what-you-expected"
|
523
|
+
end
|
524
|
+
|
525
|
+
it "ensures uniqueness" do
|
526
|
+
book1 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
|
527
|
+
book2 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
|
528
|
+
book2.to_param.should eql "not-what-you-expected-1"
|
529
|
+
end
|
530
|
+
|
531
|
+
it "updates the slug when a new one is passed in" do
|
532
|
+
book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
|
533
|
+
book.slug = "not-it-either"
|
534
|
+
book.save
|
535
|
+
book.to_param.should eql "not-it-either"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context "when it is set to an empty string" do
|
540
|
+
it "generate a new one" do
|
541
|
+
book = Book.create(:title => "A Thousand Plateaus", :slug => "")
|
542
|
+
book.to_param.should eql "a-thousand-plateaus"
|
543
|
+
end
|
394
544
|
end
|
395
545
|
end
|
396
546
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
3
|
|
4
|
-
require
|
4
|
+
require 'pry'
|
5
|
+
require 'rspec'
|
5
6
|
|
6
|
-
require File.expand_path(
|
7
|
+
require File.expand_path('../../lib/mongoid/slug', __FILE__)
|
7
8
|
|
8
9
|
Mongoid.configure do |config|
|
9
|
-
name =
|
10
|
+
name = 'mongoid_slug_test'
|
10
11
|
config.master = Mongo::Connection.new.db(name)
|
11
12
|
end
|
12
13
|
|
metadata
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid_slug
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
-
-
|
8
|
+
- Hakan Ensari
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-03-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mongoid
|
16
|
-
requirement: &
|
16
|
+
requirement: &70314214792060 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '2.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70314214792060
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: stringex
|
27
|
-
requirement: &
|
27
|
+
requirement: &70314214791560 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,32 @@ dependencies:
|
|
32
32
|
version: '1.3'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70314214791560
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bson_ext
|
38
|
+
requirement: &70314214791100 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.6'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70314214791100
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: pry
|
49
|
+
requirement: &70314214790640 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.9'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70314214790640
|
36
58
|
- !ruby/object:Gem::Dependency
|
37
59
|
name: rake
|
38
|
-
requirement: &
|
60
|
+
requirement: &70314214790180 !ruby/object:Gem::Requirement
|
39
61
|
none: false
|
40
62
|
requirements:
|
41
63
|
- - ~>
|
@@ -43,22 +65,21 @@ dependencies:
|
|
43
65
|
version: '0.9'
|
44
66
|
type: :development
|
45
67
|
prerelease: false
|
46
|
-
version_requirements: *
|
68
|
+
version_requirements: *70314214790180
|
47
69
|
- !ruby/object:Gem::Dependency
|
48
70
|
name: rspec
|
49
|
-
requirement: &
|
71
|
+
requirement: &70314214789720 !ruby/object:Gem::Requirement
|
50
72
|
none: false
|
51
73
|
requirements:
|
52
74
|
- - ~>
|
53
75
|
- !ruby/object:Gem::Version
|
54
|
-
version: '2.
|
76
|
+
version: '2.8'
|
55
77
|
type: :development
|
56
78
|
prerelease: false
|
57
|
-
version_requirements: *
|
58
|
-
description:
|
59
|
-
in a Mongoid model.
|
79
|
+
version_requirements: *70314214789720
|
80
|
+
description: ! " a \n a ."
|
60
81
|
email:
|
61
|
-
-
|
82
|
+
- hakan.ensari@papercavalier.com
|
62
83
|
executables: []
|
63
84
|
extensions: []
|
64
85
|
extra_rdoc_files: []
|
@@ -68,10 +89,12 @@ files:
|
|
68
89
|
- lib/mongoid_slug.rb
|
69
90
|
- LICENSE
|
70
91
|
- README.md
|
92
|
+
- spec/models/alias.rb
|
71
93
|
- spec/models/article.rb
|
72
94
|
- spec/models/author.rb
|
73
95
|
- spec/models/book.rb
|
74
96
|
- spec/models/caption.rb
|
97
|
+
- spec/models/friend.rb
|
75
98
|
- spec/models/magazine.rb
|
76
99
|
- spec/models/page.rb
|
77
100
|
- spec/models/partner.rb
|
@@ -80,7 +103,7 @@ files:
|
|
80
103
|
- spec/models/subject.rb
|
81
104
|
- spec/mongoid/slug_spec.rb
|
82
105
|
- spec/spec_helper.rb
|
83
|
-
homepage: http://github.com/
|
106
|
+
homepage: http://github.com/hakanensari/mongoid-slug
|
84
107
|
licenses: []
|
85
108
|
post_install_message:
|
86
109
|
rdoc_options: []
|
@@ -103,12 +126,14 @@ rubyforge_project: mongoid_slug
|
|
103
126
|
rubygems_version: 1.8.11
|
104
127
|
signing_key:
|
105
128
|
specification_version: 3
|
106
|
-
summary: Generates a URL slug
|
129
|
+
summary: Generates a URL slug in a Mongoid model
|
107
130
|
test_files:
|
131
|
+
- spec/models/alias.rb
|
108
132
|
- spec/models/article.rb
|
109
133
|
- spec/models/author.rb
|
110
134
|
- spec/models/book.rb
|
111
135
|
- spec/models/caption.rb
|
136
|
+
- spec/models/friend.rb
|
112
137
|
- spec/models/magazine.rb
|
113
138
|
- spec/models/page.rb
|
114
139
|
- spec/models/partner.rb
|