friendly_id4 4.0.0.pre → 4.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)