friendly_id4 4.0.0.beta4 → 4.0.0.beta5
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 +4 -0
- data/README.md +21 -24
- data/Rakefile +1 -1
- data/friendly_id.gemspec +2 -2
- data/lib/friendly_id.rb +102 -8
- data/lib/friendly_id/base.rb +76 -8
- data/lib/friendly_id/configuration.rb +9 -27
- data/lib/friendly_id/finder_methods.rb +8 -0
- data/lib/friendly_id/history.rb +35 -0
- data/lib/friendly_id/migration.rb +1 -0
- data/lib/friendly_id/object_utils.rb +16 -6
- data/lib/friendly_id/reserved.rb +46 -0
- data/lib/friendly_id/scoped.rb +101 -19
- data/lib/friendly_id/slug.rb +3 -0
- data/lib/friendly_id/slug_sequencer.rb +3 -0
- data/lib/friendly_id/slugged.rb +180 -8
- data/lib/generators/friendly_id_generator.rb +3 -0
- data/test/base_test.rb +23 -0
- data/test/configuration_test.rb +6 -6
- data/test/core_test.rb +3 -11
- data/test/helper.rb +2 -2
- data/test/history_test.rb +9 -9
- data/test/object_utils_test.rb +3 -3
- data/test/reserved_test.rb +26 -0
- data/test/scoped_test.rb +8 -8
- data/test/shared.rb +15 -15
- data/test/slugged_test.rb +30 -20
- metadata +19 -18
- data/Guide.md +0 -363
- data/lib/friendly_id/version.rb +0 -9
@@ -1,7 +1,11 @@
|
|
1
1
|
module FriendlyId
|
2
|
-
# Utility methods
|
3
|
-
#
|
4
|
-
#
|
2
|
+
# Utility methods for determining whether any object is a friendly id.
|
3
|
+
#
|
4
|
+
# Monkey-patching Object is a somewhat extreme measure not to be taken lightly
|
5
|
+
# by libraries, but in this case I decided to do it because to me, it feels
|
6
|
+
# cleaner than adding a module method to {FriendlyId}. I've given the methods
|
7
|
+
# names that unambigously refer to the library of their origin, which should
|
8
|
+
# be sufficient to avoid conflicts with other libraries.
|
5
9
|
module ObjectUtils
|
6
10
|
|
7
11
|
# True is the id is definitely friendly, false if definitely unfriendly,
|
@@ -9,6 +13,14 @@ module FriendlyId
|
|
9
13
|
#
|
10
14
|
# An object is considired "definitely unfriendly" if its class is or
|
11
15
|
# inherits from Numeric, Symbol or ActiveRecord::Base.
|
16
|
+
#
|
17
|
+
# An object is considered "definitely friendly" if it responds to +to_i+,
|
18
|
+
# and its value when cast to an integer and then back to a string is
|
19
|
+
# different from its value when merely cast to a string:
|
20
|
+
#
|
21
|
+
# 123.friendly_id? #=> false
|
22
|
+
# "123".friendly_id? #=> nil
|
23
|
+
# "abc123".friendly_id? #=> true
|
12
24
|
def friendly_id?
|
13
25
|
if [Numeric, Symbol, ActiveRecord::Base].detect {|klass| self.class < klass}
|
14
26
|
false
|
@@ -25,6 +37,4 @@ module FriendlyId
|
|
25
37
|
end
|
26
38
|
end
|
27
39
|
|
28
|
-
|
29
|
-
include FriendlyId::ObjectUtils
|
30
|
-
end
|
40
|
+
Object.send :include, FriendlyId::ObjectUtils
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
=begin
|
4
|
+
This module adds the ability to exlude a list of words from use as
|
5
|
+
FriendlyId slugs.
|
6
|
+
|
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}:
|
9
|
+
|
10
|
+
FriendlyId.defaults do |config|
|
11
|
+
config.use :reserved
|
12
|
+
# Reserve words for English and Spanish URLs
|
13
|
+
config.reserved_words = %w(new edit nueva nuevo editar)
|
14
|
+
end
|
15
|
+
=end
|
16
|
+
module Reserved
|
17
|
+
|
18
|
+
# When included, this module adds configuration options to the model class's
|
19
|
+
# friendly_id_config.
|
20
|
+
def self.included(model_class)
|
21
|
+
model_class.class_eval do
|
22
|
+
friendly_id_config.class.send :include, Reserved::Configuration
|
23
|
+
friendly_id_config.defaults[:reserved_words] ||= ["new", "edit"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# This module adds the +:reserved_words+ configuration option to
|
28
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
29
|
+
module Configuration
|
30
|
+
attr_writer :reserved_words
|
31
|
+
|
32
|
+
# Overrides {FriendlyId::Configuration#base} to add a validation to the
|
33
|
+
# model class.
|
34
|
+
def base=(base)
|
35
|
+
super
|
36
|
+
reserved_words = model_class.friendly_id_config.reserved_words
|
37
|
+
model_class.validates_exclusion_of base, :in => reserved_words
|
38
|
+
end
|
39
|
+
|
40
|
+
# An array of words forbidden as slugs.
|
41
|
+
def reserved_words
|
42
|
+
@reserved_words ||= @defaults[:reserved_words]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/friendly_id/scoped.rb
CHANGED
@@ -2,21 +2,88 @@ require "friendly_id/slugged"
|
|
2
2
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
=begin
|
6
|
+
This module allows FriendlyId to generate unique slugs within a scope.
|
7
|
+
|
8
|
+
This allows, for example, two restaurants in different cities to have the slug
|
9
|
+
+joes-diner+:
|
10
|
+
|
11
|
+
class Restaurant < ActiveRecord::Base
|
12
|
+
extend FriendlyId
|
13
|
+
belongs_to :city
|
14
|
+
friendly_id :name, :use => :scoped, :scope => :city
|
15
|
+
end
|
16
|
+
|
17
|
+
class City < ActiveRecord::Base
|
18
|
+
extend FriendlyId
|
19
|
+
has_many :restaurants
|
20
|
+
friendly_id :name, :use => :slugged
|
21
|
+
end
|
22
|
+
|
23
|
+
City.find("seattle").restaurants.find("joes-diner")
|
24
|
+
City.find("chicago").restaurants.find("joes-diner")
|
25
|
+
|
26
|
+
Without :scoped in this case, one of the restaurants would have the slug
|
27
|
+
+joes-diner+ and the other would have +joes-diner--2+.
|
28
|
+
|
29
|
+
The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
|
30
|
+
a column.
|
31
|
+
|
32
|
+
== Tips For Working With Scoped Slugs
|
33
|
+
|
34
|
+
=== Finding Records by Friendly ID
|
35
|
+
|
36
|
+
If you are using scopes your friendly ids may not be unique, so a simple find
|
37
|
+
like
|
38
|
+
|
39
|
+
Restaurant.find("joes-diner")
|
40
|
+
|
41
|
+
may return the wrong record. In these cases it's best to query through the
|
42
|
+
relation:
|
43
|
+
|
44
|
+
@city.restaurants.find("joes-diner")
|
45
|
+
|
46
|
+
Alternatively, you could pass the scope value as a query parameter:
|
47
|
+
|
48
|
+
Restaurant.find("joes-diner").where(:city_id => @city.id)
|
49
|
+
|
50
|
+
|
51
|
+
=== Finding All Records That Match a Scoped ID
|
52
|
+
|
53
|
+
Query the slug column directly:
|
54
|
+
|
55
|
+
Restaurant.find_all_by_slug("joes-diner")
|
56
|
+
|
57
|
+
=== Routes for Scoped Models
|
58
|
+
|
59
|
+
Recall that FriendlyId is a database-centric library, and does not set up any
|
60
|
+
routes for scoped models. You must do this yourself in your application. Here's
|
61
|
+
an example of one way to set this up:
|
62
|
+
|
63
|
+
# in routes.rb
|
64
|
+
resources :cities do
|
65
|
+
resources :restaurants
|
66
|
+
end
|
67
|
+
|
68
|
+
# in views
|
69
|
+
<%= link_to 'Show', [@city, @restaurant] %>
|
70
|
+
|
71
|
+
# in controllers
|
72
|
+
@city = City.find(params[:city_id])
|
73
|
+
@restaurant = @city.restaurants.find(params[:id])
|
74
|
+
|
75
|
+
# URLs:
|
76
|
+
http://example.org/cities/seattle/restaurants/joes-diner
|
77
|
+
http://example.org/cities/chicago/restaurants/joes-diner
|
78
|
+
|
79
|
+
=end
|
17
80
|
module Scoped
|
18
|
-
|
19
|
-
|
81
|
+
|
82
|
+
|
83
|
+
# Sets up behavior and configuration options for FriendlyId's scoped slugs
|
84
|
+
# feature.
|
85
|
+
def self.included(model_class)
|
86
|
+
model_class.instance_eval do
|
20
87
|
raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
|
21
88
|
include Slugged unless self < Slugged
|
22
89
|
friendly_id_config.class.send :include, Configuration
|
@@ -24,22 +91,37 @@ module FriendlyId
|
|
24
91
|
end
|
25
92
|
end
|
26
93
|
|
94
|
+
# This module adds the +:scope+ configuration option to
|
95
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
27
96
|
module Configuration
|
97
|
+
|
98
|
+
# Gets the scope value.
|
99
|
+
#
|
100
|
+
# When setting this value, the argument should be a symbol referencing a
|
101
|
+
# +belongs_to+ relation, or a column.
|
102
|
+
#
|
103
|
+
# @return Symbol The scope value
|
28
104
|
attr_accessor :scope
|
29
105
|
|
30
106
|
# Gets the scope column.
|
31
107
|
#
|
32
|
-
# Checks to see if the +:scope+ option passed to
|
33
|
-
# refers to a relation, and if so, returns
|
34
|
-
# Otherwise it assumes the option value was
|
35
|
-
# it cast to a String.
|
108
|
+
# Checks to see if the +:scope+ option passed to
|
109
|
+
# {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
|
110
|
+
# the realtion's foreign key. Otherwise it assumes the option value was
|
111
|
+
# the name of column and returns it cast to a String.
|
112
|
+
#
|
36
113
|
# @return String The scope column
|
37
114
|
def scope_column
|
38
|
-
(
|
115
|
+
(model_class.reflections[@scope].try(:association_foreign_key) || @scope).to_s
|
39
116
|
end
|
40
117
|
end
|
41
118
|
|
119
|
+
# This module overrides {FriendlyId::SlugSequencer#conflict} to consider
|
120
|
+
# scope, to avoid adding sequences to slugs under different scopes.
|
42
121
|
module SlugSequencer
|
122
|
+
|
123
|
+
private
|
124
|
+
|
43
125
|
def conflict
|
44
126
|
column = friendly_id_config.scope_column
|
45
127
|
conflicts.where("#{column} = ?", sluggable.send(column)).first
|
data/lib/friendly_id/slug.rb
CHANGED
@@ -8,12 +8,14 @@ module FriendlyId
|
|
8
8
|
@sluggable = sluggable
|
9
9
|
end
|
10
10
|
|
11
|
+
# Given a slug, get the next available slug in the sequence.
|
11
12
|
def next
|
12
13
|
sequence = conflict.slug.split(separator)[1].to_i
|
13
14
|
next_sequence = sequence == 0 ? 2 : sequence.next
|
14
15
|
"#{normalized}#{separator}#{next_sequence}"
|
15
16
|
end
|
16
17
|
|
18
|
+
# Generate a new sequenced slug.
|
17
19
|
def generate
|
18
20
|
if new_record? or slug_changed?
|
19
21
|
conflict? ? self.next : normalized
|
@@ -22,6 +24,7 @@ module FriendlyId
|
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
27
|
+
# Whether or not the model instance's slug has changed.
|
25
28
|
def slug_changed?
|
26
29
|
separator = Regexp.escape friendly_id_config.sequence_separator
|
27
30
|
base != sluggable.current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
|
data/lib/friendly_id/slugged.rb
CHANGED
@@ -1,46 +1,218 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
require "friendly_id/slug_sequencer"
|
2
3
|
|
3
4
|
module FriendlyId
|
5
|
+
=begin
|
6
|
+
This module adds in-table slugs to a model.
|
4
7
|
|
5
|
-
|
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:
|
12
|
+
|
13
|
+
"Gone With The Wind" -> "gone-with-the-wind"
|
14
|
+
|
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
|
18
|
+
{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.
|
21
|
+
|
22
|
+
=== Example Setup
|
23
|
+
|
24
|
+
# your model
|
25
|
+
class Post < ActiveRecord::Base
|
26
|
+
extend FriendlyId
|
27
|
+
friendly_id :title, :use => :slugged
|
28
|
+
validates_presence_of :title, :slug, :body
|
29
|
+
end
|
30
|
+
|
31
|
+
# a migration
|
32
|
+
class CreatePosts < ActiveRecord::Migration
|
33
|
+
def self.up
|
34
|
+
create_table :posts do |t|
|
35
|
+
t.string :title, :null => false
|
36
|
+
t.string :slug, :null => false
|
37
|
+
t.text :body
|
38
|
+
end
|
39
|
+
|
40
|
+
add_index :posts, :slug, :unique => true
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.down
|
44
|
+
drop_table :posts
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
=== Slug Format
|
49
|
+
|
50
|
+
By default, FriendlyId uses Active Support's
|
51
|
+
paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
|
52
|
+
method to create slugs. This method will intelligently replace spaces with
|
53
|
+
dashes, and Unicode Latin characters with ASCII approximations:
|
54
|
+
|
55
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
56
|
+
movie.slug #=> "der-preis-furs-uberleben"
|
57
|
+
|
58
|
+
==== Slug Uniqueness
|
59
|
+
|
60
|
+
When you try to insert a record that would generate a duplicate friendly id,
|
61
|
+
FriendlyId will append a sequence to the generated slug to ensure uniqueness:
|
62
|
+
|
63
|
+
car = Car.create :title => "Peugot 206"
|
64
|
+
car2 = Car.create :title => "Peugot 206"
|
65
|
+
|
66
|
+
car.friendly_id #=> "peugot-206"
|
67
|
+
car2.friendly_id #=> "peugot-206--2"
|
68
|
+
|
69
|
+
==== Changing the Slug Sequence Separator
|
70
|
+
|
71
|
+
You can do this with the {Slugged::Configuration#sequence_separator
|
72
|
+
sequence_separator} configuration option.
|
73
|
+
|
74
|
+
==== Column or Method?
|
75
|
+
|
76
|
+
FriendlyId always uses a method as the basis of the slug text - not a column. It
|
77
|
+
first glance, this may sound confusing, but remember that Active Record provides
|
78
|
+
methods for each column in a model's associated table, and that's what
|
79
|
+
FriendlyId uses.
|
80
|
+
|
81
|
+
Here's an example of a class that uses a custom method to generate the slug:
|
82
|
+
|
83
|
+
class Person < ActiveRecord::Base
|
84
|
+
friendly_id :name_and_location
|
85
|
+
def name_and_location
|
86
|
+
"#{name} from #{location}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
91
|
+
bob.friendly_id #=> "bob-smith-from-new-york-city"
|
92
|
+
|
93
|
+
==== Providing Your Own Slug Processing Method
|
94
|
+
|
95
|
+
You can override {Slugged#normalize_friendly_id} in your model for total
|
96
|
+
control over the slug format.
|
97
|
+
|
98
|
+
==== Locale-specific Transliterations
|
99
|
+
|
100
|
+
Active Support's +parameterize+ uses
|
101
|
+
transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
|
102
|
+
which in turn can use I18n's transliteration rules to consider the current
|
103
|
+
locale when replacing Latin characters:
|
104
|
+
|
105
|
+
# config/locales/de.yml
|
106
|
+
de:
|
107
|
+
i18n:
|
108
|
+
transliterate:
|
109
|
+
rule:
|
110
|
+
ü: "ue"
|
111
|
+
ö: "oe"
|
112
|
+
etc...
|
113
|
+
|
114
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
115
|
+
movie.slug #=> "der-preis-fuers-ueberleben"
|
116
|
+
|
117
|
+
This functionality was in fact taken from earlier versions of FriendlyId.
|
118
|
+
=end
|
6
119
|
module Slugged
|
7
120
|
|
8
|
-
|
9
|
-
|
121
|
+
# Sets up behavior and configuration options for FriendlyId's slugging
|
122
|
+
# feature.
|
123
|
+
def self.included(model_class)
|
124
|
+
model_class.instance_eval do
|
10
125
|
friendly_id_config.class.send :include, Configuration
|
11
|
-
friendly_id_config.defaults[:slug_column]
|
12
|
-
friendly_id_config.defaults[:sequence_separator]
|
13
|
-
friendly_id_config.slug_sequencer_class
|
126
|
+
friendly_id_config.defaults[:slug_column] ||= 'slug'
|
127
|
+
friendly_id_config.defaults[:sequence_separator] ||= '--'
|
128
|
+
friendly_id_config.slug_sequencer_class ||= Class.new(SlugSequencer)
|
14
129
|
before_validation :set_slug
|
15
130
|
end
|
16
131
|
end
|
17
132
|
|
133
|
+
# Process the given value to make it suitable for use as a slug.
|
134
|
+
#
|
135
|
+
# This method is not intended to be invoked directly; FriendlyId uses it
|
136
|
+
# internaly to process strings into slugs.
|
137
|
+
#
|
138
|
+
# However, if FriendlyId's default slug generation doesn't suite your needs,
|
139
|
+
# you can override this method in your model class to control exactly how
|
140
|
+
# slugs are generated.
|
141
|
+
#
|
142
|
+
# === Example
|
143
|
+
#
|
144
|
+
# class Person < ActiveRecord::Base
|
145
|
+
# friendly_id :name_and_location
|
146
|
+
#
|
147
|
+
# def name_and_location
|
148
|
+
# "#{name} from #{location}"
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# # Use default slug, but uupper case and with underscores
|
152
|
+
# def normalize_friendly_id(string)
|
153
|
+
# super.upcase.gsub("-", "_")
|
154
|
+
# end
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
158
|
+
# bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
|
159
|
+
#
|
160
|
+
# === More Resources
|
161
|
+
#
|
162
|
+
# You might want to look into Babosa[https://github.com/norman/babosa],
|
163
|
+
# which is the slugging library used by FriendlyId prior to version 4, which
|
164
|
+
# offers some specialized functionality missing from Active Support.
|
165
|
+
#
|
166
|
+
# @param [#to_s] value The value used as the basis of the slug.
|
167
|
+
# @return The candidate slug text, without a sequence.
|
18
168
|
def normalize_friendly_id(value)
|
19
169
|
value.to_s.parameterize
|
20
170
|
end
|
21
171
|
|
172
|
+
# Gets a new instance of the configured slug sequencing class.
|
173
|
+
#
|
174
|
+
# @see FriendlyId::SlugSequencer
|
22
175
|
def slug_sequencer
|
23
176
|
friendly_id_config.slug_sequencer_class.new(self)
|
24
177
|
end
|
25
178
|
|
26
|
-
|
27
|
-
|
179
|
+
# Sets the slug.
|
28
180
|
def set_slug
|
29
181
|
send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
|
30
182
|
end
|
183
|
+
private :set_slug
|
31
184
|
|
185
|
+
# This module adds the +:slug_column+, and +:sequence_separator+, and
|
186
|
+
# +:slug_sequencer_class+ configuration options to
|
187
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
32
188
|
module Configuration
|
33
189
|
attr_writer :slug_column, :sequence_separator
|
34
190
|
attr_accessor :slug_sequencer_class
|
35
191
|
|
192
|
+
# Makes FriendlyId use the slug column for querying.
|
193
|
+
# @return String The slug column.
|
36
194
|
def query_field
|
37
195
|
slug_column
|
38
196
|
end
|
39
197
|
|
198
|
+
# The string used to separate a slug base from a numeric sequence.
|
199
|
+
#
|
200
|
+
# By default, +--+ is used to separate the slug from the sequence.
|
201
|
+
# FriendlyId uses two dashes to distinguish sequences from slugs with
|
202
|
+
# numbers in their name.
|
203
|
+
#
|
204
|
+
# You can change the default separator by setting the
|
205
|
+
# {FriendlyId::Slugged::Configuration#sequence_separator
|
206
|
+
# sequence_separator} configuration option.
|
207
|
+
#
|
208
|
+
# For obvious reasons, you should avoid setting it to "+-+" unless you're
|
209
|
+
# sure you will never want to have a friendly id with a number in it.
|
210
|
+
# @return String The sequence separator string. Defaults to "+--+".
|
40
211
|
def sequence_separator
|
41
212
|
@sequence_separator or defaults[:sequence_separator]
|
42
213
|
end
|
43
214
|
|
215
|
+
# The column that will be used to store the generated slug.
|
44
216
|
def slug_column
|
45
217
|
@slug_column or defaults[:slug_column]
|
46
218
|
end
|