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 CHANGED
@@ -1,4 +1,3 @@
1
- --files=*.md
1
+ --files=WhatsNew.md,Changelog.md,Guide.rdoc
2
2
  --protected
3
- --list-undoc
4
3
  --exclude lib/friendly_id/migration
@@ -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"
@@ -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::Slugged optional add-on} for FriendlyId 4.
49
+ {FriendlyId::History optional add-on} for FriendlyId 4.
50
50
 
51
51
  ## No more multiple finds
52
52
 
@@ -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
- Although FriendlyId helps with URLs, it does all of its work inside your models,
28
- not your routes.
26
+ ==== Slugs
29
27
 
30
- === Simple Models
28
+ The concept of "slugs[http://en.wikipedia.org/wiki/Slug_(web_publishing)]" is at
29
+ the heart of FriendlyId.
31
30
 
32
- The simplest way to use FriendlyId is with a model that has a uniquely indexed
33
- column with no spaces or special characters, and that is seldom or never
34
- updated. The most common example of this is a user name:
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
- class User < ActiveRecord::Base
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
- @post = Post.create(:title => "This is the first post!")
74
- @post.friendly_id # returns "this-is-the-first-post"
75
- redirect_to @post # the URL will be /posts/this-is-the-first-post
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
- In general, use slugs by default unless you know for sure you don't need them.
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.beta14"
48
+ VERSION = "4.0.0.rc1"
85
49
 
86
50
  @mutex = Mutex.new
87
51
 
@@ -1,5 +1,55 @@
1
1
  module FriendlyId
2
- # Class methods that will be added to model classes that extend {FriendlyId}.
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
@@ -3,12 +3,16 @@ require "friendly_id/slug"
3
3
  module FriendlyId
4
4
 
5
5
  =begin
6
- This module adds the ability to store a log of a model's slugs, so that when its
7
- friendly id changes, it's still possible to perform finds by the old id.
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
- == Setup
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
- == Considerations
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
- == Example
34
+ === Example
31
35
 
32
36
  class Post < ActiveRecord::Base
33
37
  extend FriendlyId
@@ -3,7 +3,10 @@ require "i18n"
3
3
  module FriendlyId
4
4
 
5
5
  =begin
6
- This module adds very basic i18n support to FriendlyId.
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
- == Example migration
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
- == Finds
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
- == Creating Records
46
+ === Creating Records
44
47
 
45
48
  When new records are created, the slug is generated for the current locale only.
46
49
 
47
- == Translating Slugs
50
+ === Translating Slugs
48
51
 
49
- To translate an existing record's friendly_id, use {#set_friendly_id}. This will
50
- ensure that the slug you add is properly escaped, transliterated and sequenced:
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)
@@ -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
- By default, FriendlyId reserves the words "new" and "edit" when this module
8
- is included. You can configure this globally by using {FriendlyId.defaults FriendlyId.defaults}:
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
@@ -3,7 +3,11 @@ require "friendly_id/slugged"
3
3
  module FriendlyId
4
4
 
5
5
  =begin
6
- This module allows FriendlyId to generate unique slugs within a scope.
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
- sequence = conflict.to_param.split(separator)[1].to_i
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
@@ -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
- Slugs are unique id strings that have been processed to remove or replace
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
- "Gone With The Wind" -> "gone-with-the-wind"
9
+ FriendlyId can use a separate column to store slugs for models which require
10
+ some text processing.
14
11
 
15
- FriendlyId generates slugs from a method or column that you specify, and stores
16
- them in a field in your model. By default, this field must be named +:slug+,
17
- though you may change this using the
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 field. You may also wish to constrain it
20
- to NOT NULL, but this depends on your app's behavior and requirements.
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
- === Slug Format
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
- ==== Slug Uniqueness
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
- ==== Changing the Slug Sequence Separator
87
+ ==== Sequence Separator - The Two Dashes
70
88
 
71
- You can do this with the {Slugged::Configuration#sequence_separator
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 total
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 when to generate new slugs
118
+ ==== Deciding When to Generate New Slugs
99
119
 
100
- Overriding {Slugged#should_generate_new_friendly_id?} lets you control whether
101
- new friendly ids are created when a model is updated. For example, if you only
102
- want to generate slugs once and then treat them as read-only:
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 send(friendly_id_config.base)
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.
@@ -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.beta14
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-10-11 00:00:00.000000000 Z
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: &70302852816780 !ruby/object:Gem::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: *70302852816780
24
+ version_requirements: *70137630484920
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: activerecord
27
- requirement: &70302852816240 !ruby/object:Gem::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: *70302852816240
35
+ version_requirements: *70137630470220
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3
38
- requirement: &70302852815740 !ruby/object:Gem::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: *70302852815740
46
+ version_requirements: *70137630469500
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: minitest
49
- requirement: &70302852815220 !ruby/object:Gem::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: *70302852815220
57
+ version_requirements: *70137630469000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: mocha
60
- requirement: &70302852814740 !ruby/object:Gem::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: *70302852814740
68
+ version_requirements: *70137630468520
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: ffaker
71
- requirement: &70302852814280 !ruby/object:Gem::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: *70302852814280
79
+ version_requirements: *70137630468040
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: maruku
82
- requirement: &70302852813800 !ruby/object:Gem::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: *70302852813800
90
+ version_requirements: *70137630467540
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: yard
93
- requirement: &70302852813340 !ruby/object:Gem::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: *70302852813340
101
+ version_requirements: *70137630466980
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: i18n
104
- requirement: &70302852812880 !ruby/object:Gem::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: *70302852812880
112
+ version_requirements: *70137630466280
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: simplecov
115
- requirement: &70302852812460 !ruby/object:Gem::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: *70302852812460
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.5
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:
@@ -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