friendly_id 4.0.0.beta14 → 4.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -2
- data/Guide.rdoc +466 -0
- data/README.md +3 -0
- data/Rakefile +24 -1
- data/WhatsNew.md +1 -1
- data/lib/friendly_id.rb +14 -50
- data/lib/friendly_id/base.rb +76 -1
- data/lib/friendly_id/history.rb +9 -5
- data/lib/friendly_id/i18n.rb +11 -7
- data/lib/friendly_id/reserved.rb +8 -4
- data/lib/friendly_id/scoped.rb +5 -3
- data/lib/friendly_id/slug_generator.rb +2 -1
- data/lib/friendly_id/slugged.rb +70 -23
- data/test/slugged_test.rb +33 -2
- metadata +25 -24
- data/lib/friendly_id/model.rb +0 -26
data/.yardopts
CHANGED
data/Guide.rdoc
ADDED
@@ -0,0 +1,466 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
== About FriendlyId
|
5
|
+
|
6
|
+
FriendlyId is an add-on to Ruby's Active Record that allows you to replace ids
|
7
|
+
in your URLs with strings:
|
8
|
+
|
9
|
+
# without FriendlyId
|
10
|
+
http://example.com/states/4323454
|
11
|
+
|
12
|
+
# with FriendlyId
|
13
|
+
http://example.com/states/washington
|
14
|
+
|
15
|
+
It requires few changes to your application code and offers flexibility,
|
16
|
+
performance and a well-documented codebase.
|
17
|
+
|
18
|
+
=== Core Concepts
|
19
|
+
|
20
|
+
==== Slugs
|
21
|
+
|
22
|
+
The concept of "slugs[http://en.wikipedia.org/wiki/Slug_(web_publishing)]" is at
|
23
|
+
the heart of FriendlyId.
|
24
|
+
|
25
|
+
A slug is the part of a URL which identifies a page using human-readable
|
26
|
+
keywords, rather than an opaque identifier such as a numeric id. This can make
|
27
|
+
your application more friendly both for users and search engine.
|
28
|
+
|
29
|
+
==== Finders: Slugs Act Like Numeric IDs
|
30
|
+
|
31
|
+
To the extent possible, FriendlyId lets you treat text-based identifiers like
|
32
|
+
normal IDs. This means that you can perform finds with slugs just like you do
|
33
|
+
with numeric ids:
|
34
|
+
|
35
|
+
Person.find(82542335)
|
36
|
+
Person.find("joe")
|
37
|
+
|
38
|
+
|
39
|
+
== Setting Up FriendlyId in Your Model
|
40
|
+
|
41
|
+
To use FriendlyId in your ActiveRecord models, you must first extend the
|
42
|
+
FriendlyId module, then invoke the {FriendlyId::Base#friendly_id friendly_id}
|
43
|
+
method to configure your desired options:
|
44
|
+
|
45
|
+
class Foo < ActiveRecord::Base
|
46
|
+
extend FriendlyId
|
47
|
+
friendly_id bar, :use => [:slugged, :i18n]
|
48
|
+
end
|
49
|
+
|
50
|
+
The most important option is `:use`, which you use to tell FriendlyId which
|
51
|
+
addons it should use. See the documentation for this method for a list of all
|
52
|
+
available addons, or skim through the rest of the docs to get a high-level
|
53
|
+
overview.
|
54
|
+
|
55
|
+
=== The Default Setup: Simple Models
|
56
|
+
|
57
|
+
The simplest way to use FriendlyId is with a model that has a uniquely indexed
|
58
|
+
column with no spaces or special characters, and that is seldom or never
|
59
|
+
updated. The most common example of this is a user name:
|
60
|
+
|
61
|
+
class User < ActiveRecord::Base
|
62
|
+
extend FriendlyId
|
63
|
+
friendly_id :login
|
64
|
+
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
65
|
+
end
|
66
|
+
|
67
|
+
@user = User.find "joe" # the old User.find(1) still works, too
|
68
|
+
@user.to_param # returns "joe"
|
69
|
+
redirect_to @user # the URL will be /users/joe
|
70
|
+
|
71
|
+
In this case, FriendlyId assumes you want to use the column as-is; it will never
|
72
|
+
modify the value of the column, and your application should ensure that the
|
73
|
+
value is unique and admissible in a URL:
|
74
|
+
|
75
|
+
class City < ActiveRecord::Base
|
76
|
+
extend FriendlyId
|
77
|
+
friendly_id :name
|
78
|
+
end
|
79
|
+
|
80
|
+
@city.find "Viña del Mar"
|
81
|
+
redirect_to @city # the URL will be /cities/Viña%20del%20Mar
|
82
|
+
|
83
|
+
Writing the code to process an arbitrary string into a good identifier for use
|
84
|
+
in a URL can be repetitive and surprisingly tricky, so for this reason it's
|
85
|
+
often better and easier to use {FriendlyId::Slugged slugs}.
|
86
|
+
|
87
|
+
|
88
|
+
== Slugged Models
|
89
|
+
|
90
|
+
FriendlyId can use a separate column to store slugs for models which require
|
91
|
+
some text processing.
|
92
|
+
|
93
|
+
For example, blog applications typically use a post title to provide the basis
|
94
|
+
of a search engine friendly URL. Such identifiers typically lack uppercase
|
95
|
+
characters, use ASCII to approximate UTF-8 character, and strip out other
|
96
|
+
characters which may make them aesthetically unappealing or error-prone when
|
97
|
+
used in a URL.
|
98
|
+
|
99
|
+
class Post < ActiveRecord::Base
|
100
|
+
extend FriendlyId
|
101
|
+
friendly_id :title, :use => :slugged
|
102
|
+
end
|
103
|
+
|
104
|
+
@post = Post.create(:title => "This is the first post!")
|
105
|
+
@post.friendly_id # returns "this-is-the-first-post"
|
106
|
+
redirect_to @post # the URL will be /posts/this-is-the-first-post
|
107
|
+
|
108
|
+
In general, use slugs by default unless you know for sure you don't need them.
|
109
|
+
To activate the slugging functionality, use the {FriendlyId::Slugged} module.
|
110
|
+
|
111
|
+
FriendlyId will generate slugs from a method or column that you specify, and
|
112
|
+
store them in a field in your model. By default, this field must be named
|
113
|
+
+:slug+, though you may change this using the
|
114
|
+
{FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
|
115
|
+
option. You should add an index to this column, and in most cases, make it
|
116
|
+
unique. You may also wish to constrain it to NOT NULL, but this depends on your
|
117
|
+
app's behavior and requirements.
|
118
|
+
|
119
|
+
=== Example Setup
|
120
|
+
|
121
|
+
# your model
|
122
|
+
class Post < ActiveRecord::Base
|
123
|
+
extend FriendlyId
|
124
|
+
friendly_id :title, :use => :slugged
|
125
|
+
validates_presence_of :title, :slug, :body
|
126
|
+
end
|
127
|
+
|
128
|
+
# a migration
|
129
|
+
class CreatePosts < ActiveRecord::Migration
|
130
|
+
def self.up
|
131
|
+
create_table :posts do |t|
|
132
|
+
t.string :title, :null => false
|
133
|
+
t.string :slug, :null => false
|
134
|
+
t.text :body
|
135
|
+
end
|
136
|
+
|
137
|
+
add_index :posts, :slug, :unique => true
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.down
|
141
|
+
drop_table :posts
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
=== Working With Slugs
|
146
|
+
|
147
|
+
==== Formatting
|
148
|
+
|
149
|
+
By default, FriendlyId uses Active Support's
|
150
|
+
paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
|
151
|
+
method to create slugs. This method will intelligently replace spaces with
|
152
|
+
dashes, and Unicode Latin characters with ASCII approximations:
|
153
|
+
|
154
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
155
|
+
movie.slug #=> "der-preis-furs-uberleben"
|
156
|
+
|
157
|
+
==== Uniqueness
|
158
|
+
|
159
|
+
When you try to insert a record that would generate a duplicate friendly id,
|
160
|
+
FriendlyId will append a sequence to the generated slug to ensure uniqueness:
|
161
|
+
|
162
|
+
car = Car.create :title => "Peugot 206"
|
163
|
+
car2 = Car.create :title => "Peugot 206"
|
164
|
+
|
165
|
+
car.friendly_id #=> "peugot-206"
|
166
|
+
car2.friendly_id #=> "peugot-206--2"
|
167
|
+
|
168
|
+
==== Sequence Separator - The Two Dashes
|
169
|
+
|
170
|
+
By default, FriendlyId uses two dashes to separate the slug from a sequence.
|
171
|
+
|
172
|
+
You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
|
173
|
+
sequence_separator} configuration option.
|
174
|
+
|
175
|
+
==== Column or Method?
|
176
|
+
|
177
|
+
FriendlyId always uses a method as the basis of the slug text - not a column. It
|
178
|
+
first glance, this may sound confusing, but remember that Active Record provides
|
179
|
+
methods for each column in a model's associated table, and that's what
|
180
|
+
FriendlyId uses.
|
181
|
+
|
182
|
+
Here's an example of a class that uses a custom method to generate the slug:
|
183
|
+
|
184
|
+
class Person < ActiveRecord::Base
|
185
|
+
friendly_id :name_and_location
|
186
|
+
def name_and_location
|
187
|
+
"#{name} from #{location}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
192
|
+
bob.friendly_id #=> "bob-smith-from-new-york-city"
|
193
|
+
|
194
|
+
==== Providing Your Own Slug Processing Method
|
195
|
+
|
196
|
+
You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
|
197
|
+
total control over the slug format.
|
198
|
+
|
199
|
+
==== Deciding When to Generate New Slugs
|
200
|
+
|
201
|
+
Overriding {FriendlyId::Slugged#should_generate_new_friendly_id?} lets you
|
202
|
+
control whether new friendly ids are created when a model is updated. For
|
203
|
+
example, if you only want to generate slugs once and then treat them as
|
204
|
+
read-only:
|
205
|
+
|
206
|
+
class Post < ActiveRecord::Base
|
207
|
+
extend FriendlyId
|
208
|
+
friendly_id :title, :use => :slugged
|
209
|
+
|
210
|
+
def should_generate_new_friendly_id?
|
211
|
+
new_record?
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
post = Post.create!(:title => "Hello world!")
|
216
|
+
post.slug #=> "hello-world"
|
217
|
+
post.title = "Hello there, world!"
|
218
|
+
post.save!
|
219
|
+
post.slug #=> "hello-world"
|
220
|
+
|
221
|
+
==== Locale-specific Transliterations
|
222
|
+
|
223
|
+
Active Support's +parameterize+ uses
|
224
|
+
transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
|
225
|
+
which in turn can use I18n's transliteration rules to consider the current
|
226
|
+
locale when replacing Latin characters:
|
227
|
+
|
228
|
+
# config/locales/de.yml
|
229
|
+
de:
|
230
|
+
i18n:
|
231
|
+
transliterate:
|
232
|
+
rule:
|
233
|
+
ü: "ue"
|
234
|
+
ö: "oe"
|
235
|
+
etc...
|
236
|
+
|
237
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
238
|
+
movie.slug #=> "der-preis-fuers-ueberleben"
|
239
|
+
|
240
|
+
This functionality was in fact taken from earlier versions of FriendlyId.
|
241
|
+
|
242
|
+
==== Gotchas: Common Problems
|
243
|
+
|
244
|
+
FriendlyId uses a before_validation callback to generate and set the slug. This
|
245
|
+
means that if you create two model instances before saving them, it's possible
|
246
|
+
they will generate the same slug, and the second save will fail.
|
247
|
+
|
248
|
+
This can happen in two fairly normal cases: the first, when a model using nested
|
249
|
+
attributes creates more than one record for a model that uses friendly_id. The
|
250
|
+
second, in concurrent code, either in threads or multiple processes.
|
251
|
+
|
252
|
+
===== Nested Attributes
|
253
|
+
|
254
|
+
To solve the nested attributes issue, I recommend simply avoiding them when
|
255
|
+
creating more than one nested record for a model that uses FriendlyId. See {this
|
256
|
+
Github issue}[https://github.com/norman/friendly_id/issues/185] for discussion.
|
257
|
+
|
258
|
+
===== Concurrency
|
259
|
+
|
260
|
+
To solve the concurrency issue, I recommend locking the model's table against
|
261
|
+
inserts while when saving the record. See {this Github
|
262
|
+
issue}[https://github.com/norman/friendly_id/issues/180] for discussion.
|
263
|
+
|
264
|
+
|
265
|
+
== History: Avoiding 404's When Slugs Change
|
266
|
+
|
267
|
+
FriendlyId's {FriendlyId::History History} module adds the ability to store a
|
268
|
+
log of a model's slugs, so that when its friendly id changes, it's still
|
269
|
+
possible to perform finds by the old id.
|
270
|
+
|
271
|
+
The primary use case for this is avoiding broken URLs.
|
272
|
+
|
273
|
+
=== Setup
|
274
|
+
|
275
|
+
In order to use this module, you must add a table to your database schema to
|
276
|
+
store the slug records. FriendlyId provides a generator for this purpose:
|
277
|
+
|
278
|
+
rails generate friendly_id
|
279
|
+
rake db:migrate
|
280
|
+
|
281
|
+
This will add a table named +friendly_id_slugs+, used by the {FriendlyId::Slug}
|
282
|
+
model.
|
283
|
+
|
284
|
+
=== Considerations
|
285
|
+
|
286
|
+
This module is incompatible with the +:scoped+ module.
|
287
|
+
|
288
|
+
Because recording slug history requires creating additional database records,
|
289
|
+
this module has an impact on the performance of the associated model's +create+
|
290
|
+
method.
|
291
|
+
|
292
|
+
=== Example
|
293
|
+
|
294
|
+
class Post < ActiveRecord::Base
|
295
|
+
extend FriendlyId
|
296
|
+
friendly_id :title, :use => :history
|
297
|
+
end
|
298
|
+
|
299
|
+
class PostsController < ApplicationController
|
300
|
+
|
301
|
+
before_filter :find_post
|
302
|
+
|
303
|
+
...
|
304
|
+
|
305
|
+
def find_post
|
306
|
+
Post.find params[:id]
|
307
|
+
|
308
|
+
# If an old id or a numeric id was used to find the record, then
|
309
|
+
# the request path will not match the post_path, and we should do
|
310
|
+
# a 301 redirect that uses the current friendly id.
|
311
|
+
if request.path != post_path(@post)
|
312
|
+
return redirect_to @post, :status => :moved_permanently
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
== Unique Slugs by Scope
|
319
|
+
|
320
|
+
The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
|
321
|
+
within a scope.
|
322
|
+
|
323
|
+
This allows, for example, two restaurants in different cities to have the slug
|
324
|
+
+joes-diner+:
|
325
|
+
|
326
|
+
class Restaurant < ActiveRecord::Base
|
327
|
+
extend FriendlyId
|
328
|
+
belongs_to :city
|
329
|
+
friendly_id :name, :use => :scoped, :scope => :city
|
330
|
+
end
|
331
|
+
|
332
|
+
class City < ActiveRecord::Base
|
333
|
+
extend FriendlyId
|
334
|
+
has_many :restaurants
|
335
|
+
friendly_id :name, :use => :slugged
|
336
|
+
end
|
337
|
+
|
338
|
+
City.find("seattle").restaurants.find("joes-diner")
|
339
|
+
City.find("chicago").restaurants.find("joes-diner")
|
340
|
+
|
341
|
+
Without :scoped in this case, one of the restaurants would have the slug
|
342
|
+
+joes-diner+ and the other would have +joes-diner--2+.
|
343
|
+
|
344
|
+
The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
|
345
|
+
a column.
|
346
|
+
|
347
|
+
=== Finding Records by Friendly ID
|
348
|
+
|
349
|
+
If you are using scopes your friendly ids may not be unique, so a simple find
|
350
|
+
like
|
351
|
+
|
352
|
+
Restaurant.find("joes-diner")
|
353
|
+
|
354
|
+
may return the wrong record. In these cases it's best to query through the
|
355
|
+
relation:
|
356
|
+
|
357
|
+
@city.restaurants.find("joes-diner")
|
358
|
+
|
359
|
+
Alternatively, you could pass the scope value as a query parameter:
|
360
|
+
|
361
|
+
Restaurant.find("joes-diner").where(:city_id => @city.id)
|
362
|
+
|
363
|
+
|
364
|
+
=== Finding All Records That Match a Scoped ID
|
365
|
+
|
366
|
+
Query the slug column directly:
|
367
|
+
|
368
|
+
Restaurant.find_all_by_slug("joes-diner")
|
369
|
+
|
370
|
+
=== Routes for Scoped Models
|
371
|
+
|
372
|
+
Recall that FriendlyId is a database-centric library, and does not set up any
|
373
|
+
routes for scoped models. You must do this yourself in your application. Here's
|
374
|
+
an example of one way to set this up:
|
375
|
+
|
376
|
+
# in routes.rb
|
377
|
+
resources :cities do
|
378
|
+
resources :restaurants
|
379
|
+
end
|
380
|
+
|
381
|
+
# in views
|
382
|
+
<%= link_to 'Show', [@city, @restaurant] %>
|
383
|
+
|
384
|
+
# in controllers
|
385
|
+
@city = City.find(params[:city_id])
|
386
|
+
@restaurant = @city.restaurants.find(params[:id])
|
387
|
+
|
388
|
+
# URLs:
|
389
|
+
http://example.org/cities/seattle/restaurants/joes-diner
|
390
|
+
http://example.org/cities/chicago/restaurants/joes-diner
|
391
|
+
|
392
|
+
|
393
|
+
== Basic I18n
|
394
|
+
|
395
|
+
This {FriendlyId::I18n i18n} adds very basic i18n support to FriendlyId.
|
396
|
+
|
397
|
+
In order to use this module, your model must have a slug column for each locale.
|
398
|
+
By default FriendlyId looks for columns named, for example, "slug_en",
|
399
|
+
"slug_es", etc. The first part of the name can be configured by passing the
|
400
|
+
+:slug_column+ option if you choose. Note that as of 4.0.0.beta11, the column
|
401
|
+
for the default locale must also include the locale in its name.
|
402
|
+
|
403
|
+
=== Example migration
|
404
|
+
|
405
|
+
def self.up
|
406
|
+
create_table :posts do |t|
|
407
|
+
t.string :title
|
408
|
+
t.string :slug_en
|
409
|
+
t.string :slug_es
|
410
|
+
t.text :body
|
411
|
+
end
|
412
|
+
add_index :posts, :slug_en
|
413
|
+
add_index :posts, :slug_es
|
414
|
+
end
|
415
|
+
|
416
|
+
=== Finds
|
417
|
+
|
418
|
+
Finds will take into consideration the current locale:
|
419
|
+
|
420
|
+
I18n.locale = :es
|
421
|
+
Post.find("la-guerra-de-las-galaxas")
|
422
|
+
I18n.locale = :en
|
423
|
+
Post.find("star-wars")
|
424
|
+
|
425
|
+
To find a slug by an explicit locale, perform the find inside a block
|
426
|
+
passed to I18n's +with_locale+ method:
|
427
|
+
|
428
|
+
I18n.with_locale(:es) do
|
429
|
+
Post.find("la-guerra-de-las-galaxas")
|
430
|
+
end
|
431
|
+
|
432
|
+
=== Creating Records
|
433
|
+
|
434
|
+
When new records are created, the slug is generated for the current locale only.
|
435
|
+
|
436
|
+
=== Translating Slugs
|
437
|
+
|
438
|
+
To translate an existing record's friendly_id, use
|
439
|
+
{FriendlyId::I18n::Model#set_friendly_id}. This will ensure that the slug you
|
440
|
+
add is properly escaped, transliterated and sequenced:
|
441
|
+
|
442
|
+
post = Post.create :name => "Star Wars"
|
443
|
+
post.set_friendly_id("La guerra de las galaxas", :es)
|
444
|
+
|
445
|
+
If you don't pass in a locale argument, FriendlyId::I18n will just use the
|
446
|
+
current locale:
|
447
|
+
|
448
|
+
I18n.with_locale(:es) do
|
449
|
+
post.set_friendly_id("la-guerra-de-las-galaxas")
|
450
|
+
end
|
451
|
+
|
452
|
+
|
453
|
+
== Reserved Words
|
454
|
+
|
455
|
+
The {FriendlyId::Reserved Reserved} module adds the ability to exlude a list of
|
456
|
+
words from use as FriendlyId slugs.
|
457
|
+
|
458
|
+
By default, FriendlyId reserves the words "new" and "edit" when this module is
|
459
|
+
included. You can configure this globally by using {FriendlyId.defaults
|
460
|
+
FriendlyId.defaults}:
|
461
|
+
|
462
|
+
FriendlyId.defaults do |config|
|
463
|
+
config.use :reserved
|
464
|
+
# Reserve words for English and Spanish URLs
|
465
|
+
config.reserved_words = %w(new edit nueva nuevo editar)
|
466
|
+
end
|
data/README.md
CHANGED
@@ -34,6 +34,9 @@ new.
|
|
34
34
|
The current docs can always be found
|
35
35
|
[here](http://rubydoc.info/github/norman/friendly_id/master/frames).
|
36
36
|
|
37
|
+
The best place to start is with the
|
38
|
+
[Guide](http://rubydoc.info/github/norman/friendly_id/master/file/Guide.rdoc),
|
39
|
+
which compiles the top-level RDocs into one outlined document.
|
37
40
|
|
38
41
|
## Rails Quickstart
|
39
42
|
|
data/Rakefile
CHANGED
@@ -17,7 +17,7 @@ task :gem do
|
|
17
17
|
%x{gem build friendly_id.gemspec}
|
18
18
|
end
|
19
19
|
|
20
|
-
task :yard do
|
20
|
+
task :yard => :guide do
|
21
21
|
puts %x{bundle exec yard}
|
22
22
|
end
|
23
23
|
|
@@ -25,6 +25,29 @@ task :bench do
|
|
25
25
|
require File.expand_path("../bench", __FILE__)
|
26
26
|
end
|
27
27
|
|
28
|
+
task :guide do
|
29
|
+
def read_comments(path)
|
30
|
+
path = File.expand_path("../#{path}", __FILE__)
|
31
|
+
match = File.read(path).match(/\n=begin(.*)\n=end/m)[1].to_s
|
32
|
+
match.split("\n").reject {|x| x =~ /^@/}.join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
buffer = []
|
36
|
+
|
37
|
+
buffer << read_comments("lib/friendly_id.rb")
|
38
|
+
buffer << read_comments("lib/friendly_id/base.rb")
|
39
|
+
buffer << read_comments("lib/friendly_id/slugged.rb")
|
40
|
+
buffer << read_comments("lib/friendly_id/history.rb")
|
41
|
+
buffer << read_comments("lib/friendly_id/scoped.rb")
|
42
|
+
buffer << read_comments("lib/friendly_id/i18n.rb")
|
43
|
+
buffer << read_comments("lib/friendly_id/reserved.rb")
|
44
|
+
|
45
|
+
File.open("Guide.rdoc", "w") do |file|
|
46
|
+
file.write("#encoding: utf-8\n")
|
47
|
+
file.write(buffer.join("\n"))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
28
51
|
namespace :test do
|
29
52
|
|
30
53
|
desc "Run each test class in a separate process"
|
data/WhatsNew.md
CHANGED
@@ -46,7 +46,7 @@ maintained until people don't want it any more.
|
|
46
46
|
FriendlyId no longer creates a separate slugs table - it just stores the
|
47
47
|
generated slug value in the model table, which is simpler, faster and what most
|
48
48
|
want by default. Keeping slug history in a separate table is an
|
49
|
-
{FriendlyId::
|
49
|
+
{FriendlyId::History optional add-on} for FriendlyId 4.
|
50
50
|
|
51
51
|
## No more multiple finds
|
52
52
|
|
data/lib/friendly_id.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "thread"
|
3
3
|
require "friendly_id/base"
|
4
|
-
require "friendly_id/model"
|
5
4
|
require "friendly_id/object_utils"
|
6
5
|
require "friendly_id/configuration"
|
7
6
|
require "friendly_id/finder_methods"
|
@@ -22,66 +21,31 @@ in your URLs with strings:
|
|
22
21
|
It requires few changes to your application code and offers flexibility,
|
23
22
|
performance and a well-documented codebase.
|
24
23
|
|
25
|
-
=== Concepts
|
24
|
+
=== Core Concepts
|
26
25
|
|
27
|
-
|
28
|
-
not your routes.
|
26
|
+
==== Slugs
|
29
27
|
|
30
|
-
|
28
|
+
The concept of "slugs[http://en.wikipedia.org/wiki/Slug_(web_publishing)]" is at
|
29
|
+
the heart of FriendlyId.
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
A slug is the part of a URL which identifies a page using human-readable
|
32
|
+
keywords, rather than an opaque identifier such as a numeric id. This can make
|
33
|
+
your application more friendly both for users and search engine.
|
35
34
|
|
36
|
-
|
37
|
-
extend FriendlyId
|
38
|
-
friendly_id :login
|
39
|
-
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
40
|
-
end
|
41
|
-
|
42
|
-
@user = User.find "joe" # the old User.find(1) still works, too
|
43
|
-
@user.to_param # returns "joe"
|
44
|
-
redirect_to @user # the URL will be /users/joe
|
45
|
-
|
46
|
-
In this case, FriendlyId assumes you want to use the column as-is; it will never
|
47
|
-
modify the value of the column, and your application should ensure that the
|
48
|
-
value is admissible in a URL:
|
49
|
-
|
50
|
-
class City < ActiveRecord::Base
|
51
|
-
extend FriendlyId
|
52
|
-
friendly_id :name
|
53
|
-
end
|
54
|
-
|
55
|
-
@city.find "Viña del Mar"
|
56
|
-
redirect_to @city # the URL will be /cities/Viña%20del%20Mar
|
57
|
-
|
58
|
-
For this reason, it is often more convenient to use "slugs" rather than a single
|
59
|
-
column.
|
60
|
-
|
61
|
-
=== Slugged Models
|
62
|
-
|
63
|
-
FriendlyId can uses a separate column to store slugs for models which require
|
64
|
-
some processing of the friendly_id text. The most common example is a blog
|
65
|
-
post's title, which may have spaces, uppercase characters, or other attributes
|
66
|
-
you wish to modify to make them more suitable for use in URL's.
|
67
|
-
|
68
|
-
class Post < ActiveRecord::Base
|
69
|
-
extend FriendlyId
|
70
|
-
friendly_id :title, :use => :slugged
|
71
|
-
end
|
35
|
+
==== Finders: Slugs Act Like Numeric IDs
|
72
36
|
|
73
|
-
|
74
|
-
|
75
|
-
|
37
|
+
To the extent possible, FriendlyId lets you treat text-based identifiers like
|
38
|
+
normal IDs. This means that you can perform finds with slugs just like you do
|
39
|
+
with numeric ids:
|
76
40
|
|
77
|
-
|
41
|
+
Person.find(82542335)
|
42
|
+
Person.find("joe")
|
78
43
|
|
79
|
-
@author Norman Clarke
|
80
44
|
=end
|
81
45
|
module FriendlyId
|
82
46
|
|
83
47
|
# The current version.
|
84
|
-
VERSION = "4.0.0.
|
48
|
+
VERSION = "4.0.0.rc1"
|
85
49
|
|
86
50
|
@mutex = Mutex.new
|
87
51
|
|
data/lib/friendly_id/base.rb
CHANGED
@@ -1,5 +1,55 @@
|
|
1
1
|
module FriendlyId
|
2
|
-
|
2
|
+
=begin
|
3
|
+
|
4
|
+
== Setting Up FriendlyId in Your Model
|
5
|
+
|
6
|
+
To use FriendlyId in your ActiveRecord models, you must first extend the
|
7
|
+
FriendlyId module, then invoke the {FriendlyId::Base#friendly_id friendly_id}
|
8
|
+
method to configure your desired options:
|
9
|
+
|
10
|
+
class Foo < ActiveRecord::Base
|
11
|
+
extend FriendlyId
|
12
|
+
friendly_id bar, :use => [:slugged, :i18n]
|
13
|
+
end
|
14
|
+
|
15
|
+
The most important option is `:use`, which you use to tell FriendlyId which
|
16
|
+
addons it should use. See the documentation for this method for a list of all
|
17
|
+
available addons, or skim through the rest of the docs to get a high-level
|
18
|
+
overview.
|
19
|
+
|
20
|
+
=== The Default Setup: Simple Models
|
21
|
+
|
22
|
+
The simplest way to use FriendlyId is with a model that has a uniquely indexed
|
23
|
+
column with no spaces or special characters, and that is seldom or never
|
24
|
+
updated. The most common example of this is a user name:
|
25
|
+
|
26
|
+
class User < ActiveRecord::Base
|
27
|
+
extend FriendlyId
|
28
|
+
friendly_id :login
|
29
|
+
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
30
|
+
end
|
31
|
+
|
32
|
+
@user = User.find "joe" # the old User.find(1) still works, too
|
33
|
+
@user.to_param # returns "joe"
|
34
|
+
redirect_to @user # the URL will be /users/joe
|
35
|
+
|
36
|
+
In this case, FriendlyId assumes you want to use the column as-is; it will never
|
37
|
+
modify the value of the column, and your application should ensure that the
|
38
|
+
value is unique and admissible in a URL:
|
39
|
+
|
40
|
+
class City < ActiveRecord::Base
|
41
|
+
extend FriendlyId
|
42
|
+
friendly_id :name
|
43
|
+
end
|
44
|
+
|
45
|
+
@city.find "Viña del Mar"
|
46
|
+
redirect_to @city # the URL will be /cities/Viña%20del%20Mar
|
47
|
+
|
48
|
+
Writing the code to process an arbitrary string into a good identifier for use
|
49
|
+
in a URL can be repetitive and surprisingly tricky, so for this reason it's
|
50
|
+
often better and easier to use {FriendlyId::Slugged slugs}.
|
51
|
+
|
52
|
+
=end
|
3
53
|
module Base
|
4
54
|
|
5
55
|
# Configure FriendlyId's behavior in a model.
|
@@ -169,4 +219,29 @@ module FriendlyId
|
|
169
219
|
end
|
170
220
|
end
|
171
221
|
end
|
222
|
+
|
223
|
+
# Instance methods that will be added to all classes using FriendlyId.
|
224
|
+
module Model
|
225
|
+
|
226
|
+
attr_reader :current_friendly_id
|
227
|
+
|
228
|
+
# Convenience method for accessing the class method of the same name.
|
229
|
+
def friendly_id_config
|
230
|
+
self.class.friendly_id_config
|
231
|
+
end
|
232
|
+
|
233
|
+
# Get the instance's friendly_id.
|
234
|
+
def friendly_id
|
235
|
+
send friendly_id_config.query_field
|
236
|
+
end
|
237
|
+
|
238
|
+
# Either the friendly_id, or the numeric id cast to a string.
|
239
|
+
def to_param
|
240
|
+
if diff = changes[friendly_id_config.query_field]
|
241
|
+
diff.first
|
242
|
+
else
|
243
|
+
friendly_id.present? ? friendly_id : id.to_s
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
172
247
|
end
|
data/lib/friendly_id/history.rb
CHANGED
@@ -3,12 +3,16 @@ require "friendly_id/slug"
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
5
|
=begin
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
== History: Avoiding 404's When Slugs Change
|
8
|
+
|
9
|
+
FriendlyId's {FriendlyId::History History} module adds the ability to store a
|
10
|
+
log of a model's slugs, so that when its friendly id changes, it's still
|
11
|
+
possible to perform finds by the old id.
|
8
12
|
|
9
13
|
The primary use case for this is avoiding broken URLs.
|
10
14
|
|
11
|
-
|
15
|
+
=== Setup
|
12
16
|
|
13
17
|
In order to use this module, you must add a table to your database schema to
|
14
18
|
store the slug records. FriendlyId provides a generator for this purpose:
|
@@ -19,7 +23,7 @@ store the slug records. FriendlyId provides a generator for this purpose:
|
|
19
23
|
This will add a table named +friendly_id_slugs+, used by the {FriendlyId::Slug}
|
20
24
|
model.
|
21
25
|
|
22
|
-
|
26
|
+
=== Considerations
|
23
27
|
|
24
28
|
This module is incompatible with the +:scoped+ module.
|
25
29
|
|
@@ -27,7 +31,7 @@ Because recording slug history requires creating additional database records,
|
|
27
31
|
this module has an impact on the performance of the associated model's +create+
|
28
32
|
method.
|
29
33
|
|
30
|
-
|
34
|
+
=== Example
|
31
35
|
|
32
36
|
class Post < ActiveRecord::Base
|
33
37
|
extend FriendlyId
|
data/lib/friendly_id/i18n.rb
CHANGED
@@ -3,7 +3,10 @@ require "i18n"
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
5
|
=begin
|
6
|
-
|
6
|
+
|
7
|
+
== Basic I18n
|
8
|
+
|
9
|
+
This {FriendlyId::I18n i18n} adds very basic i18n support to FriendlyId.
|
7
10
|
|
8
11
|
In order to use this module, your model must have a slug column for each locale.
|
9
12
|
By default FriendlyId looks for columns named, for example, "slug_en",
|
@@ -11,7 +14,7 @@ By default FriendlyId looks for columns named, for example, "slug_en",
|
|
11
14
|
+:slug_column+ option if you choose. Note that as of 4.0.0.beta11, the column
|
12
15
|
for the default locale must also include the locale in its name.
|
13
16
|
|
14
|
-
|
17
|
+
=== Example migration
|
15
18
|
|
16
19
|
def self.up
|
17
20
|
create_table :posts do |t|
|
@@ -24,7 +27,7 @@ for the default locale must also include the locale in its name.
|
|
24
27
|
add_index :posts, :slug_es
|
25
28
|
end
|
26
29
|
|
27
|
-
|
30
|
+
=== Finds
|
28
31
|
|
29
32
|
Finds will take into consideration the current locale:
|
30
33
|
|
@@ -40,14 +43,15 @@ passed to I18n's +with_locale+ method:
|
|
40
43
|
Post.find("la-guerra-de-las-galaxas")
|
41
44
|
end
|
42
45
|
|
43
|
-
|
46
|
+
=== Creating Records
|
44
47
|
|
45
48
|
When new records are created, the slug is generated for the current locale only.
|
46
49
|
|
47
|
-
|
50
|
+
=== Translating Slugs
|
48
51
|
|
49
|
-
To translate an existing record's friendly_id, use
|
50
|
-
ensure that the slug you
|
52
|
+
To translate an existing record's friendly_id, use
|
53
|
+
{FriendlyId::I18n::Model#set_friendly_id}. This will ensure that the slug you
|
54
|
+
add is properly escaped, transliterated and sequenced:
|
51
55
|
|
52
56
|
post = Post.create :name => "Star Wars"
|
53
57
|
post.set_friendly_id("La guerra de las galaxas", :es)
|
data/lib/friendly_id/reserved.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
module FriendlyId
|
2
2
|
|
3
3
|
=begin
|
4
|
-
This module adds the ability to exlude a list of words from use as
|
5
|
-
FriendlyId slugs.
|
6
4
|
|
7
|
-
|
8
|
-
|
5
|
+
== Reserved Words
|
6
|
+
|
7
|
+
The {FriendlyId::Reserved Reserved} module adds the ability to exlude a list of
|
8
|
+
words from use as FriendlyId slugs.
|
9
|
+
|
10
|
+
By default, FriendlyId reserves the words "new" and "edit" when this module is
|
11
|
+
included. You can configure this globally by using {FriendlyId.defaults
|
12
|
+
FriendlyId.defaults}:
|
9
13
|
|
10
14
|
FriendlyId.defaults do |config|
|
11
15
|
config.use :reserved
|
data/lib/friendly_id/scoped.rb
CHANGED
@@ -3,7 +3,11 @@ require "friendly_id/slugged"
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
5
|
=begin
|
6
|
-
|
6
|
+
|
7
|
+
== Unique Slugs by Scope
|
8
|
+
|
9
|
+
The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
|
10
|
+
within a scope.
|
7
11
|
|
8
12
|
This allows, for example, two restaurants in different cities to have the slug
|
9
13
|
+joes-diner+:
|
@@ -29,8 +33,6 @@ Without :scoped in this case, one of the restaurants would have the slug
|
|
29
33
|
The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
|
30
34
|
a column.
|
31
35
|
|
32
|
-
== Tips For Working With Scoped Slugs
|
33
|
-
|
34
36
|
=== Finding Records by Friendly ID
|
35
37
|
|
36
38
|
If you are using scopes your friendly ids may not be unique, so a simple find
|
@@ -12,7 +12,8 @@ module FriendlyId
|
|
12
12
|
|
13
13
|
# Given a slug, get the next available slug in the sequence.
|
14
14
|
def next
|
15
|
-
|
15
|
+
# Don't assume that the separator is unique within the slug
|
16
|
+
sequence = conflict.to_param.gsub(/^#{Regexp.quote(normalized)}(#{Regexp.quote(separator)})?/, '').to_i
|
16
17
|
next_sequence = sequence == 0 ? 2 : sequence.next
|
17
18
|
"#{normalized}#{separator}#{next_sequence}"
|
18
19
|
end
|
data/lib/friendly_id/slugged.rb
CHANGED
@@ -3,21 +3,37 @@ require "friendly_id/slug_generator"
|
|
3
3
|
|
4
4
|
module FriendlyId
|
5
5
|
=begin
|
6
|
-
This module adds in-table slugs to a model.
|
7
6
|
|
8
|
-
|
9
|
-
characters that a developer considers inconvenient for use in URLs. For example,
|
10
|
-
blog applications typically use a post title to provide the basis of a search
|
11
|
-
engine friendly URL:
|
7
|
+
== Slugged Models
|
12
8
|
|
13
|
-
|
9
|
+
FriendlyId can use a separate column to store slugs for models which require
|
10
|
+
some text processing.
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
For example, blog applications typically use a post title to provide the basis
|
13
|
+
of a search engine friendly URL. Such identifiers typically lack uppercase
|
14
|
+
characters, use ASCII to approximate UTF-8 character, and strip out other
|
15
|
+
characters which may make them aesthetically unappealing or error-prone when
|
16
|
+
used in a URL.
|
17
|
+
|
18
|
+
class Post < ActiveRecord::Base
|
19
|
+
extend FriendlyId
|
20
|
+
friendly_id :title, :use => :slugged
|
21
|
+
end
|
22
|
+
|
23
|
+
@post = Post.create(:title => "This is the first post!")
|
24
|
+
@post.friendly_id # returns "this-is-the-first-post"
|
25
|
+
redirect_to @post # the URL will be /posts/this-is-the-first-post
|
26
|
+
|
27
|
+
In general, use slugs by default unless you know for sure you don't need them.
|
28
|
+
To activate the slugging functionality, use the {FriendlyId::Slugged} module.
|
29
|
+
|
30
|
+
FriendlyId will generate slugs from a method or column that you specify, and
|
31
|
+
store them in a field in your model. By default, this field must be named
|
32
|
+
+:slug+, though you may change this using the
|
18
33
|
{FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
|
19
|
-
option. You should add an index to this
|
20
|
-
to NOT NULL, but this depends on your
|
34
|
+
option. You should add an index to this column, and in most cases, make it
|
35
|
+
unique. You may also wish to constrain it to NOT NULL, but this depends on your
|
36
|
+
app's behavior and requirements.
|
21
37
|
|
22
38
|
=== Example Setup
|
23
39
|
|
@@ -45,7 +61,9 @@ to NOT NULL, but this depends on your app's behavior and requirements.
|
|
45
61
|
end
|
46
62
|
end
|
47
63
|
|
48
|
-
===
|
64
|
+
=== Working With Slugs
|
65
|
+
|
66
|
+
==== Formatting
|
49
67
|
|
50
68
|
By default, FriendlyId uses Active Support's
|
51
69
|
paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
|
@@ -55,7 +73,7 @@ dashes, and Unicode Latin characters with ASCII approximations:
|
|
55
73
|
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
56
74
|
movie.slug #=> "der-preis-furs-uberleben"
|
57
75
|
|
58
|
-
====
|
76
|
+
==== Uniqueness
|
59
77
|
|
60
78
|
When you try to insert a record that would generate a duplicate friendly id,
|
61
79
|
FriendlyId will append a sequence to the generated slug to ensure uniqueness:
|
@@ -66,9 +84,11 @@ FriendlyId will append a sequence to the generated slug to ensure uniqueness:
|
|
66
84
|
car.friendly_id #=> "peugot-206"
|
67
85
|
car2.friendly_id #=> "peugot-206--2"
|
68
86
|
|
69
|
-
====
|
87
|
+
==== Sequence Separator - The Two Dashes
|
70
88
|
|
71
|
-
|
89
|
+
By default, FriendlyId uses two dashes to separate the slug from a sequence.
|
90
|
+
|
91
|
+
You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
|
72
92
|
sequence_separator} configuration option.
|
73
93
|
|
74
94
|
==== Column or Method?
|
@@ -92,14 +112,15 @@ Here's an example of a class that uses a custom method to generate the slug:
|
|
92
112
|
|
93
113
|
==== Providing Your Own Slug Processing Method
|
94
114
|
|
95
|
-
You can override {Slugged#normalize_friendly_id} in your model for
|
96
|
-
control over the slug format.
|
115
|
+
You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
|
116
|
+
total control over the slug format.
|
97
117
|
|
98
|
-
==== Deciding
|
118
|
+
==== Deciding When to Generate New Slugs
|
99
119
|
|
100
|
-
Overriding {Slugged#should_generate_new_friendly_id?} lets you
|
101
|
-
new friendly ids are created when a model is updated. For
|
102
|
-
want to generate slugs once and then treat them as
|
120
|
+
Overriding {FriendlyId::Slugged#should_generate_new_friendly_id?} lets you
|
121
|
+
control whether new friendly ids are created when a model is updated. For
|
122
|
+
example, if you only want to generate slugs once and then treat them as
|
123
|
+
read-only:
|
103
124
|
|
104
125
|
class Post < ActiveRecord::Base
|
105
126
|
extend FriendlyId
|
@@ -136,6 +157,29 @@ locale when replacing Latin characters:
|
|
136
157
|
movie.slug #=> "der-preis-fuers-ueberleben"
|
137
158
|
|
138
159
|
This functionality was in fact taken from earlier versions of FriendlyId.
|
160
|
+
|
161
|
+
==== Gotchas: Common Problems
|
162
|
+
|
163
|
+
FriendlyId uses a before_validation callback to generate and set the slug. This
|
164
|
+
means that if you create two model instances before saving them, it's possible
|
165
|
+
they will generate the same slug, and the second save will fail.
|
166
|
+
|
167
|
+
This can happen in two fairly normal cases: the first, when a model using nested
|
168
|
+
attributes creates more than one record for a model that uses friendly_id. The
|
169
|
+
second, in concurrent code, either in threads or multiple processes.
|
170
|
+
|
171
|
+
===== Nested Attributes
|
172
|
+
|
173
|
+
To solve the nested attributes issue, I recommend simply avoiding them when
|
174
|
+
creating more than one nested record for a model that uses FriendlyId. See {this
|
175
|
+
Github issue}[https://github.com/norman/friendly_id/issues/185] for discussion.
|
176
|
+
|
177
|
+
===== Concurrency
|
178
|
+
|
179
|
+
To solve the concurrency issue, I recommend locking the model's table against
|
180
|
+
inserts while when saving the record. See {this Github
|
181
|
+
issue}[https://github.com/norman/friendly_id/issues/180] for discussion.
|
182
|
+
|
139
183
|
=end
|
140
184
|
module Slugged
|
141
185
|
|
@@ -195,10 +239,13 @@ This functionality was in fact taken from earlier versions of FriendlyId.
|
|
195
239
|
# You can override this method in your model if, for example, you only want
|
196
240
|
# slugs to be generated once, and then never updated.
|
197
241
|
def should_generate_new_friendly_id?
|
242
|
+
base = send(friendly_id_config.base)
|
243
|
+
slug_value = send(friendly_id_config.slug_column)
|
244
|
+
return false if base.nil? && slug_value.nil?
|
198
245
|
return true if new_record?
|
199
|
-
slug_base = normalize_friendly_id
|
246
|
+
slug_base = normalize_friendly_id(base)
|
200
247
|
separator = Regexp.escape friendly_id_config.sequence_separator
|
201
|
-
slug_base != current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
|
248
|
+
slug_base != (current_friendly_id || slug_value).try(:sub, /#{separator}[\d]*\z/, '')
|
202
249
|
end
|
203
250
|
|
204
251
|
# Sets the slug.
|
data/test/slugged_test.rb
CHANGED
@@ -42,6 +42,14 @@ class SluggedTest < MiniTest::Unit::TestCase
|
|
42
42
|
refute instance.valid?
|
43
43
|
end
|
44
44
|
|
45
|
+
test "should allow nil slugs" do
|
46
|
+
transaction do
|
47
|
+
m1 = model_class.create!
|
48
|
+
model_class.create!
|
49
|
+
assert_nil m1.slug
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
45
53
|
test "should not break validates_uniqueness_of" do
|
46
54
|
model_class = Class.new(ActiveRecord::Base) do
|
47
55
|
self.table_name = "journalists"
|
@@ -59,14 +67,16 @@ class SluggedTest < MiniTest::Unit::TestCase
|
|
59
67
|
refute instance2.valid?
|
60
68
|
end
|
61
69
|
end
|
62
|
-
|
63
|
-
|
64
70
|
end
|
65
71
|
|
66
72
|
class SlugGeneratorTest < MiniTest::Unit::TestCase
|
67
73
|
|
68
74
|
include FriendlyId::Test
|
69
75
|
|
76
|
+
def model_class
|
77
|
+
Journalist
|
78
|
+
end
|
79
|
+
|
70
80
|
test "should quote column names" do
|
71
81
|
model_class = Class.new(ActiveRecord::Base)
|
72
82
|
model_class.table_name = "journalists"
|
@@ -78,6 +88,27 @@ class SlugGeneratorTest < MiniTest::Unit::TestCase
|
|
78
88
|
flunk "column name was not quoted"
|
79
89
|
end
|
80
90
|
end
|
91
|
+
|
92
|
+
test "should not resequence lower sequences on update" do
|
93
|
+
transaction do
|
94
|
+
m1 = model_class.create! :name => "a b c d"
|
95
|
+
assert_equal "a-b-c-d", m1.slug
|
96
|
+
model_class.create! :name => "a b c d"
|
97
|
+
m1 = model_class.find(m1.id)
|
98
|
+
m1.save!
|
99
|
+
assert_equal "a-b-c-d", m1.slug
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
test "should correctly sequence slugs that end with numbers" do
|
104
|
+
transaction do
|
105
|
+
record1 = model_class.create! :name => "Peugeuot 206"
|
106
|
+
assert_equal "peugeuot-206", record1.slug
|
107
|
+
record2 = model_class.create! :name => "Peugeuot 206"
|
108
|
+
assert_equal "peugeuot-206--2", record2.slug
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
81
112
|
end
|
82
113
|
|
83
114
|
class SlugSeparatorTest < MiniTest::Unit::TestCase
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: friendly_id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.0.
|
4
|
+
version: 4.0.0.rc1
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-
|
12
|
+
date: 2011-12-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: railties
|
16
|
-
requirement: &
|
16
|
+
requirement: &70137630484920 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.1.0
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70137630484920
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activerecord
|
27
|
-
requirement: &
|
27
|
+
requirement: &70137630470220 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 3.1.0
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70137630470220
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: sqlite3
|
38
|
-
requirement: &
|
38
|
+
requirement: &70137630469500 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 1.3.4
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70137630469500
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: minitest
|
49
|
-
requirement: &
|
49
|
+
requirement: &70137630469000 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 2.4.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70137630469000
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: mocha
|
60
|
-
requirement: &
|
60
|
+
requirement: &70137630468520 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 0.9.12
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70137630468520
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: ffaker
|
71
|
-
requirement: &
|
71
|
+
requirement: &70137630468040 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: 1.8.0
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70137630468040
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: maruku
|
82
|
-
requirement: &
|
82
|
+
requirement: &70137630467540 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ~>
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: 0.6.0
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70137630467540
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: yard
|
93
|
-
requirement: &
|
93
|
+
requirement: &70137630466980 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ~>
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: 0.7.2
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70137630466980
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: i18n
|
104
|
-
requirement: &
|
104
|
+
requirement: &70137630466280 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ~>
|
@@ -109,10 +109,10 @@ dependencies:
|
|
109
109
|
version: 0.6.0
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *70137630466280
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: simplecov
|
115
|
-
requirement: &
|
115
|
+
requirement: &70137630465020 !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
118
118
|
- - ! '>='
|
@@ -120,7 +120,7 @@ dependencies:
|
|
120
120
|
version: '0'
|
121
121
|
type: :development
|
122
122
|
prerelease: false
|
123
|
-
version_requirements: *
|
123
|
+
version_requirements: *70137630465020
|
124
124
|
description: ! 'FriendlyId is the "Swiss Army bulldozer" of slugging and permalink
|
125
125
|
plugins for
|
126
126
|
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- .yardopts
|
142
142
|
- Changelog.md
|
143
143
|
- Gemfile
|
144
|
+
- Guide.rdoc
|
144
145
|
- MIT-LICENSE
|
145
146
|
- README.md
|
146
147
|
- Rakefile
|
@@ -156,7 +157,6 @@ files:
|
|
156
157
|
- lib/friendly_id/history.rb
|
157
158
|
- lib/friendly_id/i18n.rb
|
158
159
|
- lib/friendly_id/migration.rb
|
159
|
-
- lib/friendly_id/model.rb
|
160
160
|
- lib/friendly_id/object_utils.rb
|
161
161
|
- lib/friendly_id/reserved.rb
|
162
162
|
- lib/friendly_id/scoped.rb
|
@@ -210,8 +210,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
210
|
version: 1.3.1
|
211
211
|
requirements: []
|
212
212
|
rubyforge_project: friendly_id
|
213
|
-
rubygems_version: 1.8.
|
213
|
+
rubygems_version: 1.8.10
|
214
214
|
signing_key:
|
215
215
|
specification_version: 3
|
216
216
|
summary: A comprehensive slugging and pretty-URL plugin.
|
217
217
|
test_files: []
|
218
|
+
has_rdoc:
|
data/lib/friendly_id/model.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
module FriendlyId
|
2
|
-
# Instance methods that will be added to all classes using FriendlyId.
|
3
|
-
module Model
|
4
|
-
|
5
|
-
attr_reader :current_friendly_id
|
6
|
-
|
7
|
-
# Convenience method for accessing the class method of the same name.
|
8
|
-
def friendly_id_config
|
9
|
-
self.class.friendly_id_config
|
10
|
-
end
|
11
|
-
|
12
|
-
# Get the instance's friendly_id.
|
13
|
-
def friendly_id
|
14
|
-
send friendly_id_config.query_field
|
15
|
-
end
|
16
|
-
|
17
|
-
# Either the friendly_id, or the numeric id cast to a string.
|
18
|
-
def to_param
|
19
|
-
if diff = changes[friendly_id_config.query_field]
|
20
|
-
diff.first
|
21
|
-
else
|
22
|
-
friendly_id.present? ? friendly_id : id.to_s
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|