friendly_id 5.0.0.beta4 → 5.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.yardopts +1 -0
- data/Changelog.md +7 -0
- data/README.md +11 -4
- data/Rakefile +6 -21
- data/guide.rb +17 -0
- data/lib/friendly_id.rb +13 -0
- data/lib/friendly_id/initializer.rb +1 -1
- data/lib/friendly_id/object_utils.rb +1 -4
- data/lib/friendly_id/reserved.rb +8 -8
- data/lib/friendly_id/simple_i18n.rb +4 -4
- data/lib/friendly_id/version.rb +1 -1
- metadata +3 -8
- data/Guide.md +0 -597
- data/test/compatibility/ancestry/Gemfile +0 -8
- data/test/compatibility/ancestry/ancestry_test.rb +0 -34
- data/test/compatibility/threading/Gemfile +0 -8
- data/test/compatibility/threading/threading.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb59a1c03451e5b3848ab8fa00da5aa476276d79
|
4
|
+
data.tar.gz: f6057d3fad4267d1cea422b0eb72b78f433afc78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bbd5d21be8df888836e6e98b14c43d45eb89da31816830c5287038dce5f1a60c9920adf3b69a498e96b5d0cb6addfe952c5983d307228f5c57726095d9160c7
|
7
|
+
data.tar.gz: cae59c6cbc8406c0cedf700ad1201a23b01d74ec6b7d871333d8c156e273332485f581b59d2df9e59da689e4c4219dffe6a8f770f7a48e9f8af1b8b0a18b8de3
|
data/.gitignore
CHANGED
data/.yardopts
CHANGED
data/Changelog.md
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
We would like to think our many {file:Contributors contributors} for
|
4
4
|
suggestions, ideas and improvements to FriendlyId.
|
5
5
|
|
6
|
+
## 5.0.0.rc1 (2013-08-28)
|
7
|
+
|
8
|
+
* Removed some outdated tests.
|
9
|
+
* Improved documentation.
|
10
|
+
* Removed Guide from repository and added tasks to maintain docs up to date
|
11
|
+
on Github pages at http://norman.github.io/friendly_id.
|
12
|
+
|
6
13
|
## 5.0.0.beta4 (2013-08-21)
|
7
14
|
|
8
15
|
* Add an initializer to the generator; move the default reserved words there.
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ branch](https://github.com/norman/friendly_id/tree/4.0-stable).
|
|
15
15
|
|
16
16
|
# FriendlyId
|
17
17
|
|
18
|
-
<em>For the most complete, user-friendly documentation, see the [FriendlyId Guide](http://
|
18
|
+
<em>For the most complete, user-friendly documentation, see the [FriendlyId Guide](http://norman.github.io/friendly_id/file.Guide.html).</em>
|
19
19
|
|
20
20
|
FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for
|
21
21
|
Active Record. It lets you create pretty URLs and work with human-friendly
|
@@ -91,11 +91,13 @@ The most important changes are:
|
|
91
91
|
restaurant.friendly_id # the-plaza-diner
|
92
92
|
|
93
93
|
You can restore some of the old behavior by overriding the
|
94
|
-
`should_generate_new_friendly_id
|
94
|
+
`should_generate_new_friendly_id?` method.
|
95
95
|
|
96
96
|
* The `friendly_id` Rails generator now generates an initializer showing you
|
97
97
|
how to do some commmon global configuration.
|
98
98
|
|
99
|
+
* The Globalize plugin has moved to a separate gem (currently in alpha).
|
100
|
+
|
99
101
|
* The `:reserved` module no longer includes any default reserved words.
|
100
102
|
Previously it blocked "edit" and "new" everywhere. The default word list has
|
101
103
|
been moved to `config/initializers/friendly_id.rb` and now includes many more
|
@@ -127,10 +129,15 @@ A migration like this should be sufficient:
|
|
127
129
|
## Docs
|
128
130
|
|
129
131
|
The most current docs from the master branch can always be found
|
130
|
-
[here](http://
|
132
|
+
[here](http://norman.github.io/friendly_id).
|
133
|
+
|
134
|
+
Docs for older versions are also available:
|
135
|
+
|
136
|
+
* [4.0](http://norman.github.io/friendly_id/4.0/)
|
137
|
+
* [3.3](http://norman.github.io/friendly_id/3.3/)
|
131
138
|
|
132
139
|
The best place to start is with the
|
133
|
-
[Guide](http://
|
140
|
+
[Guide](http://norman.github.io/friendly_id/file.Guide.html),
|
134
141
|
which compiles the top-level RDocs into one outlined document.
|
135
142
|
|
136
143
|
You might also want to watch Ryan Bates's [Railscast on FriendlyId](http://railscasts.com/episodes/314-pretty-urls-with-friendlyid),
|
data/Rakefile
CHANGED
@@ -27,7 +27,7 @@ task :gem do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
desc "Build YARD documentation"
|
30
|
-
task :yard
|
30
|
+
task :yard do
|
31
31
|
puts %x{bundle exec yard}
|
32
32
|
end
|
33
33
|
|
@@ -38,26 +38,7 @@ end
|
|
38
38
|
|
39
39
|
desc "Generate Guide.md"
|
40
40
|
task :guide do
|
41
|
-
|
42
|
-
path = File.expand_path("../#{path}", __FILE__)
|
43
|
-
match = File.read(path).match(/\n=begin(.*)\n=end/m)[1].to_s
|
44
|
-
match.split("\n").reject {|x| x =~ /^@/}.join("\n")
|
45
|
-
end
|
46
|
-
|
47
|
-
buffer = []
|
48
|
-
|
49
|
-
buffer << read_comments("lib/friendly_id.rb")
|
50
|
-
buffer << read_comments("lib/friendly_id/base.rb")
|
51
|
-
buffer << read_comments("lib/friendly_id/finders.rb")
|
52
|
-
buffer << read_comments("lib/friendly_id/slugged.rb")
|
53
|
-
buffer << read_comments("lib/friendly_id/history.rb")
|
54
|
-
buffer << read_comments("lib/friendly_id/scoped.rb")
|
55
|
-
buffer << read_comments("lib/friendly_id/simple_i18n.rb")
|
56
|
-
buffer << read_comments("lib/friendly_id/reserved.rb")
|
57
|
-
|
58
|
-
File.open("Guide.md", "w") do |file|
|
59
|
-
file.write(buffer.join("\n"))
|
60
|
-
end
|
41
|
+
load File.expand_path('../guide.rb', __FILE__)
|
61
42
|
end
|
62
43
|
|
63
44
|
namespace :test do
|
@@ -110,3 +91,7 @@ namespace :db do
|
|
110
91
|
end
|
111
92
|
|
112
93
|
task :doc => :yard
|
94
|
+
|
95
|
+
task :docs do
|
96
|
+
sh %{git checkout gh-pages && rake doc && git checkout @{-1}}
|
97
|
+
end
|
data/guide.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This script generates the Guide.md file included in the Yard docs.
|
4
|
+
|
5
|
+
def comments_from path
|
6
|
+
path = File.expand_path("../lib/friendly_id/#{path}", __FILE__)
|
7
|
+
match = File.read(path).match(/\n=begin(.*)\n=end/m)[1].to_s
|
8
|
+
match.split("\n").reject {|x| x =~ /^@/}.join("\n").strip
|
9
|
+
end
|
10
|
+
|
11
|
+
File.open(File.expand_path('../Guide.md', __FILE__), 'w:utf-8') do |guide|
|
12
|
+
['../friendly_id.rb', 'base.rb', 'finders.rb', 'slugged.rb', 'history.rb',
|
13
|
+
'scoped.rb', 'simple_i18n.rb', 'reserved.rb'].each do |file|
|
14
|
+
guide.write comments_from file
|
15
|
+
guide.write "\n"
|
16
|
+
end
|
17
|
+
end
|
data/lib/friendly_id.rb
CHANGED
@@ -50,6 +50,19 @@ module FriendlyId
|
|
50
50
|
autoload :Slugged, "friendly_id/slugged"
|
51
51
|
autoload :Finders, "friendly_id/finders"
|
52
52
|
|
53
|
+
# Instances of these classes will never be considered a friendly id.
|
54
|
+
# @see FriendlyId::ObjectUtils#friendly_id
|
55
|
+
UNFRIENDLY_CLASSES = [
|
56
|
+
ActiveRecord::Base,
|
57
|
+
Array,
|
58
|
+
FalseClass,
|
59
|
+
Hash,
|
60
|
+
NilClass,
|
61
|
+
Numeric,
|
62
|
+
Symbol,
|
63
|
+
TrueClass
|
64
|
+
]
|
65
|
+
|
53
66
|
# FriendlyId takes advantage of `extended` to do basic model setup, primarily
|
54
67
|
# extending {FriendlyId::Base} to add {FriendlyId::Base#friendly_id
|
55
68
|
# friendly_id} as a class method.
|
@@ -29,10 +29,7 @@ module FriendlyId
|
|
29
29
|
def friendly_id?
|
30
30
|
# Considered unfriendly if this is an instance of an unfriendly class or
|
31
31
|
# one of its descendants.
|
32
|
-
|
33
|
-
Symbol, TrueClass, FalseClass]
|
34
|
-
|
35
|
-
if unfriendly_classes.detect {|klass| self.class <= klass}
|
32
|
+
if FriendlyId::UNFRIENDLY_CLASSES.detect {|klass| self.class <= klass}
|
36
33
|
false
|
37
34
|
elsif respond_to?(:to_i) && to_i.to_s != to_s
|
38
35
|
true
|
data/lib/friendly_id/reserved.rb
CHANGED
@@ -4,7 +4,7 @@ module FriendlyId
|
|
4
4
|
|
5
5
|
## Reserved Words
|
6
6
|
|
7
|
-
The {FriendlyId::Reserved Reserved} module adds the ability to
|
7
|
+
The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
|
8
8
|
words from use as FriendlyId slugs.
|
9
9
|
|
10
10
|
With Ruby on Rails, FriendlyId's generator generates an initializer that
|
@@ -17,16 +17,16 @@ it will have no field to highlight. If you'd like to change this so that
|
|
17
17
|
scaffolding works as expected, one way to accomplish this is to move the error
|
18
18
|
message to a different field. For example:
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
class Person < ActiveRecord::Base
|
21
|
+
extend FriendlyId
|
22
|
+
friendly_id :name, use: :slugged
|
23
23
|
|
24
|
-
|
24
|
+
after_validation :move_friendly_id_error_to_name
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
def move_friendly_id_error_to_name
|
27
|
+
errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
|
28
|
+
end
|
28
29
|
end
|
29
|
-
end
|
30
30
|
|
31
31
|
=end
|
32
32
|
module Reserved
|
@@ -37,7 +37,7 @@ friendly_id_globalize gem instead.
|
|
37
37
|
Finds will take into consideration the current locale:
|
38
38
|
|
39
39
|
I18n.locale = :es
|
40
|
-
Post.find("la-guerra-de-las-
|
40
|
+
Post.find("la-guerra-de-las-galaxias")
|
41
41
|
I18n.locale = :en
|
42
42
|
Post.find("star-wars")
|
43
43
|
|
@@ -45,7 +45,7 @@ To find a slug by an explicit locale, perform the find inside a block
|
|
45
45
|
passed to I18n's `with_locale` method:
|
46
46
|
|
47
47
|
I18n.with_locale(:es) do
|
48
|
-
Post.find("la-guerra-de-las-
|
48
|
+
Post.find("la-guerra-de-las-galaxias")
|
49
49
|
end
|
50
50
|
|
51
51
|
### Creating Records
|
@@ -59,13 +59,13 @@ To translate an existing record's friendly_id, use
|
|
59
59
|
you add is properly escaped, transliterated and sequenced:
|
60
60
|
|
61
61
|
post = Post.create :name => "Star Wars"
|
62
|
-
post.set_friendly_id("La guerra de las
|
62
|
+
post.set_friendly_id("La guerra de las galaxias", :es)
|
63
63
|
|
64
64
|
If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
|
65
65
|
current locale:
|
66
66
|
|
67
67
|
I18n.with_locale(:es) do
|
68
|
-
post.set_friendly_id("La guerra de las
|
68
|
+
post.set_friendly_id("La guerra de las galaxias")
|
69
69
|
end
|
70
70
|
=end
|
71
71
|
module SimpleI18n
|
data/lib/friendly_id/version.rb
CHANGED
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: 5.0.0.
|
4
|
+
version: 5.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Norman Clarke
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-08-
|
12
|
+
date: 2013-08-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -155,13 +155,13 @@ files:
|
|
155
155
|
- CONTRIBUTING.md
|
156
156
|
- Changelog.md
|
157
157
|
- Gemfile
|
158
|
-
- Guide.md
|
159
158
|
- MIT-LICENSE
|
160
159
|
- README.md
|
161
160
|
- Rakefile
|
162
161
|
- bench.rb
|
163
162
|
- friendly_id.gemspec
|
164
163
|
- gemfiles/Gemfile.rails-4.0.rb
|
164
|
+
- guide.rb
|
165
165
|
- lib/friendly_id.rb
|
166
166
|
- lib/friendly_id/.gitattributes
|
167
167
|
- lib/friendly_id/base.rb
|
@@ -182,11 +182,6 @@ files:
|
|
182
182
|
- lib/friendly_id/version.rb
|
183
183
|
- lib/generators/friendly_id_generator.rb
|
184
184
|
- test/base_test.rb
|
185
|
-
- test/compatibility/ancestry/Gemfile
|
186
|
-
- test/compatibility/ancestry/ancestry_test.rb
|
187
|
-
- test/compatibility/threading/Gemfile
|
188
|
-
- test/compatibility/threading/Gemfile.lock
|
189
|
-
- test/compatibility/threading/threading.rb
|
190
185
|
- test/configuration_test.rb
|
191
186
|
- test/core_test.rb
|
192
187
|
- test/databases.yml
|
data/Guide.md
DELETED
@@ -1,597 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
## About FriendlyId
|
4
|
-
|
5
|
-
FriendlyId is an add-on to Ruby's Active Record that allows you to replace ids
|
6
|
-
in your URLs with strings:
|
7
|
-
|
8
|
-
# without FriendlyId
|
9
|
-
http://example.com/states/4323454
|
10
|
-
|
11
|
-
# with FriendlyId
|
12
|
-
http://example.com/states/washington
|
13
|
-
|
14
|
-
It requires few changes to your application code and offers flexibility,
|
15
|
-
performance and a well-documented codebase.
|
16
|
-
|
17
|
-
### Core Concepts
|
18
|
-
|
19
|
-
#### Slugs
|
20
|
-
|
21
|
-
The concept of *slugs* is at the heart of FriendlyId.
|
22
|
-
|
23
|
-
A slug is the part of a URL which identifies a page using human-readable
|
24
|
-
keywords, rather than an opaque identifier such as a numeric id. This can make
|
25
|
-
your application more friendly both for users and search engine.
|
26
|
-
|
27
|
-
#### Finders: Slugs Act Like Numeric IDs
|
28
|
-
|
29
|
-
To the extent possible, FriendlyId lets you treat text-based identifiers like
|
30
|
-
normal IDs. This means that you can perform finds with slugs just like you do
|
31
|
-
with numeric ids:
|
32
|
-
|
33
|
-
Person.find(82542335)
|
34
|
-
Person.friendly.find("joe")
|
35
|
-
|
36
|
-
|
37
|
-
## Setting Up FriendlyId in Your Model
|
38
|
-
|
39
|
-
To use FriendlyId in your ActiveRecord models, you must first either extend or
|
40
|
-
include the FriendlyId module (it makes no difference), then invoke the
|
41
|
-
{FriendlyId::Base#friendly_id friendly_id} method to configure your desired
|
42
|
-
options:
|
43
|
-
|
44
|
-
class Foo < ActiveRecord::Base
|
45
|
-
include FriendlyId
|
46
|
-
friendly_id :bar, :use => [:slugged, :simple_i18n]
|
47
|
-
end
|
48
|
-
|
49
|
-
The most important option is `:use`, which you use to tell FriendlyId which
|
50
|
-
addons it should use. See the documentation for this method for a list of all
|
51
|
-
available addons, or skim through the rest of the docs to get a high-level
|
52
|
-
overview.
|
53
|
-
|
54
|
-
### The Default Setup: Simple Models
|
55
|
-
|
56
|
-
The simplest way to use FriendlyId is with a model that has a uniquely indexed
|
57
|
-
column with no spaces or special characters, and that is seldom or never
|
58
|
-
updated. The most common example of this is a user name:
|
59
|
-
|
60
|
-
class User < ActiveRecord::Base
|
61
|
-
extend FriendlyId
|
62
|
-
friendly_id :login
|
63
|
-
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
64
|
-
end
|
65
|
-
|
66
|
-
@user = User.friendly.find "joe" # the old User.find(1) still works, too
|
67
|
-
@user.to_param # returns "joe"
|
68
|
-
redirect_to @user # the URL will be /users/joe
|
69
|
-
|
70
|
-
In this case, FriendlyId assumes you want to use the column as-is; it will never
|
71
|
-
modify the value of the column, and your application should ensure that the
|
72
|
-
value is unique and admissible in a URL:
|
73
|
-
|
74
|
-
class City < ActiveRecord::Base
|
75
|
-
extend FriendlyId
|
76
|
-
friendly_id :name
|
77
|
-
end
|
78
|
-
|
79
|
-
@city.friendly.find "Viña del Mar"
|
80
|
-
redirect_to @city # the URL will be /cities/Viña%20del%20Mar
|
81
|
-
|
82
|
-
Writing the code to process an arbitrary string into a good identifier for use
|
83
|
-
in a URL can be repetitive and surprisingly tricky, so for this reason it's
|
84
|
-
often better and easier to use {FriendlyId::Slugged slugs}.
|
85
|
-
|
86
|
-
## Performing Finds with FriendlyId
|
87
|
-
|
88
|
-
FriendlyId offers enhanced finders which will search for your record by
|
89
|
-
friendly id, and fall back to the numeric id if necessary. This makes it easy
|
90
|
-
to add FriendlyId to an existing application with minimal code modification.
|
91
|
-
|
92
|
-
By default, these methods are available only on the `friendly` scope:
|
93
|
-
|
94
|
-
Restaurant.friendly.find('plaza-diner') #=> works
|
95
|
-
Restaurant.friendly.find(23) #=> also works
|
96
|
-
Restaurant.find(23) #=> still works
|
97
|
-
Restaurant.find('plaza-diner') #=> will not work
|
98
|
-
|
99
|
-
### Restoring FriendlyId 4.0-style finders
|
100
|
-
|
101
|
-
Prior to version 5.0, FriendlyId overrode the default finder methods to perform
|
102
|
-
friendly finds all the time. This required modifying parts of Rails that did
|
103
|
-
not have a public API, which was harder to maintain and at times caused
|
104
|
-
compatiblity problems. In 5.0 we decided change the library's defaults and add
|
105
|
-
the friendly finder methods only to the `friendly` scope in order to boost
|
106
|
-
compatiblity. However, you can still opt-in to original functionality very
|
107
|
-
easily by using the `:finders` addon:
|
108
|
-
|
109
|
-
class Restaurant < ActiveRecord::Base
|
110
|
-
extend FriendlyId
|
111
|
-
|
112
|
-
scope :active, -> {where(:active => true)}
|
113
|
-
|
114
|
-
friendly_id :name, :use => [:slugged, :finders]
|
115
|
-
end
|
116
|
-
|
117
|
-
Restaurant.friendly.find('plaza-diner') #=> works
|
118
|
-
Restaurant.find('plaza-diner') #=> now also works
|
119
|
-
Restaurant.active.find('plaza-diner') #=> now also works
|
120
|
-
|
121
|
-
### Updating your application to use FriendlyId's finders
|
122
|
-
|
123
|
-
Unless you've chosen to use the `:finders` addon, be sure to modify the finders
|
124
|
-
in your controllers to use the `friendly` scope. For example:
|
125
|
-
|
126
|
-
# before
|
127
|
-
def set_restaurant
|
128
|
-
@restaurant = Restaurant.find(params[:id])
|
129
|
-
end
|
130
|
-
|
131
|
-
# after
|
132
|
-
def set_restaurant
|
133
|
-
@restaurant = Restaurant.friendly.find(params[:id])
|
134
|
-
end
|
135
|
-
|
136
|
-
|
137
|
-
## Slugged Models
|
138
|
-
|
139
|
-
FriendlyId can use a separate column to store slugs for models which require
|
140
|
-
some text processing.
|
141
|
-
|
142
|
-
For example, blog applications typically use a post title to provide the basis
|
143
|
-
of a search engine friendly URL. Such identifiers typically lack uppercase
|
144
|
-
characters, use ASCII to approximate UTF-8 character, and strip out other
|
145
|
-
characters which may make them aesthetically unappealing or error-prone when
|
146
|
-
used in a URL.
|
147
|
-
|
148
|
-
class Post < ActiveRecord::Base
|
149
|
-
extend FriendlyId
|
150
|
-
friendly_id :title, :use => :slugged
|
151
|
-
end
|
152
|
-
|
153
|
-
@post = Post.create(:title => "This is the first post!")
|
154
|
-
@post.friendly_id # returns "this-is-the-first-post"
|
155
|
-
redirect_to @post # the URL will be /posts/this-is-the-first-post
|
156
|
-
|
157
|
-
In general, use slugs by default unless you know for sure you don't need them.
|
158
|
-
To activate the slugging functionality, use the {FriendlyId::Slugged} module.
|
159
|
-
|
160
|
-
FriendlyId will generate slugs from a method or column that you specify, and
|
161
|
-
store them in a field in your model. By default, this field must be named
|
162
|
-
`:slug`, though you may change this using the
|
163
|
-
{FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
|
164
|
-
option. You should add an index to this column, and in most cases, make it
|
165
|
-
unique. You may also wish to constrain it to NOT NULL, but this depends on your
|
166
|
-
app's behavior and requirements.
|
167
|
-
|
168
|
-
### Example Setup
|
169
|
-
|
170
|
-
# your model
|
171
|
-
class Post < ActiveRecord::Base
|
172
|
-
extend FriendlyId
|
173
|
-
friendly_id :title, :use => :slugged
|
174
|
-
validates_presence_of :title, :slug, :body
|
175
|
-
end
|
176
|
-
|
177
|
-
# a migration
|
178
|
-
class CreatePosts < ActiveRecord::Migration
|
179
|
-
def self.up
|
180
|
-
create_table :posts do |t|
|
181
|
-
t.string :title, :null => false
|
182
|
-
t.string :slug, :null => false
|
183
|
-
t.text :body
|
184
|
-
end
|
185
|
-
|
186
|
-
add_index :posts, :slug, :unique => true
|
187
|
-
end
|
188
|
-
|
189
|
-
def self.down
|
190
|
-
drop_table :posts
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
### Working With Slugs
|
195
|
-
|
196
|
-
#### Formatting
|
197
|
-
|
198
|
-
By default, FriendlyId uses Active Support's
|
199
|
-
[paramaterize](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize)
|
200
|
-
method to create slugs. This method will intelligently replace spaces with
|
201
|
-
dashes, and Unicode Latin characters with ASCII approximations:
|
202
|
-
|
203
|
-
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
204
|
-
movie.slug #=> "der-preis-furs-uberleben"
|
205
|
-
|
206
|
-
#### Column or Method?
|
207
|
-
|
208
|
-
FriendlyId always uses a method as the basis of the slug text - not a column. It
|
209
|
-
first glance, this may sound confusing, but remember that Active Record provides
|
210
|
-
methods for each column in a model's associated table, and that's what
|
211
|
-
FriendlyId uses.
|
212
|
-
|
213
|
-
Here's an example of a class that uses a custom method to generate the slug:
|
214
|
-
|
215
|
-
class Person < ActiveRecord::Base
|
216
|
-
friendly_id :name_and_location
|
217
|
-
def name_and_location
|
218
|
-
"#{name} from #{location}"
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
223
|
-
bob.friendly_id #=> "bob-smith-from-new-york-city"
|
224
|
-
|
225
|
-
FriendlyId refers to this internally as the "base" method.
|
226
|
-
|
227
|
-
#### Uniqueness
|
228
|
-
|
229
|
-
When you try to insert a record that would generate a duplicate friendly id,
|
230
|
-
FriendlyId will append a UUID to the generated slug to ensure uniqueness:
|
231
|
-
|
232
|
-
car = Car.create :title => "Peugot 206"
|
233
|
-
car2 = Car.create :title => "Peugot 206"
|
234
|
-
|
235
|
-
car.friendly_id #=> "peugot-206"
|
236
|
-
car2.friendly_id #=> "peugot-206-f9f3789a-daec-4156-af1d-fab81aa16ee5"
|
237
|
-
|
238
|
-
Previous versions of FriendlyId appended a numeric sequence a to make slugs
|
239
|
-
unique, but this was removed to simplify using FriendlyId in concurrent code.
|
240
|
-
|
241
|
-
#### Candidates
|
242
|
-
|
243
|
-
Since UUIDs are ugly, FriendlyId provides a "slug candidates" functionality to
|
244
|
-
let you specify alternate slugs to use in the event the one you want to use is
|
245
|
-
already taken. For example:
|
246
|
-
|
247
|
-
class Restaurant < ActiveRecord::Base
|
248
|
-
extend FriendlyId
|
249
|
-
friendly_id :slug_candidates, use: :slugged
|
250
|
-
|
251
|
-
# Try building a slug based on the following fields in
|
252
|
-
# increasing order of specificity.
|
253
|
-
def slug_candidates
|
254
|
-
[
|
255
|
-
:name,
|
256
|
-
[:name, :city],
|
257
|
-
[:name, :street, :city],
|
258
|
-
[:name, :street_number, :street, :city]
|
259
|
-
]
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
r1 = Restaurant.create! name: 'Plaza Diner', city: 'New Paltz'
|
264
|
-
r2 = Restaurant.create! name: 'Plaza Diner', city: 'Kingston'
|
265
|
-
|
266
|
-
r1.friendly_id #=> 'plaza-diner'
|
267
|
-
r2.friendly_id #=> 'plaza-diner-kingston'
|
268
|
-
|
269
|
-
To use candidates, make your FriendlyId base method return an array. The
|
270
|
-
method need not be named `slug_candidates`; it can be anything you want. The
|
271
|
-
array may contain any combination of symbols, strings, procs or lambdas and
|
272
|
-
will be evaluated lazily and in order. If you include symbols, FriendlyId will
|
273
|
-
invoke a method on your model class with the same name. Strings will be
|
274
|
-
interpreted literally. Procs and lambdas will be called and their return values
|
275
|
-
used as the basis of the friendly id. If none of the candidates can generate a
|
276
|
-
unique slug, then FriendlyId will append a UUID to the first candidate as a
|
277
|
-
last resort.
|
278
|
-
|
279
|
-
#### Sequence Separator
|
280
|
-
|
281
|
-
By default, FriendlyId uses a dash to separate the slug from a sequence.
|
282
|
-
|
283
|
-
You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
|
284
|
-
sequence_separator} configuration option.
|
285
|
-
|
286
|
-
#### Providing Your Own Slug Processing Method
|
287
|
-
|
288
|
-
You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
|
289
|
-
total control over the slug format. It will be invoked for any generated slug,
|
290
|
-
whether for a single slug or for slug candidates.
|
291
|
-
|
292
|
-
#### Deciding When to Generate New Slugs
|
293
|
-
|
294
|
-
As of FriendlyId 5.0, slugs are only generated when the `slug` field is nil. If
|
295
|
-
you want a slug to be regenerated,set the slug field to nil:
|
296
|
-
|
297
|
-
restaurant.friendly_id # joes-diner
|
298
|
-
restaurant.name = "The Plaza Diner"
|
299
|
-
restaurant.save!
|
300
|
-
restaurant.friendly_id # joes-diner
|
301
|
-
restaurant.slug = nil
|
302
|
-
restaurant.save!
|
303
|
-
restaurant.friendly_id # the-plaza-diner
|
304
|
-
|
305
|
-
You can also override the
|
306
|
-
{FriendlyId::Slugged#should_generate_new_friendly_id?} method, which lets you
|
307
|
-
control exactly when new friendly ids are set:
|
308
|
-
|
309
|
-
class Post < ActiveRecord::Base
|
310
|
-
extend FriendlyId
|
311
|
-
friendly_id :title, :use => :slugged
|
312
|
-
|
313
|
-
def should_generate_new_friendly_id?
|
314
|
-
title_changed?
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
#### Locale-specific Transliterations
|
319
|
-
|
320
|
-
Active Support's `parameterize` uses
|
321
|
-
[transliterate](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate),
|
322
|
-
which in turn can use I18n's transliteration rules to consider the current
|
323
|
-
locale when replacing Latin characters:
|
324
|
-
|
325
|
-
# config/locales/de.yml
|
326
|
-
de:
|
327
|
-
i18n:
|
328
|
-
transliterate:
|
329
|
-
rule:
|
330
|
-
ü: "ue"
|
331
|
-
ö: "oe"
|
332
|
-
etc...
|
333
|
-
|
334
|
-
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
335
|
-
movie.slug #=> "der-preis-fuers-ueberleben"
|
336
|
-
|
337
|
-
This functionality was in fact taken from earlier versions of FriendlyId.
|
338
|
-
|
339
|
-
#### Gotchas: Common Problems
|
340
|
-
|
341
|
-
FriendlyId uses a before_validation callback to generate and set the slug. This
|
342
|
-
means that if you create two model instances before saving them, it's possible
|
343
|
-
they will generate the same slug, and the second save will fail.
|
344
|
-
|
345
|
-
This can happen in two fairly normal cases: the first, when a model using nested
|
346
|
-
attributes creates more than one record for a model that uses friendly_id. The
|
347
|
-
second, in concurrent code, either in threads or multiple processes.
|
348
|
-
|
349
|
-
To solve the nested attributes issue, I recommend simply avoiding them when
|
350
|
-
creating more than one nested record for a model that uses FriendlyId. See [this
|
351
|
-
Github issue](https://github.com/norman/friendly_id/issues/185) for discussion.
|
352
|
-
|
353
|
-
|
354
|
-
## History: Avoiding 404's When Slugs Change
|
355
|
-
|
356
|
-
FriendlyId's {FriendlyId::History History} module adds the ability to store a
|
357
|
-
log of a model's slugs, so that when its friendly id changes, it's still
|
358
|
-
possible to perform finds by the old id.
|
359
|
-
|
360
|
-
The primary use case for this is avoiding broken URLs.
|
361
|
-
|
362
|
-
### Setup
|
363
|
-
|
364
|
-
In order to use this module, you must add a table to your database schema to
|
365
|
-
store the slug records. FriendlyId provides a generator for this purpose:
|
366
|
-
|
367
|
-
rails generate friendly_id
|
368
|
-
rake db:migrate
|
369
|
-
|
370
|
-
This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
|
371
|
-
model.
|
372
|
-
|
373
|
-
### Considerations
|
374
|
-
|
375
|
-
Because recording slug history requires creating additional database records,
|
376
|
-
this module has an impact on the performance of the associated model's `create`
|
377
|
-
method.
|
378
|
-
|
379
|
-
### Example
|
380
|
-
|
381
|
-
class Post < ActiveRecord::Base
|
382
|
-
extend FriendlyId
|
383
|
-
friendly_id :title, :use => :history
|
384
|
-
end
|
385
|
-
|
386
|
-
class PostsController < ApplicationController
|
387
|
-
|
388
|
-
before_filter :find_post
|
389
|
-
|
390
|
-
...
|
391
|
-
|
392
|
-
def find_post
|
393
|
-
@post = Post.find params[:id]
|
394
|
-
|
395
|
-
# If an old id or a numeric id was used to find the record, then
|
396
|
-
# the request path will not match the post_path, and we should do
|
397
|
-
# a 301 redirect that uses the current friendly id.
|
398
|
-
if request.path != post_path(@post)
|
399
|
-
return redirect_to @post, :status => :moved_permanently
|
400
|
-
end
|
401
|
-
end
|
402
|
-
end
|
403
|
-
|
404
|
-
|
405
|
-
## Unique Slugs by Scope
|
406
|
-
|
407
|
-
The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
|
408
|
-
within a scope.
|
409
|
-
|
410
|
-
This allows, for example, two restaurants in different cities to have the slug
|
411
|
-
`joes-diner`:
|
412
|
-
|
413
|
-
class Restaurant < ActiveRecord::Base
|
414
|
-
extend FriendlyId
|
415
|
-
belongs_to :city
|
416
|
-
friendly_id :name, :use => :scoped, :scope => :city
|
417
|
-
end
|
418
|
-
|
419
|
-
class City < ActiveRecord::Base
|
420
|
-
extend FriendlyId
|
421
|
-
has_many :restaurants
|
422
|
-
friendly_id :name, :use => :slugged
|
423
|
-
end
|
424
|
-
|
425
|
-
City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
|
426
|
-
City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
|
427
|
-
|
428
|
-
Without :scoped in this case, one of the restaurants would have the slug
|
429
|
-
`joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
|
430
|
-
|
431
|
-
The value for the `:scope` option can be the name of a `belongs_to` relation, or
|
432
|
-
a column.
|
433
|
-
|
434
|
-
Additionally, the `:scope` option can receive an array of scope values:
|
435
|
-
|
436
|
-
class Cuisine < ActiveRecord::Base
|
437
|
-
extend FriendlyId
|
438
|
-
has_many :restaurants
|
439
|
-
friendly_id :name, :use => :slugged
|
440
|
-
end
|
441
|
-
|
442
|
-
class City < ActiveRecord::Base
|
443
|
-
extend FriendlyId
|
444
|
-
has_many :restaurants
|
445
|
-
friendly_id :name, :use => :slugged
|
446
|
-
end
|
447
|
-
|
448
|
-
class Restaurant < ActiveRecord::Base
|
449
|
-
extend FriendlyId
|
450
|
-
belongs_to :city
|
451
|
-
friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
|
452
|
-
end
|
453
|
-
|
454
|
-
All supplied values will be used to determine scope.
|
455
|
-
|
456
|
-
### Finding Records by Friendly ID
|
457
|
-
|
458
|
-
If you are using scopes your friendly ids may not be unique, so a simple find
|
459
|
-
like
|
460
|
-
|
461
|
-
Restaurant.friendly.find("joes-diner")
|
462
|
-
|
463
|
-
may return the wrong record. In these cases it's best to query through the
|
464
|
-
relation:
|
465
|
-
|
466
|
-
@city.restaurants.friendly.find("joes-diner")
|
467
|
-
|
468
|
-
Alternatively, you could pass the scope value as a query parameter:
|
469
|
-
|
470
|
-
Restaurant.friendly.find("joes-diner").where(:city_id => @city.id)
|
471
|
-
|
472
|
-
|
473
|
-
### Finding All Records That Match a Scoped ID
|
474
|
-
|
475
|
-
Query the slug column directly:
|
476
|
-
|
477
|
-
Restaurant.where(:slug => "joes-diner")
|
478
|
-
|
479
|
-
### Routes for Scoped Models
|
480
|
-
|
481
|
-
Recall that FriendlyId is a database-centric library, and does not set up any
|
482
|
-
routes for scoped models. You must do this yourself in your application. Here's
|
483
|
-
an example of one way to set this up:
|
484
|
-
|
485
|
-
# in routes.rb
|
486
|
-
resources :cities do
|
487
|
-
resources :restaurants
|
488
|
-
end
|
489
|
-
|
490
|
-
# in views
|
491
|
-
<%= link_to 'Show', [@city, @restaurant] %>
|
492
|
-
|
493
|
-
# in controllers
|
494
|
-
@city = City.friendly.find(params[:city_id])
|
495
|
-
@restaurant = @city.restaurants.friendly.find(params[:id])
|
496
|
-
|
497
|
-
# URLs:
|
498
|
-
http://example.org/cities/seattle/restaurants/joes-diner
|
499
|
-
http://example.org/cities/chicago/restaurants/joes-diner
|
500
|
-
|
501
|
-
|
502
|
-
## Translating Slugs Using Simple I18n
|
503
|
-
|
504
|
-
The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
|
505
|
-
FriendlyId.
|
506
|
-
|
507
|
-
In order to use this module, your model must have a slug column for each locale.
|
508
|
-
By default FriendlyId looks for columns named, for example, "slug_en",
|
509
|
-
"slug_es", etc. The first part of the name can be configured by passing the
|
510
|
-
`:slug_column` option if you choose. Note that the column for the default locale
|
511
|
-
must also include the locale in its name.
|
512
|
-
|
513
|
-
This module is most suitable to applications that need to support few locales.
|
514
|
-
If you need to support two or more locales, you may wish to use the
|
515
|
-
friendly_id_globalize gem instead.
|
516
|
-
|
517
|
-
### Example migration
|
518
|
-
|
519
|
-
def self.up
|
520
|
-
create_table :posts do |t|
|
521
|
-
t.string :title
|
522
|
-
t.string :slug_en
|
523
|
-
t.string :slug_es
|
524
|
-
t.text :body
|
525
|
-
end
|
526
|
-
add_index :posts, :slug_en
|
527
|
-
add_index :posts, :slug_es
|
528
|
-
end
|
529
|
-
|
530
|
-
### Finds
|
531
|
-
|
532
|
-
Finds will take into consideration the current locale:
|
533
|
-
|
534
|
-
I18n.locale = :es
|
535
|
-
Post.find("la-guerra-de-las-galaxas")
|
536
|
-
I18n.locale = :en
|
537
|
-
Post.find("star-wars")
|
538
|
-
|
539
|
-
To find a slug by an explicit locale, perform the find inside a block
|
540
|
-
passed to I18n's `with_locale` method:
|
541
|
-
|
542
|
-
I18n.with_locale(:es) do
|
543
|
-
Post.find("la-guerra-de-las-galaxas")
|
544
|
-
end
|
545
|
-
|
546
|
-
### Creating Records
|
547
|
-
|
548
|
-
When new records are created, the slug is generated for the current locale only.
|
549
|
-
|
550
|
-
### Translating Slugs
|
551
|
-
|
552
|
-
To translate an existing record's friendly_id, use
|
553
|
-
{FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
|
554
|
-
you add is properly escaped, transliterated and sequenced:
|
555
|
-
|
556
|
-
post = Post.create :name => "Star Wars"
|
557
|
-
post.set_friendly_id("La guerra de las galaxas", :es)
|
558
|
-
|
559
|
-
If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
|
560
|
-
current locale:
|
561
|
-
|
562
|
-
I18n.with_locale(:es) do
|
563
|
-
post.set_friendly_id("La guerra de las galaxas")
|
564
|
-
end
|
565
|
-
|
566
|
-
|
567
|
-
## Reserved Words
|
568
|
-
|
569
|
-
The {FriendlyId::Reserved Reserved} module adds the ability to exlude a list of
|
570
|
-
words from use as FriendlyId slugs.
|
571
|
-
|
572
|
-
By default, FriendlyId reserves the words "new" and "edit" when this module is
|
573
|
-
included. You can configure this globally by using {FriendlyId.defaults
|
574
|
-
FriendlyId.defaults}:
|
575
|
-
|
576
|
-
FriendlyId.defaults do |config|
|
577
|
-
config.use :reserved
|
578
|
-
# Reserve words for English and Spanish URLs
|
579
|
-
config.reserved_words = %w(new edit nueva nuevo editar)
|
580
|
-
end
|
581
|
-
|
582
|
-
Note that the error message will appear on the field `:friendly_id`. If you are
|
583
|
-
using Rails's scaffolded form errors display, then it will have no field to
|
584
|
-
highlight. If you'd like to change this so that scaffolding works as expected,
|
585
|
-
one way to accomplish this is to move the error message to a different field.
|
586
|
-
For example:
|
587
|
-
|
588
|
-
class Person < ActiveRecord::Base
|
589
|
-
extend FriendlyId
|
590
|
-
friendly_id :name, use: :slugged
|
591
|
-
|
592
|
-
after_validation :move_friendly_id_error_to_name
|
593
|
-
|
594
|
-
def move_friendly_id_error_to_name
|
595
|
-
errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
|
596
|
-
end
|
597
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
require File.expand_path("../../../helper", __FILE__)
|
2
|
-
|
3
|
-
require "ancestry"
|
4
|
-
|
5
|
-
ActiveRecord::Migration.create_table("things") do |t|
|
6
|
-
t.string :name
|
7
|
-
t.string :slug
|
8
|
-
t.string :ancestry
|
9
|
-
end
|
10
|
-
ActiveRecord::Migration.add_index :things, :ancestry
|
11
|
-
|
12
|
-
class Thing < ActiveRecord::Base
|
13
|
-
extend FriendlyId
|
14
|
-
friendly_id do |config|
|
15
|
-
config.use :slugged
|
16
|
-
config.use :scoped
|
17
|
-
config.base = :name
|
18
|
-
config.scope = :ancestry
|
19
|
-
end
|
20
|
-
has_ancestry
|
21
|
-
end
|
22
|
-
|
23
|
-
class AncestryTest < MiniTest::Unit::TestCase
|
24
|
-
include FriendlyId::Test
|
25
|
-
|
26
|
-
test "should sequence slugs when scoped by ancestry" do
|
27
|
-
3.times.inject([]) do |memo, _|
|
28
|
-
memo << Thing.create!(:name => "a", :parent => memo.last)
|
29
|
-
end.each do |thing|
|
30
|
-
assert_equal "a", thing.friendly_id
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
@@ -1,45 +0,0 @@
|
|
1
|
-
ENV["DB"] = "postgres"
|
2
|
-
|
3
|
-
require "thread"
|
4
|
-
require File.expand_path("../../../helper", __FILE__)
|
5
|
-
require "active_record/locking/fatalistic"
|
6
|
-
|
7
|
-
ActiveRecord::Migration.tap do |m|
|
8
|
-
m.drop_table "things"
|
9
|
-
m.create_table("things") do |t|
|
10
|
-
t.string :name
|
11
|
-
t.string :slug
|
12
|
-
end
|
13
|
-
m.add_index :things, :slug, :unique => true
|
14
|
-
end
|
15
|
-
|
16
|
-
class Thing < ActiveRecord::Base
|
17
|
-
extend FriendlyId
|
18
|
-
friendly_id :name, :use => :slugged
|
19
|
-
end
|
20
|
-
|
21
|
-
$things = 10.times.map do
|
22
|
-
Thing.new :name => "a b c"
|
23
|
-
end
|
24
|
-
|
25
|
-
$mutex = Mutex.new
|
26
|
-
|
27
|
-
def save_thing
|
28
|
-
thing = $mutex.synchronize do
|
29
|
-
$things.pop
|
30
|
-
end
|
31
|
-
if thing.nil? then return end
|
32
|
-
Thing.lock do
|
33
|
-
thing.save!
|
34
|
-
print "#{thing.friendly_id}\n"
|
35
|
-
end
|
36
|
-
true
|
37
|
-
end
|
38
|
-
|
39
|
-
2.times.map do
|
40
|
-
Thread.new do
|
41
|
-
while true do
|
42
|
-
break unless save_thing
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end.map(&:value)
|