friendly_id 4.0.0.beta14 → 4.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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