mongoid_slug 1.0.1 → 2.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +34 -7
- data/lib/mongoid/slug.rb +13 -128
- data/lib/mongoid/slug/criteria.rb +25 -15
- data/lib/mongoid/slug/slug_id_strategy.rb +3 -0
- data/lib/mongoid/slug/unique_slug.rb +143 -0
- data/lib/mongoid/slug/version.rb +1 -1
- data/lib/mongoid_slug.rb +2 -0
- data/spec/models/entity.rb +12 -0
- data/spec/mongoid/slug_spec.rb +86 -30
- data/spec/spec_helper.rb +8 -0
- metadata +42 -6
data/README.md
CHANGED
@@ -43,22 +43,35 @@ book = Book.find params[:book_id]
|
|
43
43
|
|
44
44
|
Mongoid Slug will attempt to determine whether you want to find using the `slugs` field or the `_id` field by inspecting the supplied parameters.
|
45
45
|
|
46
|
-
*
|
46
|
+
* Mongoid Slug will perform a find based on `slugs` only if all arguments passed to `find` are of the type `String`
|
47
|
+
* If your document uses `BSON::ObjectId` identifiers, and all arguments look like valid `BSON::ObjectId`, then Mongoid Slug will perform a find based on `_id`.
|
47
48
|
* If your document uses any other type of identifiers, and all arguments passed to `find` are of the same type, then Mongoid Slug will perform a find based on `_id`.
|
48
|
-
*
|
49
|
+
* If your document uses `String` identifiers and you want to be able find by slugs or ids, to get the correct behaviour, you should add a slug_id_strategy option to your _id field definition. This option should return something that responds to `call` (a callable) and takes one string argument, e.g. a lambda. This callable must return true if the string looks like one of your ids.
|
49
50
|
|
50
|
-
To override this behaviour you may supply a hash of options as the final argument to `find` with the key `force_slugs` set to `true` or `false` as required. For example:
|
51
51
|
|
52
52
|
```ruby
|
53
53
|
Book.fields['_id'].type
|
54
54
|
=> String
|
55
|
-
book = Book.find 'a-thousand-plateaus' # Finds by
|
55
|
+
book = Book.find 'a-thousand-plateaus' # Finds by slugs
|
56
56
|
=> ...
|
57
|
-
book = Book.find 'a-thousand-plateaus', { force_slugs: true } # Finds by slugs
|
58
|
-
=> ...
|
59
|
-
```
|
60
57
|
|
58
|
+
class Post
|
59
|
+
include Mongoid::Document
|
60
|
+
include Mongoid::Slug
|
61
|
+
|
62
|
+
field :_id, type: String, slug_id_strategy: lambda {|id| id.start_with?('....')}
|
61
63
|
|
64
|
+
field :name
|
65
|
+
slug :name, :history => true
|
66
|
+
end
|
67
|
+
|
68
|
+
Post.fields['_id'].type
|
69
|
+
=> String
|
70
|
+
post = Post.find 'a-thousand-plateaus' # Finds by slugs
|
71
|
+
=> ...
|
72
|
+
post = Post.find '....1000/Plateus' # Finds by ids
|
73
|
+
=> ...
|
74
|
+
```
|
62
75
|
[Read here] [4] for all available options.
|
63
76
|
|
64
77
|
Custom Slug Generation
|
@@ -171,6 +184,20 @@ Friend.find('admin') # => nil
|
|
171
184
|
friend.slug # => 'admin-1'
|
172
185
|
```
|
173
186
|
|
187
|
+
Adhoc checking whether a string is unique on a per Model basis
|
188
|
+
--------------------------------------------------------------
|
189
|
+
|
190
|
+
Lets say you want to have a auto-suggest function on your GUI that could provide a preview of what the url or slug could be before the form to create the record was submitted.
|
191
|
+
|
192
|
+
You can use the UniqueSlug class in your server side code to do this, e.g.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
title = params[:title]
|
196
|
+
unique = Mongoid::Slug::UniqueSlug.new(Book.new).find_unique(title)
|
197
|
+
...
|
198
|
+
# return some representation of unique
|
199
|
+
```
|
200
|
+
|
174
201
|
[1]: https://github.com/rsl/stringex/
|
175
202
|
[2]: https://secure.travis-ci.org/hakanensari/mongoid-slug.png
|
176
203
|
[3]: http://travis-ci.org/hakanensari/mongoid-slug
|
data/lib/mongoid/slug.rb
CHANGED
@@ -15,8 +15,6 @@ module Mongoid
|
|
15
15
|
end
|
16
16
|
|
17
17
|
module ClassMethods
|
18
|
-
|
19
|
-
|
20
18
|
# @overload slug(*fields)
|
21
19
|
# Sets one ore more fields as source of slug.
|
22
20
|
# @param [Array] fields One or more fields the slug should be based on.
|
@@ -51,10 +49,10 @@ module Mongoid
|
|
51
49
|
def slug(*fields, &block)
|
52
50
|
options = fields.extract_options!
|
53
51
|
|
54
|
-
self.slug_scope
|
55
|
-
self.reserved_words
|
56
|
-
self.slugged_attributes
|
57
|
-
self.history
|
52
|
+
self.slug_scope = options[:scope]
|
53
|
+
self.reserved_words = options[:reserve] || Set.new([:new, :edit])
|
54
|
+
self.slugged_attributes = fields.map &:to_s
|
55
|
+
self.history = options[:history]
|
58
56
|
|
59
57
|
unless embedded?
|
60
58
|
if slug_scope
|
@@ -79,16 +77,12 @@ module Mongoid
|
|
79
77
|
else
|
80
78
|
set_callback :save, :before, :build_slug, :if => :slug_should_be_rebuilt?
|
81
79
|
end
|
82
|
-
|
83
|
-
|
84
80
|
end
|
85
81
|
|
86
82
|
def look_like_slugs?(*args)
|
87
83
|
with_default_scope.look_like_slugs?(*args)
|
88
84
|
end
|
89
85
|
|
90
|
-
|
91
|
-
|
92
86
|
# Find documents by slugs.
|
93
87
|
#
|
94
88
|
# A document matches if any of its slugs match one of the supplied params.
|
@@ -122,7 +116,7 @@ module Mongoid
|
|
122
116
|
def build_slug
|
123
117
|
_new_slug = find_unique_slug
|
124
118
|
self._slugs.delete(_new_slug)
|
125
|
-
if self.history
|
119
|
+
if !!self.history
|
126
120
|
self._slugs << _new_slug
|
127
121
|
else
|
128
122
|
self._slugs = [_new_slug]
|
@@ -130,89 +124,16 @@ module Mongoid
|
|
130
124
|
true
|
131
125
|
end
|
132
126
|
|
133
|
-
|
134
127
|
# Finds a unique slug, were specified string used to generate a slug.
|
135
128
|
#
|
136
129
|
# Returned slug will the same as the specified string when there are no
|
137
130
|
# duplicates.
|
138
131
|
#
|
139
|
-
# @param [String] Desired slug
|
140
132
|
# @return [String] A unique slug
|
141
133
|
def find_unique_slug
|
142
|
-
|
143
|
-
_slug = self.url_builder.call(self)
|
144
|
-
|
145
|
-
# Regular expression that matches slug, slug-1, ... slug-n
|
146
|
-
# If slug_name field was indexed, MongoDB will utilize that
|
147
|
-
# index to match /^.../ pattern.
|
148
|
-
pattern = /^#{Regexp.escape(_slug)}(?:-(\d+))?$/
|
149
|
-
|
150
|
-
where_hash = {}
|
151
|
-
where_hash[:_slugs.all] = [pattern]
|
152
|
-
where_hash[:_id.ne] = self._id
|
153
|
-
|
154
|
-
if slug_scope && self.reflect_on_association(slug_scope).nil?
|
155
|
-
# scope is not an association, so it's scoped to a local field
|
156
|
-
# (e.g. an association id in a denormalized db design)
|
157
|
-
where_hash[slug_scope] = self.try(:read_attribute, slug_scope)
|
158
|
-
|
159
|
-
end
|
160
|
-
|
161
|
-
history_slugged_documents =
|
162
|
-
uniqueness_scope.
|
163
|
-
where(where_hash)
|
164
|
-
|
165
|
-
existing_slugs = []
|
166
|
-
existing_history_slugs = []
|
167
|
-
last_entered_slug = []
|
168
|
-
history_slugged_documents.each do |doc|
|
169
|
-
history_slugs = doc._slugs
|
170
|
-
next if history_slugs.nil?
|
171
|
-
existing_slugs.push(*history_slugs.find_all { |cur_slug| cur_slug =~ pattern })
|
172
|
-
last_entered_slug.push(*history_slugs.last) if history_slugs.last =~ pattern
|
173
|
-
existing_history_slugs.push(*history_slugs.first(history_slugs.length() -1).find_all { |cur_slug| cur_slug =~ pattern })
|
174
|
-
end
|
175
|
-
|
176
|
-
#do not allow a slug that can be interpreted as the current document id
|
177
|
-
existing_slugs << _slug unless self.class.look_like_slugs?([_slug])
|
178
|
-
|
179
|
-
#make sure that the slug is not equal to a reserved word
|
180
|
-
if reserved_words.any? { |word| word === _slug }
|
181
|
-
existing_slugs << _slug
|
182
|
-
end
|
183
|
-
|
184
|
-
#only look for a new unique slug if the existing slugs contains the current slug
|
185
|
-
# - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
|
186
|
-
if existing_slugs.include? _slug
|
187
|
-
# If the only conflict is in the history of a document in the same scope,
|
188
|
-
# transfer the slug
|
189
|
-
if slug_scope && last_entered_slug.count == 0 && existing_history_slugs.count > 0
|
190
|
-
history_slugged_documents.each do |doc|
|
191
|
-
doc._slugs -= existing_history_slugs
|
192
|
-
doc.save
|
193
|
-
end
|
194
|
-
existing_slugs = []
|
195
|
-
end
|
196
|
-
|
197
|
-
if existing_slugs.count > 0
|
198
|
-
# Sort the existing_slugs in increasing order by comparing the
|
199
|
-
# suffix numbers:
|
200
|
-
# slug, slug-1, slug-2, ..., slug-n
|
201
|
-
existing_slugs.sort! do |a, b|
|
202
|
-
(pattern.match(a)[1] || -1).to_i <=>
|
203
|
-
(pattern.match(b)[1] || -1).to_i
|
204
|
-
end
|
205
|
-
max = existing_slugs.last.match(/-(\d+)$/).try(:[], 1).to_i
|
206
|
-
|
207
|
-
_slug += "-#{max + 1}"
|
208
|
-
end
|
209
|
-
|
210
|
-
end
|
211
|
-
|
212
|
-
_slug
|
134
|
+
UniqueSlug.new(self).find_unique
|
213
135
|
end
|
214
136
|
|
215
|
-
|
216
137
|
# @return [Boolean] Whether the slug requires to be rebuilt
|
217
138
|
def slug_should_be_rebuilt?
|
218
139
|
new_record? or _slugs_changed? or slugged_attributes_changed?
|
@@ -225,65 +146,29 @@ module Mongoid
|
|
225
146
|
# @return [String] A string which Action Pack uses for constructing an URL
|
226
147
|
# to this record.
|
227
148
|
def to_param
|
228
|
-
|
229
|
-
|
230
|
-
save
|
231
|
-
end
|
149
|
+
slug || super
|
150
|
+
end
|
232
151
|
|
152
|
+
# @return [String] the slug, or nil if the document does not have a slug.
|
153
|
+
def slug
|
233
154
|
_slugs.last
|
234
155
|
end
|
235
|
-
alias_method :slug, :to_param
|
236
156
|
|
237
157
|
def slug_builder
|
238
|
-
|
239
158
|
_cur_slug = nil
|
240
159
|
if (new_record? and _slugs.present?) or (persisted? and _slugs_changed?)
|
241
160
|
#user defined slug
|
242
161
|
_cur_slug = _slugs.last
|
243
162
|
end
|
244
|
-
|
245
163
|
#generate slug if the slug is not user defined or does not exist
|
246
|
-
|
247
|
-
self.slugged_attributes.map { |f| self.send f }.join ' '
|
248
|
-
else
|
249
|
-
_cur_slug
|
250
|
-
end
|
164
|
+
_cur_slug || pre_slug_string
|
251
165
|
end
|
252
166
|
|
253
167
|
private
|
254
168
|
|
255
|
-
def
|
256
|
-
|
257
|
-
if slug_scope &&
|
258
|
-
metadata = self.reflect_on_association(slug_scope)
|
259
|
-
|
260
|
-
parent = self.send(metadata.name)
|
261
|
-
|
262
|
-
# Make sure doc is actually associated with something, and that
|
263
|
-
# some referenced docs have been persisted to the parent
|
264
|
-
#
|
265
|
-
# TODO: we need better reflection for reference associations,
|
266
|
-
# like association_name instead of forcing collection_name here
|
267
|
-
# -- maybe in the forthcoming Mongoid refactorings?
|
268
|
-
inverse = metadata.inverse_of || collection_name
|
269
|
-
return parent.respond_to?(inverse) ? parent.send(inverse) : self.class
|
270
|
-
|
271
|
-
end
|
272
|
-
|
273
|
-
if self.embedded?
|
274
|
-
parent_metadata = reflect_on_all_associations(:embedded_in)[0]
|
275
|
-
return self._parent.send(parent_metadata.inverse_of || self.metadata.name)
|
276
|
-
end
|
277
|
-
|
278
|
-
#unless embedded or slug scope, return the deepest document superclass
|
279
|
-
appropriate_class = self.class
|
280
|
-
while appropriate_class.superclass.include?(Mongoid::Document)
|
281
|
-
appropriate_class = appropriate_class.superclass
|
282
|
-
end
|
283
|
-
appropriate_class
|
284
|
-
|
169
|
+
def pre_slug_string
|
170
|
+
self.slugged_attributes.map { |f| self.send f }.join ' '
|
285
171
|
end
|
286
|
-
|
287
172
|
end
|
288
173
|
end
|
289
174
|
|
@@ -50,24 +50,34 @@ module Mongoid
|
|
50
50
|
for_slugs(slugs).execute_or_raise_for_slugs(slugs, args.multi_arged?)
|
51
51
|
end
|
52
52
|
|
53
|
-
|
54
|
-
# True if all supplied args look like slugs. Will only attempt to type cast for Moped::BSON::ObjectId.
|
55
|
-
# Thus '123' will be interpreted as a slug even if the _id is an Integer field, etc.
|
56
53
|
def look_like_slugs?(args)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
args.any? { |id| !Moped::BSON::ObjectId.legal?(id) }
|
62
|
-
else args.any? { |id| id.class != id_type }
|
63
|
-
end
|
64
|
-
else
|
65
|
-
false
|
66
|
-
end
|
54
|
+
return false unless args.all? { |id| id.is_a?(String) }
|
55
|
+
id_field = @klass.fields['_id']
|
56
|
+
@slug_strategy ||= id_field.options[:slug_id_strategy] || build_slug_strategy(id_field.type)
|
57
|
+
args.none? { |id| @slug_strategy.call(id) }
|
67
58
|
end
|
68
|
-
|
59
|
+
|
69
60
|
protected
|
70
61
|
|
62
|
+
# unless a :slug_id_strategy option is defined on the id field,
|
63
|
+
# use object_id or string strategy depending on the id_type
|
64
|
+
# otherwise default for all other id_types
|
65
|
+
def build_slug_strategy id_type
|
66
|
+
type_method = id_type.to_s.downcase.split('::').last + "_slug_strategy"
|
67
|
+
self.respond_to?(type_method) ? method(type_method) : lambda {|id| false}
|
68
|
+
end
|
69
|
+
|
70
|
+
# a string will not look like a slug if it looks like a legal ObjectId
|
71
|
+
def objectid_slug_strategy id
|
72
|
+
Moped::BSON::ObjectId.legal?(id)
|
73
|
+
end
|
74
|
+
|
75
|
+
# a string will always look like a slug
|
76
|
+
def string_slug_strategy id
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
|
71
81
|
def for_slugs(slugs)
|
72
82
|
where({ _slugs: { '$in' => slugs } }).limit(slugs.length)
|
73
83
|
end
|
@@ -87,4 +97,4 @@ module Mongoid
|
|
87
97
|
end
|
88
98
|
end
|
89
99
|
end
|
90
|
-
end
|
100
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
# Can use e.g. Mongoid::Slug::UniqueSlug.new(ModelClass.new).find_unique "slug-1" for auto-suggest ui
|
4
|
+
module Mongoid
|
5
|
+
module Slug
|
6
|
+
class UniqueSlug
|
7
|
+
|
8
|
+
class SlugState
|
9
|
+
attr_reader :last_entered_slug, :existing_slugs, :existing_history_slugs, :sorted_existing
|
10
|
+
|
11
|
+
def initialize slug, documents, pattern
|
12
|
+
@slug = slug
|
13
|
+
@documents = documents
|
14
|
+
@pattern = pattern
|
15
|
+
@last_entered_slug = []
|
16
|
+
@existing_slugs = []
|
17
|
+
@existing_history_slugs = []
|
18
|
+
@sorted_existing = []
|
19
|
+
@documents.each do |doc|
|
20
|
+
history_slugs = doc._slugs
|
21
|
+
next if history_slugs.nil?
|
22
|
+
existing_slugs.push(*history_slugs.find_all { |cur_slug| cur_slug =~ @pattern })
|
23
|
+
last_entered_slug.push(*history_slugs.last) if history_slugs.last =~ @pattern
|
24
|
+
existing_history_slugs.push(*history_slugs.first(history_slugs.length() - 1).find_all { |cur_slug| cur_slug =~ @pattern })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def slug_included?
|
29
|
+
existing_slugs.include? @slug
|
30
|
+
end
|
31
|
+
|
32
|
+
def include_slug
|
33
|
+
existing_slugs << @slug
|
34
|
+
end
|
35
|
+
|
36
|
+
def highest_existing_counter
|
37
|
+
sort_existing_slugs
|
38
|
+
@sorted_existing.last || 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def sort_existing_slugs
|
42
|
+
# remove the slug part and leave the absolute integer part and sort
|
43
|
+
re = %r(^#{Regexp.escape(@slug)})
|
44
|
+
@sorted_existing = existing_slugs.map do |s|
|
45
|
+
s.sub(re,'').to_i.abs
|
46
|
+
end.sort
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
{
|
51
|
+
:slug => @slug,
|
52
|
+
:existing_slugs => existing_slugs,
|
53
|
+
:last_entered_slug => last_entered_slug,
|
54
|
+
:existing_history_slugs => existing_history_slugs,
|
55
|
+
:sorted_existing => sorted_existing
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
extend Forwardable
|
61
|
+
|
62
|
+
attr_reader :model, :_slug
|
63
|
+
|
64
|
+
def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
|
65
|
+
:check_against_id, :reserved_words, :url_builder, :metadata,
|
66
|
+
:collection_name, :embedded?, :reflect_on_all_associations
|
67
|
+
|
68
|
+
def initialize model
|
69
|
+
@model = model
|
70
|
+
@_slug = ""
|
71
|
+
@state = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_unique attempt = nil
|
75
|
+
@_slug = if attempt
|
76
|
+
attempt.to_url
|
77
|
+
else
|
78
|
+
url_builder.call(model)
|
79
|
+
end
|
80
|
+
# Regular expression that matches slug, slug-1, ... slug-n
|
81
|
+
# If slug_name field was indexed, MongoDB will utilize that
|
82
|
+
# index to match /^.../ pattern.
|
83
|
+
pattern = /^#{Regexp.escape(_slug)}(?:-(\d+))?$/
|
84
|
+
|
85
|
+
where_hash = {}
|
86
|
+
where_hash[:_slugs.all] = [pattern]
|
87
|
+
where_hash[:_id.ne] = model._id
|
88
|
+
|
89
|
+
if (scope = slug_scope) && reflect_on_association(scope).nil?
|
90
|
+
# scope is not an association, so it's scoped to a local field
|
91
|
+
# (e.g. an association id in a denormalized db design)
|
92
|
+
where_hash[scope] = model.try(:read_attribute, scope)
|
93
|
+
end
|
94
|
+
|
95
|
+
@state = SlugState.new _slug, uniqueness_scope.where(where_hash), pattern
|
96
|
+
|
97
|
+
# do not allow a slug that can be interpreted as the current document id
|
98
|
+
@state.include_slug unless model.class.look_like_slugs?([_slug])
|
99
|
+
|
100
|
+
# make sure that the slug is not equal to a reserved word
|
101
|
+
@state.include_slug if reserved_words.any? { |word| word === _slug }
|
102
|
+
|
103
|
+
# only look for a new unique slug if the existing slugs contains the current slug
|
104
|
+
# - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
|
105
|
+
if @state.slug_included?
|
106
|
+
highest = @state.highest_existing_counter
|
107
|
+
@_slug += "-#{highest.succ}"
|
108
|
+
end
|
109
|
+
_slug
|
110
|
+
end
|
111
|
+
|
112
|
+
def uniqueness_scope
|
113
|
+
|
114
|
+
if slug_scope &&
|
115
|
+
metadata = reflect_on_association(slug_scope)
|
116
|
+
|
117
|
+
parent = model.send(metadata.name)
|
118
|
+
|
119
|
+
# Make sure doc is actually associated with something, and that
|
120
|
+
# some referenced docs have been persisted to the parent
|
121
|
+
#
|
122
|
+
# TODO: we need better reflection for reference associations,
|
123
|
+
# like association_name instead of forcing collection_name here
|
124
|
+
# -- maybe in the forthcoming Mongoid refactorings?
|
125
|
+
inverse = metadata.inverse_of || collection_name
|
126
|
+
return parent.respond_to?(inverse) ? parent.send(inverse) : model.class
|
127
|
+
end
|
128
|
+
|
129
|
+
if embedded?
|
130
|
+
parent_metadata = reflect_on_all_associations(:embedded_in)[0]
|
131
|
+
return model._parent.send(parent_metadata.inverse_of || model.metadata.name)
|
132
|
+
end
|
133
|
+
|
134
|
+
#unless embedded or slug scope, return the deepest document superclass
|
135
|
+
appropriate_class = model.class
|
136
|
+
while appropriate_class.superclass.include?(Mongoid::Document)
|
137
|
+
appropriate_class = appropriate_class.superclass
|
138
|
+
end
|
139
|
+
appropriate_class
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/mongoid/slug/version.rb
CHANGED
data/lib/mongoid_slug.rb
CHANGED
data/spec/mongoid/slug_spec.rb
CHANGED
@@ -7,7 +7,58 @@ module Mongoid
|
|
7
7
|
Book.create(:title => "A Thousand Plateaus")
|
8
8
|
end
|
9
9
|
|
10
|
+
context "when option skip_id_check is used with UUID _id " do
|
11
|
+
let(:entity0) do
|
12
|
+
Entity.create(:_id => UUID.generate, :name => 'Pelham 1 2 3', :user_edited_variation => 'pelham-1-2-3')
|
13
|
+
end
|
14
|
+
let(:entity1) do
|
15
|
+
Entity.create(:_id => UUID.generate, :name => 'Jackson 5', :user_edited_variation => 'jackson-5')
|
16
|
+
end
|
17
|
+
let(:entity2) do
|
18
|
+
Entity.create(:_id => UUID.generate, :name => 'Jackson 5', :user_edited_variation => 'jackson-5')
|
19
|
+
end
|
20
|
+
|
21
|
+
it "generates a unique slug by appending a counter to duplicate text" do
|
22
|
+
entity0.to_param.should eql "pelham-1-2-3"
|
23
|
+
|
24
|
+
5.times{ |x|
|
25
|
+
dup = Entity.create(:_id => UUID.generate, :name => entity0.name, :user_edited_variation => entity0.user_edited_variation)
|
26
|
+
dup.to_param.should eql "pelham-1-2-3-#{x.succ}"
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "allows the user to edit the sluggable field" do
|
31
|
+
entity1.to_param.should eql "jackson-5"
|
32
|
+
entity2.to_param.should eql "jackson-5-1"
|
33
|
+
entity2.user_edited_variation = "jackson-5-indiana"
|
34
|
+
entity2.save
|
35
|
+
entity2.to_param.should eql "jackson-5-indiana"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "allows users to edit the sluggable field" do
|
39
|
+
entity1.to_param.should eql "jackson-5"
|
40
|
+
entity2.to_param.should eql "jackson-5-1"
|
41
|
+
entity2.user_edited_variation = "jackson-5-indiana"
|
42
|
+
entity2.save
|
43
|
+
entity2.to_param.should eql "jackson-5-indiana"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "it restores the slug if the editing user tries to use an existing slug" do
|
47
|
+
entity1.to_param.should eql "jackson-5"
|
48
|
+
entity2.to_param.should eql "jackson-5-1"
|
49
|
+
entity2.user_edited_variation = "jackson-5"
|
50
|
+
entity2.save
|
51
|
+
entity2.to_param.should eql "jackson-5-1"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not force an appended counter on a plain string" do
|
55
|
+
entity = Entity.create(:_id => UUID.generate, :name => 'Adele', :user_edited_variation => 'adele')
|
56
|
+
entity.to_param.should eql "adele"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
10
60
|
context "when the object is top-level" do
|
61
|
+
|
11
62
|
it "generates a slug" do
|
12
63
|
book.to_param.should eql "a-thousand-plateaus"
|
13
64
|
end
|
@@ -26,8 +77,9 @@ module Mongoid
|
|
26
77
|
end
|
27
78
|
|
28
79
|
it "does not allow a Moped::BSON::ObjectId as use for a slug" do
|
29
|
-
|
30
|
-
bad.
|
80
|
+
bson_id = Moped::BSON::ObjectId.new.to_s
|
81
|
+
bad = Book.create(:title => bson_id)
|
82
|
+
bad.slugs.should_not include(bson_id)
|
31
83
|
end
|
32
84
|
|
33
85
|
it "does not update slug if slugged fields have not changed" do
|
@@ -352,30 +404,6 @@ module Mongoid
|
|
352
404
|
dup.to_param.should eql character.to_param
|
353
405
|
end
|
354
406
|
end
|
355
|
-
|
356
|
-
context "when using history and reusing a slug within the scope" do
|
357
|
-
let!(:subject1) do
|
358
|
-
book.subjects.create(:name => "A Subject")
|
359
|
-
end
|
360
|
-
let!(:subject2) do
|
361
|
-
book.subjects.create(:name => "Another Subject")
|
362
|
-
end
|
363
|
-
|
364
|
-
before(:each) do
|
365
|
-
subject1.name = "Something Else Entirely"
|
366
|
-
subject1.save
|
367
|
-
subject2.name = "A Subject"
|
368
|
-
subject2.save
|
369
|
-
end
|
370
|
-
|
371
|
-
it "allows using the slug" do
|
372
|
-
subject2.slugs.should include("a-subject")
|
373
|
-
end
|
374
|
-
|
375
|
-
it "removes the slug from the old owner's history" do
|
376
|
-
subject1.slugs.should_not include("a-subject")
|
377
|
-
end
|
378
|
-
end
|
379
407
|
end
|
380
408
|
|
381
409
|
context "when slug is scoped by one of the class's own fields" do
|
@@ -530,7 +558,7 @@ module Mongoid
|
|
530
558
|
let!(:subject2) { Subject.create(:title => "A Subject", :book => book2) }
|
531
559
|
let!(:without_slug) { WithoutSlug.new().tap { |d| d.id = 456; d.save } }
|
532
560
|
|
533
|
-
context "when the model does not use mongoid slugs" do
|
561
|
+
context "when the model does not use mongoid slugs" do
|
534
562
|
it "should not use mongoid slug's custom find methods" do
|
535
563
|
Mongoid::Slug::Criteria.any_instance.should_not_receive(:find)
|
536
564
|
WithoutSlug.find(without_slug.id.to_s).should == without_slug
|
@@ -704,15 +732,28 @@ module Mongoid
|
|
704
732
|
end
|
705
733
|
|
706
734
|
describe "#to_param" do
|
735
|
+
context "when called on a new record" do
|
736
|
+
let(:book) { Book.new }
|
737
|
+
|
738
|
+
it "should return nil" do
|
739
|
+
book.to_param.should be_nil
|
740
|
+
end
|
741
|
+
|
742
|
+
it "should not persist the record" do
|
743
|
+
book.to_param
|
744
|
+
book.should_not be_persisted
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
707
748
|
context "when called on an existing record with no slug" do
|
708
749
|
before do
|
709
750
|
Book.collection.insert(:title => "Proust and Signs")
|
710
751
|
end
|
711
752
|
|
712
|
-
it "
|
753
|
+
it "should return the id" do
|
713
754
|
book = Book.first
|
714
|
-
book.to_param
|
715
|
-
book.reload.slugs.should
|
755
|
+
book.to_param.should == book.id.to_s
|
756
|
+
book.reload.slugs.should be_empty
|
716
757
|
end
|
717
758
|
end
|
718
759
|
end
|
@@ -767,6 +808,21 @@ module Mongoid
|
|
767
808
|
book.save
|
768
809
|
book.to_param.should eql "not-it-either"
|
769
810
|
end
|
811
|
+
|
812
|
+
it "updates the slug when a new one is appended" do
|
813
|
+
book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
|
814
|
+
book.slugs.push "not-it-either"
|
815
|
+
book.save
|
816
|
+
book.to_param.should eql "not-it-either"
|
817
|
+
end
|
818
|
+
|
819
|
+
it "updates the slug to a unique slug when a new one is appended" do
|
820
|
+
book1 = Book.create(:title => "Sleepyhead")
|
821
|
+
book2 = Book.create(:title => "A Thousand Plateaus")
|
822
|
+
book2.slugs.push "sleepyhead"
|
823
|
+
book2.save
|
824
|
+
book2.to_param.should eql "sleepyhead-1"
|
825
|
+
end
|
770
826
|
end
|
771
827
|
|
772
828
|
context "when it is set to an empty string" do
|
data/spec/spec_helper.rb
CHANGED
@@ -3,9 +3,17 @@ begin
|
|
3
3
|
rescue LoadError
|
4
4
|
end
|
5
5
|
require 'rspec'
|
6
|
+
require 'uuid'
|
7
|
+
require "awesome_print"
|
6
8
|
|
7
9
|
require File.expand_path '../../lib/mongoid_slug', __FILE__
|
8
10
|
|
11
|
+
module Mongoid::Slug::UuidIdStrategy
|
12
|
+
def self.call id
|
13
|
+
id =~ /\A([0-9a-fA-F]){8}-(([0-9a-fA-F]){4}-){3}([0-9a-fA-F]){12}\z/
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
9
17
|
def database_id
|
10
18
|
ENV['CI'] ? "mongoid_slug_#{Process.pid}" : 'mongoid_slug_test'
|
11
19
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid_slug
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease:
|
4
|
+
version: 2.0.0.pre
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Andreas Saebjoernsen
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mongoid
|
@@ -91,6 +91,38 @@ dependencies:
|
|
91
91
|
- - ! '>='
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: awesome_print
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: uuid
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
94
126
|
description: Mongoid URL slug or permalink generator
|
95
127
|
email:
|
96
128
|
- andy@cosemble.com
|
@@ -99,6 +131,8 @@ extensions: []
|
|
99
131
|
extra_rdoc_files: []
|
100
132
|
files:
|
101
133
|
- lib/mongoid/slug/criteria.rb
|
134
|
+
- lib/mongoid/slug/slug_id_strategy.rb
|
135
|
+
- lib/mongoid/slug/unique_slug.rb
|
102
136
|
- lib/mongoid/slug/version.rb
|
103
137
|
- lib/mongoid/slug.rb
|
104
138
|
- lib/mongoid_slug.rb
|
@@ -109,6 +143,7 @@ files:
|
|
109
143
|
- spec/models/author.rb
|
110
144
|
- spec/models/book.rb
|
111
145
|
- spec/models/caption.rb
|
146
|
+
- spec/models/entity.rb
|
112
147
|
- spec/models/friend.rb
|
113
148
|
- spec/models/integer_id.rb
|
114
149
|
- spec/models/magazine.rb
|
@@ -121,7 +156,7 @@ files:
|
|
121
156
|
- spec/models/without_slug.rb
|
122
157
|
- spec/mongoid/slug_spec.rb
|
123
158
|
- spec/spec_helper.rb
|
124
|
-
homepage: http://github.com/
|
159
|
+
homepage: http://github.com/musicglue/mongoid-slug
|
125
160
|
licenses: []
|
126
161
|
post_install_message:
|
127
162
|
rdoc_options: []
|
@@ -136,9 +171,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
171
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
172
|
none: false
|
138
173
|
requirements:
|
139
|
-
- - ! '
|
174
|
+
- - ! '>'
|
140
175
|
- !ruby/object:Gem::Version
|
141
|
-
version:
|
176
|
+
version: 1.3.1
|
142
177
|
requirements: []
|
143
178
|
rubyforge_project: mongoid_slug
|
144
179
|
rubygems_version: 1.8.24
|
@@ -151,6 +186,7 @@ test_files:
|
|
151
186
|
- spec/models/author.rb
|
152
187
|
- spec/models/book.rb
|
153
188
|
- spec/models/caption.rb
|
189
|
+
- spec/models/entity.rb
|
154
190
|
- spec/models/friend.rb
|
155
191
|
- spec/models/integer_id.rb
|
156
192
|
- spec/models/magazine.rb
|