friendly_id4 4.0.0.pre → 4.0.0.pre3

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.
@@ -0,0 +1,9 @@
1
+ Gemfile
2
+ Gemfile.lock
3
+ doc
4
+ docs
5
+ pkg
6
+ .DS_Store
7
+ coverage
8
+ .yardoc
9
+ *.gem
@@ -0,0 +1,139 @@
1
+ # FriendlyId 4
2
+
3
+ This is a rewrite/rethink of FriendlyId. It will probably be released some time
4
+ in August or September 2011, once I've had the chance to actually use it in a
5
+ real website for a while.
6
+
7
+ It's probably not wise to use this on a real site right now unless you're
8
+ comfortable with the source code and willing to fix bugs that will likely occur.
9
+
10
+ That said, I will soon be deploying this on a high-traffic, production site, so
11
+ I have a personal stake in making this work well. Your feedback is most welcome.
12
+
13
+ ## Back to basics
14
+
15
+ This isn't the "big rewrite," it's the "small rewrite."
16
+
17
+ Adding new features with each release is not sustainable. This release *removes*
18
+ features, but makes it possible to add them back as addons. We can also remove
19
+ some complexity by relying on the better default functionality provided by newer
20
+ versions of Active Support and Active Record. Let's see how small we can make
21
+ this!
22
+
23
+ Here's what's changed:
24
+
25
+ ## Active Record 3+ only
26
+
27
+ For 2.3 support, you can use FriendlyId 3, which will continue to be maintained
28
+ until people don't want it any more.
29
+
30
+ ## In-table slugs
31
+
32
+ FriendlyId no longer creates a separate slugs table - it just stores the
33
+ generated slug value in the model table, which is simpler, faster and what most
34
+ people seem to want. Keeping slug history in a separate table is an optional
35
+ add-on for FriendlyId 4.
36
+
37
+ ## No more multiple finds
38
+
39
+ Person.find "joe-schmoe" # Supported
40
+ Person.find ["joe-schmoe", "john-doe"] # No longer supported
41
+
42
+ If you want find by more than one friendly id, build your own query:
43
+
44
+ Person.where(:slug => ["joe-schmoe", "john-doe"])
45
+
46
+ This lets us do *far* less monkeypatching in Active Record. How much less?
47
+ FriendlyId overrides the base find with a mere 2 lines of code, and otherwise
48
+ changes nothing else. This means more stability and less breakage between Rails
49
+ updates.
50
+
51
+ ## No more finder status
52
+
53
+ FriendlyId 3 offered finder statuses to help you determine when an outdated
54
+ or non-friendly id was used to find the record, so that you could decide whether
55
+ to permanently redirect to the canonical URL. However, there's a simpler way to
56
+ do that, so this feature has been removed:
57
+
58
+ if request.path != person_path(@person)
59
+ return redirect_to @person, :status => :moved_permanently
60
+ end
61
+
62
+ ## No more slug history - unless you want it
63
+
64
+ Since slugs are now stored in-table, when you update them, finds for the
65
+ previous slug will no longer work. This can be a problem for permalinks, since
66
+ renaming a friendly_id will lead to 404's.
67
+
68
+ This was transparently handled by FriendlyId 3, but there were three problems:
69
+
70
+ * Not everybody wants or needs this
71
+ * Performance was negatively affected
72
+ * Determining whether a current or old id was used was expensive, clunky, and
73
+ inconsistent when finding inside relations.
74
+
75
+ Here's how to do this in FriendlyId 4:
76
+
77
+ class PostsController < ApplicationController
78
+
79
+ before_filter :find_post
80
+
81
+ ...
82
+
83
+ def find_post
84
+ return unless params[:id]
85
+ @post = begin
86
+ Post.find params[:id]
87
+ rescue ActiveRecord::RecordNotFound
88
+ Post.find_by_friendly_id params[:id]
89
+ end
90
+ # If an old id or a numeric id was used to find the record, then
91
+ # the request path will not match the post_path, and we should do
92
+ # a 301 redirect that uses the current friendly_id
93
+ if request.path != post_path(@post)
94
+ return redirect_to @post, :status => :moved_permanently
95
+ end
96
+ end
97
+
98
+ Under FriendlyId 4 this is a little more verbose, but offers much finer-grained
99
+ controler over the finding process, performs better, and has a much simpler
100
+ implementation.
101
+
102
+ ## "Reserved words" are now just a normal validation
103
+
104
+ Rather than use a custom reserved words validator, use the validations provided
105
+ by Active Record. FriendlyId still reserves "new" and "edit" by default to avoid
106
+ routing problems.
107
+
108
+ validates_exclusion_of :name, :in => ["bad", "word"]
109
+
110
+ You can configure the default words reserved by FriendlyId in
111
+ `FriendlyId::Configuration::DEFAULTS[:reserved_words]`.
112
+
113
+ ## "Allow nil" is now just another validation
114
+
115
+ Previous versions of FriendlyId offered a special option to allow nil slug
116
+ values, but this is now the default. If you don't want this, then simply add a
117
+ validation to the slug column, and/or declare the column `NOT NULL` in your
118
+ database.
119
+
120
+ ## Bye-bye Babosa
121
+
122
+ [Babosa](http://github.com/norman/babosa) is FriendlyId 3's slugging library.
123
+
124
+ FriendlyId 4 doesn't use it by default because the most important pieces of it
125
+ were already accepted into Active Support 3.
126
+
127
+ However, Babosa is still useful, for example, for idiomatically transliterating
128
+ Cyrillic ([or other
129
+ language](https://github.com/norman/babosa/tree/master/lib/babosa/transliterator))
130
+ strings to ASCII. It's very easy to include - just override
131
+ `#normalize_friendly_id` in your model:
132
+
133
+ class MyModel < ActiveRecord::Base
134
+ ...
135
+
136
+ def normalize_friendly_id(text)
137
+ text.to_slug.normalize! :transliterate => :russian
138
+ end
139
+ end
@@ -0,0 +1,405 @@
1
+ # FriendlyId Guide
2
+
3
+ * Table of Contents
4
+ {:toc}
5
+
6
+ ## Overview
7
+
8
+ FriendlyId is an ORM-centric Ruby library that lets you work with human-friendly
9
+ strings as if they were numeric ids. Among other things, this facilitates
10
+ replacing "unfriendly" URL's like:
11
+
12
+ http://example.com/states/4323454
13
+
14
+ with "friendly" ones such as:
15
+
16
+ http://example.com/states/washington
17
+
18
+ FriendlyId is typically used with Rails and Active Record, but can also be used in
19
+ non-Rails applications.
20
+
21
+ ## Simple Models
22
+
23
+ The simplest way to use FriendlyId is with a model that has a uniquely indexed
24
+ column with no spaces or special characters, and that is seldom or never
25
+ updated. The most common example of this is a user name or login column:
26
+
27
+ class User < ActiveRecord::Base
28
+ validates_format_of :login, :with => /\A[a-z0-9]+\z/i
29
+ has_friendly_id :login
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 value
38
+ is admissible in a URL:
39
+
40
+ class City < ActiveRecord::Base
41
+ has_friendly_id :name
42
+ end
43
+
44
+ @city.find "Viña del Mar"
45
+ redirect_to @city # the URL will be /cities/Viña%20del%20Mar
46
+
47
+ For this reason, it is often more convenient to use Slugs rather than a single
48
+ column.
49
+
50
+ ## Slugged Models
51
+
52
+ FriendlyId uses a separate column to store slugs for models which require some
53
+ processing of the friendly_id text. The most common example is a blog post's
54
+ title, which may have spaces, uppercase characters, or other attributes you
55
+ wish to modify to make them more suitable for use in URL's.
56
+
57
+ class Post < ActiveRecord::Base
58
+ include FriendlyId::Slugged
59
+ has_friendly_id :title
60
+ end
61
+
62
+ @post = Post.create(:title => "This is the first post!")
63
+ @post.friendly_id # returns "this-is-the-first-post"
64
+ redirect_to @post # the URL will be /posts/this-is-the-first-post
65
+
66
+ If you are unsure whether to use slugs, then your best bet is to use them,
67
+ because FriendlyId provides many useful features that only work with this
68
+ feature. These features are explained in detail {file:Guide.md#features below}.
69
+
70
+ ## Installation
71
+
72
+ gem install friendly_id
73
+
74
+ After installing the gem, add an entry in the Gemfile:
75
+
76
+ gem "friendly_id", "~> 4.0.0"
77
+
78
+ ### Future Compatibility
79
+
80
+ FriendlyId will always remain compatible with the current release of Rails, and
81
+ at least one stable release behind. That means that support for 3.0.x will not be
82
+ dropped until a stable release of 3.2 is out, or possibly longer.
83
+
84
+ ## Configuration
85
+
86
+ FriendlyId is configured in your model using the `has_friendly_id` method. Additional
87
+ features can be activated by including various modules:
88
+
89
+ class Post < ActiveRecord::Base
90
+ # use slugs
91
+ include FriendlyId::Slugged
92
+ # record slug history
93
+ include FriendlyId::History
94
+ # use the "title" accessor as the basis of the friendly_id
95
+ has_friendly_id :title
96
+ end
97
+
98
+ Read on to learn about the various features that can be configured. For the
99
+ full list of valid configuration options, see the instance attribute summary
100
+ for {FriendlyId::Configuration}.
101
+
102
+ # Features
103
+
104
+ ## FriendlyId Strings
105
+
106
+ By default, FriendlyId uses Active Support's Transliterator class to convert strings into
107
+ ASCII slugs by default. Please see the API docs for
108
+ [transliterate](http://api.rubyonrails.org/) and
109
+ [parameterize](http://api.rubyonrails.org/) to see what options are avaialable
110
+ to you.
111
+
112
+ Previous versions of FriendlyId used [Babosa](github.com/norman/babosa) for slug
113
+ string handling, but the core functionality it provides was extracted from it
114
+ and added to Rails 3. However, Babosa offers some advanced functionality not
115
+ offered by Rails and can still be a convenient option. This section shows how
116
+ you can use it with FriendlyId.
117
+
118
+ ### Using a Custom Method to Generate the Slug Text
119
+
120
+ FriendlyId can use either a column or a method to generate the slug text for
121
+ your model:
122
+
123
+ class City < ActiveRecord::Base
124
+
125
+ belongs_to :country
126
+ has_friendly_id :name_and_country, :use_slug => true
127
+
128
+ def name_and_country
129
+ #{name} #{country.name}
130
+ end
131
+
132
+ end
133
+
134
+ @country = Country.create(:name => "Argentina")
135
+ @city = City.create(:name => "Buenos Aires", :country => @country)
136
+ @city.friendly_id # will be "buenos-aires-argentina"
137
+
138
+ One word of caution: in the example above, if the country's name were updated,
139
+ say, to "Argentine Republic", the city's friendly_id would not be
140
+ automatically updated. For this reason, it's a good idea to avoid using
141
+ frequently-updated relations as a part of the friendly_id.
142
+
143
+ ## Using a Custom Method to Process the Slug Text
144
+
145
+ If the built-in slug text handling options don't work for your application,
146
+ you can override the `normalize_friendly_id` method in your model class in
147
+ order to fine-tune the output:
148
+
149
+ class City < ActiveRecord::Base
150
+
151
+ def normalize_friendly_id(text)
152
+ my_text_modifier_method(text)
153
+ end
154
+
155
+ end
156
+
157
+ The normalize_friendly_id method takes a single argument and receives an
158
+ instance of {FriendlyId::SlugString}, a class which wraps a regular Ruby string
159
+ with additional formatting options.
160
+
161
+ ### Converting non-Latin characters to ASCII with Babosa
162
+
163
+ Babosa offers the ability to idiomatically transliterate non-ASCII characters
164
+ to ASCII:
165
+
166
+ "Jürgen".to_slug.normalize! #=> "Jurgen"
167
+ "Jürgen".to_slug.normalize! :transliterate => :german #=> "Juergen"
168
+
169
+ Using Babosa with FriendlyId is a simple matter of installing and requiring
170
+ the `babosa` gem, and overriding the `normalize_friendly_id` method in your
171
+ model:
172
+
173
+ class City < ActiveRecord::Base
174
+ def normalize_friendly_id(text)
175
+ text.slug.normalize!
176
+ end
177
+ end
178
+
179
+ ## Redirecting to the Current Friendly URL
180
+
181
+ FriendlyId can maintain a history of your record's older slugs, so if your
182
+ record's friendly_id changes, your URL's won't break.
183
+
184
+ class Post < ActiveRecord::Base
185
+ include FriendlyId::Slugged
186
+ include FriendlyId::History
187
+ has_friendly_id :title
188
+ end
189
+
190
+ class PostsController < ApplicationController
191
+
192
+ before_filter :find_post
193
+
194
+ ...
195
+ def find_post
196
+ return unless params[:id]
197
+ @post = begin
198
+ Post.find params[:id]
199
+ rescue ActiveRecord::RecordNotFound
200
+ Post.find_by_friendly_id params[:id]
201
+ end
202
+ # If an old id or a numeric id was used to find the record, then
203
+ # the request path will not match the post_path, and we should do
204
+ # a 301 redirect that uses the current friendly_id
205
+ if request.path != post_path(@post)
206
+ return redirect_to @post, :status => :moved_permanently
207
+ end
208
+ end
209
+ end
210
+
211
+ ## Non-unique Slugs
212
+
213
+ FriendlyId will append a arbitrary number to the end of the id to keep it
214
+ unique if necessary:
215
+
216
+ /posts/new-version-released
217
+ /posts/new-version-released--2
218
+ /posts/new-version-released--3
219
+ ...
220
+ etc.
221
+
222
+ Note that the number is preceded by "--" rather than "-" to distinguish it from
223
+ the rest of the slug. This is important to enable having slugs like:
224
+
225
+ /cars/peugeot-206
226
+ /cars/peugeot-206--2
227
+
228
+ You can configure the separator string used by your model by setting the
229
+ `:sequence_separator` option in `has_friendly_id`:
230
+
231
+ has_friendly_id :title, :use_slug => true, :sequence_separator => ":"
232
+
233
+ You can also override the default used in
234
+ {FriendlyId::Configuration::DEFAULTS} to set the value for any model using
235
+ FriendlyId. If you change this value in an existing application, be sure to
236
+ {file:Guide.md#regenerating_slugs regenerate the slugs} afterwards.
237
+
238
+ For reasons I hope are obvious, you can't change this value to "-". If you try,
239
+ FriendlyId will raise an error.
240
+
241
+ ## Reserved Words
242
+
243
+ You can configure a list of strings as reserved so that, for example, you
244
+ don't end up with this problem:
245
+
246
+ /users/joe-schmoe # A user chose "joe schmoe" as his user name - no worries.
247
+ /users/new # A user chose "new" as his user name, and now no one can sign up.
248
+
249
+ Reserved words are configured using the `:reserved_words` option:
250
+
251
+ class Restaurant < ActiveRecord::Base
252
+ belongs_to :city
253
+ has_friendly_id :name, :use_slug => true, :reserved_words => ["my", "values"]
254
+ end
255
+
256
+ The reserved words can be specified as an array or (since 3.1.7) as a regular
257
+ expression.
258
+
259
+ The strings "new" and "index" are reserved by default. When you attempt to
260
+ store a reserved value, FriendlyId raises a
261
+ {FriendlyId::ReservedError}. You can also override the default
262
+ reserved words in {FriendlyId::Configuration::DEFAULTS} to set the value for any
263
+ model using FriendlyId.
264
+
265
+ If you'd like to show a validation error when a word is reserved, you can add
266
+ an callback to your model that catches the error:
267
+
268
+ class Person < ActiveRecord::Base
269
+ has_friendly_id :name, :use_slug => true
270
+
271
+ after_validation :validate_reserved
272
+
273
+ def validate_reserved
274
+ slug_text
275
+ rescue FriendlyId::ReservedError
276
+ @errors[friendly_id_config.method] = "is reserved. Please choose something else"
277
+ return false
278
+ end
279
+ end
280
+
281
+ ## Scoped Slugs
282
+
283
+ FriendlyId can generate unique slugs within a given scope. For example, assume
284
+ you have an application that displays restaurants. Without scoped slugs, if two
285
+ restaurants are named "Joe's Diner," the second one will end up with
286
+ "joes-diner--2" as its friendly_id. Using scoped allows you to keep the slug
287
+ names unique for each city, so that the second "Joe's Diner" can also have the
288
+ slug "joes-diner", as long as it's located in a different city:
289
+
290
+ class Restaurant < ActiveRecord::Base
291
+ belongs_to :city
292
+ include FriendlyId::Slugged
293
+ include FriendlyId::Scoped
294
+ has_friendly_id :name, :scope => :city
295
+ end
296
+
297
+ class City < ActiveRecord::Base
298
+ has_many :restaurants
299
+ include FriendlyId::Slugged
300
+ has_friendly_id :name
301
+ end
302
+
303
+ City.find("seattle").restaurants.find("joes-diner")
304
+ City.find("chicago").restaurants.find("joes-diner")
305
+
306
+
307
+ The value for the `:scope` key in your model can be a column, or the name of a
308
+ relation.
309
+
310
+ ### Complications with Scoped Slugs
311
+
312
+ #### Finding Records by friendly\_id
313
+
314
+ If you are using scopes your friendly ids may not be unique, so a simple find like
315
+
316
+ Restaurant.find("joes-diner")
317
+
318
+ may return the wrong record. In these cases when you want to use the friendly\_id for queries,
319
+ either query as a relation, or specify the scope in your query conditions:
320
+
321
+ # will only return restaurants named "Joe's Diner" in the given city
322
+ @city.restaurants.find("joes-diner")
323
+
324
+ # or
325
+
326
+ Restaurants.find("joes-diner").where(:city_id => @city.id)
327
+
328
+
329
+ #### Finding All Records That Match a Scoped ID
330
+
331
+ If you want to find all records with a particular friendly\_id regardless of scope,
332
+ the easiest way is to use cached slugs and query this column directly:
333
+
334
+ Restaurant.find_all_by_slug("joes-diner")
335
+
336
+ ### Routes for Scoped Models
337
+
338
+ Note that FriendlyId does not set up any routes for scoped models; you must do
339
+ this yourself in your application. Here's an example of one way to set this up:
340
+
341
+ # in routes.rb
342
+ resources :cities do
343
+ resources :restaurants
344
+ end
345
+
346
+ # in views
347
+ <%= link_to 'Show', [@city, @restaurant] %>
348
+
349
+ # in controllers
350
+ @city = City.find(params[:city_id])
351
+ @restaurant = @city.restaurants.find(params[:id])
352
+
353
+ # URL's:
354
+ http://example.org/cities/seattle/restaurants/joes-diner
355
+ http://example.org/cities/chicago/restaurants/joes-diner
356
+
357
+
358
+ # Misc tips
359
+
360
+ ## Allowing Users to Override/Control Slugs
361
+
362
+ Would you like to mostly use default slugs, but allow the option of a
363
+ custom user-chosen slug in your application? If so, then you're not the first to
364
+ want this. Here's a [demo
365
+ application](http://github.com/norman/friendly_id_manual_slug_demo) showing how
366
+ it can be done.
367
+
368
+ ## Default Scopes
369
+
370
+ Whether you're using FriendlyId or not, a good rule of thumb for default scopes
371
+ is to always use your model's table name. Otherwise any time you do a join, you
372
+ risk having queries fail because of duplicate column names - particularly for a
373
+ default scope like this one:
374
+
375
+ default_scope :order => "created_at DESC"
376
+
377
+ Instead, do this:
378
+
379
+ default_scope :order => = "#{quoted_table_name}.created_at DESC"
380
+
381
+ Or even better, unless you're using a custom primary key:
382
+
383
+ default_scope :order => = "#{quoted_table_name}.id DESC"
384
+
385
+ because sorting by a unique integer column is faster than sorting by a date
386
+ column.
387
+
388
+ ## Some Benchmarks
389
+
390
+ These benchmarks can give you an idea of FriendlyId's impact on the
391
+ performance of your application. Of course your results may vary.
392
+
393
+ activerecord (3.0.9)
394
+ ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-darwin10.6.0]
395
+ friendly_id (4.0.0.pre3)
396
+ sqlite3 (1.3.3) gem
397
+ sqlite3 3.6.12 in-memory database
398
+
399
+ user system total real
400
+ find (without FriendlyId) 0.280000 0.000000 0.280000 ( 0.278086)
401
+ find (in-table slug) 0.320000 0.000000 0.320000 ( 0.320151)
402
+ find (external slug) 3.040000 0.010000 3.050000 ( 3.048054)
403
+ insert (without FriendlyId) 0.780000 0.000000 0.780000 ( 0.785427)
404
+ insert (in-table-slug) 1.520000 0.010000 1.530000 ( 1.532350)
405
+ insert (external slug) 3.310000 0.020000 3.330000 ( 3.335548)