mongoid_slug 1.0.1 → 2.0.0.pre
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.
- 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
|