mil_friendly_id 4.0.9.8
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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +12 -0
- data/.travis.yml +20 -0
- data/.yardopts +4 -0
- data/Changelog.md +86 -0
- data/Gemfile +15 -0
- data/Guide.rdoc +553 -0
- data/MIT-LICENSE +19 -0
- data/README.md +150 -0
- data/Rakefile +108 -0
- data/WhatsNew.md +95 -0
- data/bench.rb +63 -0
- data/friendly_id.gemspec +43 -0
- data/gemfiles/Gemfile.rails-3.0.rb +21 -0
- data/gemfiles/Gemfile.rails-3.1.rb +22 -0
- data/gemfiles/Gemfile.rails-3.2.rb +22 -0
- data/geothird_friendly_id.gemspec +45 -0
- data/lib/friendly_id.rb +114 -0
- data/lib/friendly_id/base.rb +291 -0
- data/lib/friendly_id/configuration.rb +80 -0
- data/lib/friendly_id/finder_methods.rb +35 -0
- data/lib/friendly_id/globalize.rb +115 -0
- data/lib/friendly_id/history.rb +134 -0
- data/lib/friendly_id/migration.rb +19 -0
- data/lib/friendly_id/object_utils.rb +50 -0
- data/lib/friendly_id/reserved.rb +68 -0
- data/lib/friendly_id/scoped.rb +149 -0
- data/lib/friendly_id/simple_i18n.rb +95 -0
- data/lib/friendly_id/slug.rb +14 -0
- data/lib/friendly_id/slug_generator.rb +80 -0
- data/lib/friendly_id/slugged.rb +329 -0
- data/lib/generators/friendly_id_generator.rb +17 -0
- data/mil_friendly_id.gemspec +45 -0
- data/test/base_test.rb +72 -0
- data/test/compatibility/ancestry/Gemfile +8 -0
- data/test/compatibility/ancestry/ancestry_test.rb +34 -0
- data/test/compatibility/threading/Gemfile +8 -0
- data/test/compatibility/threading/threading.rb +45 -0
- data/test/configuration_test.rb +48 -0
- data/test/core_test.rb +48 -0
- data/test/databases.yml +19 -0
- data/test/generator_test.rb +20 -0
- data/test/globalize_test.rb +57 -0
- data/test/helper.rb +87 -0
- data/test/history_test.rb +149 -0
- data/test/object_utils_test.rb +28 -0
- data/test/reserved_test.rb +40 -0
- data/test/schema.rb +79 -0
- data/test/scoped_test.rb +83 -0
- data/test/shared.rb +156 -0
- data/test/simple_i18n_test.rb +133 -0
- data/test/slugged_test.rb +280 -0
- data/test/sti_test.rb +77 -0
- metadata +262 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
require "friendly_id/slugged"
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
|
5
|
+
=begin
|
6
|
+
|
7
|
+
== Unique Slugs by Scope
|
8
|
+
|
9
|
+
The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
|
10
|
+
within a scope.
|
11
|
+
|
12
|
+
This allows, for example, two restaurants in different cities to have the slug
|
13
|
+
+joes-diner+:
|
14
|
+
|
15
|
+
class Restaurant < ActiveRecord::Base
|
16
|
+
extend FriendlyId
|
17
|
+
belongs_to :city
|
18
|
+
friendly_id :name, :use => :scoped, :scope => :city
|
19
|
+
end
|
20
|
+
|
21
|
+
class City < ActiveRecord::Base
|
22
|
+
extend FriendlyId
|
23
|
+
has_many :restaurants
|
24
|
+
friendly_id :name, :use => :slugged
|
25
|
+
end
|
26
|
+
|
27
|
+
City.find("seattle").restaurants.find("joes-diner")
|
28
|
+
City.find("chicago").restaurants.find("joes-diner")
|
29
|
+
|
30
|
+
Without :scoped in this case, one of the restaurants would have the slug
|
31
|
+
+joes-diner+ and the other would have +joes-diner--2+.
|
32
|
+
|
33
|
+
The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
|
34
|
+
a column.
|
35
|
+
|
36
|
+
=== Finding Records by Friendly ID
|
37
|
+
|
38
|
+
If you are using scopes your friendly ids may not be unique, so a simple find
|
39
|
+
like
|
40
|
+
|
41
|
+
Restaurant.find("joes-diner")
|
42
|
+
|
43
|
+
may return the wrong record. In these cases it's best to query through the
|
44
|
+
relation:
|
45
|
+
|
46
|
+
@city.restaurants.find("joes-diner")
|
47
|
+
|
48
|
+
Alternatively, you could pass the scope value as a query parameter:
|
49
|
+
|
50
|
+
Restaurant.find("joes-diner").where(:city_id => @city.id)
|
51
|
+
|
52
|
+
|
53
|
+
=== Finding All Records That Match a Scoped ID
|
54
|
+
|
55
|
+
Query the slug column directly:
|
56
|
+
|
57
|
+
Restaurant.find_all_by_slug("joes-diner")
|
58
|
+
|
59
|
+
=== Routes for Scoped Models
|
60
|
+
|
61
|
+
Recall that FriendlyId is a database-centric library, and does not set up any
|
62
|
+
routes for scoped models. You must do this yourself in your application. Here's
|
63
|
+
an example of one way to set this up:
|
64
|
+
|
65
|
+
# in routes.rb
|
66
|
+
resources :cities do
|
67
|
+
resources :restaurants
|
68
|
+
end
|
69
|
+
|
70
|
+
# in views
|
71
|
+
<%= link_to 'Show', [@city, @restaurant] %>
|
72
|
+
|
73
|
+
# in controllers
|
74
|
+
@city = City.find(params[:city_id])
|
75
|
+
@restaurant = @city.restaurants.find(params[:id])
|
76
|
+
|
77
|
+
# URLs:
|
78
|
+
http://example.org/cities/seattle/restaurants/joes-diner
|
79
|
+
http://example.org/cities/chicago/restaurants/joes-diner
|
80
|
+
|
81
|
+
=end
|
82
|
+
module Scoped
|
83
|
+
|
84
|
+
|
85
|
+
# Sets up behavior and configuration options for FriendlyId's scoped slugs
|
86
|
+
# feature.
|
87
|
+
def self.included(model_class)
|
88
|
+
model_class.instance_eval do
|
89
|
+
raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
|
90
|
+
include Slugged unless self < Slugged
|
91
|
+
friendly_id_config.class.send :include, Configuration
|
92
|
+
friendly_id_config.slug_generator_class.send :include, SlugGenerator
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# This module adds the +:scope+ configuration option to
|
97
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
98
|
+
module Configuration
|
99
|
+
|
100
|
+
# Gets the scope value.
|
101
|
+
#
|
102
|
+
# When setting this value, the argument should be a symbol referencing a
|
103
|
+
# +belongs_to+ relation, or a column.
|
104
|
+
#
|
105
|
+
# @return Symbol The scope value
|
106
|
+
attr_accessor :scope
|
107
|
+
|
108
|
+
# Gets the scope columns.
|
109
|
+
#
|
110
|
+
# Checks to see if the +:scope+ option passed to
|
111
|
+
# {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
|
112
|
+
# the realtion's foreign key. Otherwise it assumes the option value was
|
113
|
+
# the name of column and returns it cast to a String.
|
114
|
+
#
|
115
|
+
# @return String The scope column
|
116
|
+
def scope_columns
|
117
|
+
[@scope].flatten.map { |s| (reflection_foreign_key(s) or s).to_s }
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
if ActiveRecord::VERSION::STRING < "3.1"
|
123
|
+
def reflection_foreign_key(scope)
|
124
|
+
model_class.reflections[scope].try(:primary_key_name)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
def reflection_foreign_key(scope)
|
128
|
+
model_class.reflections[scope].try(:foreign_key)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# This module overrides {FriendlyId::SlugGenerator#conflict} to consider
|
134
|
+
# scope, to avoid adding sequences to slugs under different scopes.
|
135
|
+
module SlugGenerator
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def conflict
|
140
|
+
columns = friendly_id_config.scope_columns
|
141
|
+
matched = columns.inject(conflicts) do |memo, column|
|
142
|
+
memo.with_deleted.where(column => sluggable.send(column))
|
143
|
+
end
|
144
|
+
|
145
|
+
matched.first
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "i18n"
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
|
5
|
+
=begin
|
6
|
+
|
7
|
+
== Translating Slugs Using Simple I18n
|
8
|
+
|
9
|
+
The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
|
10
|
+
FriendlyId.
|
11
|
+
|
12
|
+
In order to use this module, your model must have a slug column for each locale.
|
13
|
+
By default FriendlyId looks for columns named, for example, "slug_en",
|
14
|
+
"slug_es", etc. The first part of the name can be configured by passing the
|
15
|
+
+:slug_column+ option if you choose. Note that the column for the default locale
|
16
|
+
must also include the locale in its name.
|
17
|
+
|
18
|
+
This module is most suitable to applications that need to support few locales.
|
19
|
+
If you need to support two or more locales, you may wish to use the
|
20
|
+
{FriendlyId::Globalize Globalize} module instead.
|
21
|
+
|
22
|
+
=== Example migration
|
23
|
+
|
24
|
+
def self.up
|
25
|
+
create_table :posts do |t|
|
26
|
+
t.string :title
|
27
|
+
t.string :slug_en
|
28
|
+
t.string :slug_es
|
29
|
+
t.text :body
|
30
|
+
end
|
31
|
+
add_index :posts, :slug_en
|
32
|
+
add_index :posts, :slug_es
|
33
|
+
end
|
34
|
+
|
35
|
+
=== Finds
|
36
|
+
|
37
|
+
Finds will take into consideration the current locale:
|
38
|
+
|
39
|
+
I18n.locale = :es
|
40
|
+
Post.find("la-guerra-de-las-galaxas")
|
41
|
+
I18n.locale = :en
|
42
|
+
Post.find("star-wars")
|
43
|
+
|
44
|
+
To find a slug by an explicit locale, perform the find inside a block
|
45
|
+
passed to I18n's +with_locale+ method:
|
46
|
+
|
47
|
+
I18n.with_locale(:es) do
|
48
|
+
Post.find("la-guerra-de-las-galaxas")
|
49
|
+
end
|
50
|
+
|
51
|
+
=== Creating Records
|
52
|
+
|
53
|
+
When new records are created, the slug is generated for the current locale only.
|
54
|
+
|
55
|
+
=== Translating Slugs
|
56
|
+
|
57
|
+
To translate an existing record's friendly_id, use
|
58
|
+
{FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
|
59
|
+
you add is properly escaped, transliterated and sequenced:
|
60
|
+
|
61
|
+
post = Post.create :name => "Star Wars"
|
62
|
+
post.set_friendly_id("La guerra de las galaxas", :es)
|
63
|
+
|
64
|
+
If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
|
65
|
+
current locale:
|
66
|
+
|
67
|
+
I18n.with_locale(:es) do
|
68
|
+
post.set_friendly_id("La guerra de las galaxas")
|
69
|
+
end
|
70
|
+
=end
|
71
|
+
module SimpleI18n
|
72
|
+
|
73
|
+
def self.included(model_class)
|
74
|
+
model_class.instance_eval do
|
75
|
+
friendly_id_config.use :slugged
|
76
|
+
friendly_id_config.class.send :include, Configuration
|
77
|
+
include Model
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module Model
|
82
|
+
def set_friendly_id(text, locale = nil)
|
83
|
+
I18n.with_locale(locale || I18n.locale) do
|
84
|
+
set_slug(normalize_friendly_id(text))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module Configuration
|
90
|
+
def slug_column
|
91
|
+
"#{super}_#{I18n.locale}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# A FriendlyId slug stored in an external table.
|
3
|
+
#
|
4
|
+
# @see FriendlyId::History
|
5
|
+
class Slug < ActiveRecord::Base
|
6
|
+
acts_as_paranoid column_type: 'boolean'
|
7
|
+
belongs_to :sluggable, :polymorphic => true
|
8
|
+
|
9
|
+
def to_param
|
10
|
+
slug
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# The default slug generator offers functionality to check slug strings for
|
3
|
+
# uniqueness and, if necessary, appends a sequence to guarantee it.
|
4
|
+
class SlugGenerator
|
5
|
+
attr_reader :sluggable, :normalized
|
6
|
+
|
7
|
+
# Create a new slug generator.
|
8
|
+
def initialize(sluggable, normalized)
|
9
|
+
@sluggable = sluggable
|
10
|
+
@normalized = normalized
|
11
|
+
end
|
12
|
+
|
13
|
+
# Given a slug, get the next available slug in the sequence.
|
14
|
+
def next
|
15
|
+
"#{normalized}#{separator}#{next_in_sequence}"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generate a new sequenced slug.
|
19
|
+
def generate
|
20
|
+
conflict? ? self.next : normalized
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def next_in_sequence
|
26
|
+
last_in_sequence == 0 ? 2 : last_in_sequence.next
|
27
|
+
end
|
28
|
+
|
29
|
+
def last_in_sequence
|
30
|
+
@_last_in_sequence ||= extract_sequence_from_slug(conflict.to_param)
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_sequence_from_slug(slug)
|
34
|
+
slug.split("#{normalized}#{separator}").last.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def column
|
38
|
+
sluggable.connection.quote_column_name friendly_id_config.slug_column
|
39
|
+
end
|
40
|
+
|
41
|
+
def conflict?
|
42
|
+
!! conflict
|
43
|
+
end
|
44
|
+
|
45
|
+
def conflict
|
46
|
+
unless defined? @conflict
|
47
|
+
@conflict = conflicts.first
|
48
|
+
end
|
49
|
+
@conflict
|
50
|
+
end
|
51
|
+
|
52
|
+
def conflicts
|
53
|
+
sluggable_class = friendly_id_config.model_class.base_class
|
54
|
+
|
55
|
+
pkey = sluggable_class.primary_key
|
56
|
+
value = sluggable.send pkey
|
57
|
+
base = "#{column} = ? OR #{column} LIKE ?"
|
58
|
+
# Awful hack for SQLite3, which does not pick up '\' as the escape character without this.
|
59
|
+
base << "ESCAPE '\\'" if sluggable.connection.adapter_name =~ /sqlite/i
|
60
|
+
scope = sluggable_class.unscoped.with_deleted.where(base, normalized, wildcard)
|
61
|
+
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
|
62
|
+
scope = scope.order("LENGTH(#{column}) DESC, #{column} DESC")
|
63
|
+
end
|
64
|
+
|
65
|
+
def friendly_id_config
|
66
|
+
sluggable.friendly_id_config
|
67
|
+
end
|
68
|
+
|
69
|
+
def separator
|
70
|
+
friendly_id_config.sequence_separator
|
71
|
+
end
|
72
|
+
|
73
|
+
def wildcard
|
74
|
+
# Underscores (matching a single character) and percent signs (matching
|
75
|
+
# any number of characters) need to be escaped
|
76
|
+
# (While this seems like an excessive number of backslashes, it is correct)
|
77
|
+
"#{normalized}#{separator}".gsub(/[_%]/, '\\\\\&') + '%'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "friendly_id/slug_generator"
|
3
|
+
|
4
|
+
module FriendlyId
|
5
|
+
=begin
|
6
|
+
|
7
|
+
== Slugged Models
|
8
|
+
|
9
|
+
FriendlyId can use a separate column to store slugs for models which require
|
10
|
+
some text processing.
|
11
|
+
|
12
|
+
For example, blog applications typically use a post title to provide the basis
|
13
|
+
of a search engine friendly URL. Such identifiers typically lack uppercase
|
14
|
+
characters, use ASCII to approximate UTF-8 character, and strip out other
|
15
|
+
characters which may make them aesthetically unappealing or error-prone when
|
16
|
+
used in a URL.
|
17
|
+
|
18
|
+
class Post < ActiveRecord::Base
|
19
|
+
extend FriendlyId
|
20
|
+
friendly_id :title, :use => :slugged
|
21
|
+
end
|
22
|
+
|
23
|
+
@post = Post.create(:title => "This is the first post!")
|
24
|
+
@post.friendly_id # returns "this-is-the-first-post"
|
25
|
+
redirect_to @post # the URL will be /posts/this-is-the-first-post
|
26
|
+
|
27
|
+
In general, use slugs by default unless you know for sure you don't need them.
|
28
|
+
To activate the slugging functionality, use the {FriendlyId::Slugged} module.
|
29
|
+
|
30
|
+
FriendlyId will generate slugs from a method or column that you specify, and
|
31
|
+
store them in a field in your model. By default, this field must be named
|
32
|
+
+:slug+, though you may change this using the
|
33
|
+
{FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
|
34
|
+
option. You should add an index to this column, and in most cases, make it
|
35
|
+
unique. You may also wish to constrain it to NOT NULL, but this depends on your
|
36
|
+
app's behavior and requirements.
|
37
|
+
|
38
|
+
=== Example Setup
|
39
|
+
|
40
|
+
# your model
|
41
|
+
class Post < ActiveRecord::Base
|
42
|
+
extend FriendlyId
|
43
|
+
friendly_id :title, :use => :slugged
|
44
|
+
validates_presence_of :title, :slug, :body
|
45
|
+
end
|
46
|
+
|
47
|
+
# a migration
|
48
|
+
class CreatePosts < ActiveRecord::Migration
|
49
|
+
def self.up
|
50
|
+
create_table :posts do |t|
|
51
|
+
t.string :title, :null => false
|
52
|
+
t.string :slug, :null => false
|
53
|
+
t.text :body
|
54
|
+
end
|
55
|
+
|
56
|
+
add_index :posts, :slug, :unique => true
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.down
|
60
|
+
drop_table :posts
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
=== Working With Slugs
|
65
|
+
|
66
|
+
==== Formatting
|
67
|
+
|
68
|
+
By default, FriendlyId uses Active Support's
|
69
|
+
paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
|
70
|
+
method to create slugs. This method will intelligently replace spaces with
|
71
|
+
dashes, and Unicode Latin characters with ASCII approximations:
|
72
|
+
|
73
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
74
|
+
movie.slug #=> "der-preis-furs-uberleben"
|
75
|
+
|
76
|
+
==== Uniqueness
|
77
|
+
|
78
|
+
When you try to insert a record that would generate a duplicate friendly id,
|
79
|
+
FriendlyId will append a sequence to the generated slug to ensure uniqueness:
|
80
|
+
|
81
|
+
car = Car.create :title => "Peugot 206"
|
82
|
+
car2 = Car.create :title => "Peugot 206"
|
83
|
+
|
84
|
+
car.friendly_id #=> "peugot-206"
|
85
|
+
car2.friendly_id #=> "peugot-206--2"
|
86
|
+
|
87
|
+
==== Sequence Separator - The Two Dashes
|
88
|
+
|
89
|
+
By default, FriendlyId uses two dashes to separate the slug from a sequence.
|
90
|
+
|
91
|
+
You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
|
92
|
+
sequence_separator} configuration option.
|
93
|
+
|
94
|
+
==== Column or Method?
|
95
|
+
|
96
|
+
FriendlyId always uses a method as the basis of the slug text - not a column. It
|
97
|
+
first glance, this may sound confusing, but remember that Active Record provides
|
98
|
+
methods for each column in a model's associated table, and that's what
|
99
|
+
FriendlyId uses.
|
100
|
+
|
101
|
+
Here's an example of a class that uses a custom method to generate the slug:
|
102
|
+
|
103
|
+
class Person < ActiveRecord::Base
|
104
|
+
friendly_id :name_and_location
|
105
|
+
def name_and_location
|
106
|
+
"#{name} from #{location}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
111
|
+
bob.friendly_id #=> "bob-smith-from-new-york-city"
|
112
|
+
|
113
|
+
==== Providing Your Own Slug Processing Method
|
114
|
+
|
115
|
+
You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
|
116
|
+
total control over the slug format.
|
117
|
+
|
118
|
+
==== Deciding When to Generate New Slugs
|
119
|
+
|
120
|
+
Overriding {FriendlyId::Slugged#should_generate_new_friendly_id?} lets you
|
121
|
+
control whether new friendly ids are created when a model is updated. For
|
122
|
+
example, if you only want to generate slugs once and then treat them as
|
123
|
+
read-only:
|
124
|
+
|
125
|
+
class Post < ActiveRecord::Base
|
126
|
+
extend FriendlyId
|
127
|
+
friendly_id :title, :use => :slugged
|
128
|
+
|
129
|
+
def should_generate_new_friendly_id?
|
130
|
+
new_record?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
post = Post.create!(:title => "Hello world!")
|
135
|
+
post.slug #=> "hello-world"
|
136
|
+
post.title = "Hello there, world!"
|
137
|
+
post.save!
|
138
|
+
post.slug #=> "hello-world"
|
139
|
+
|
140
|
+
==== Locale-specific Transliterations
|
141
|
+
|
142
|
+
Active Support's +parameterize+ uses
|
143
|
+
transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
|
144
|
+
which in turn can use I18n's transliteration rules to consider the current
|
145
|
+
locale when replacing Latin characters:
|
146
|
+
|
147
|
+
# config/locales/de.yml
|
148
|
+
de:
|
149
|
+
i18n:
|
150
|
+
transliterate:
|
151
|
+
rule:
|
152
|
+
ü: "ue"
|
153
|
+
ö: "oe"
|
154
|
+
etc...
|
155
|
+
|
156
|
+
movie = Movie.create! :title => "Der Preis fürs Überleben"
|
157
|
+
movie.slug #=> "der-preis-fuers-ueberleben"
|
158
|
+
|
159
|
+
This functionality was in fact taken from earlier versions of FriendlyId.
|
160
|
+
|
161
|
+
==== Gotchas: Common Problems
|
162
|
+
|
163
|
+
===== Slugs That Begin With Numbers
|
164
|
+
|
165
|
+
Ruby's `to_i` function casts strings to integers in such a way that +23abc.to_i+
|
166
|
+
returns 23. Because FriendlyId falls back to finding by numeric id, this means
|
167
|
+
that if you attempt to find a record with a non-existant slug, and that slug
|
168
|
+
begins with a number, your find will probably return the wrong record.
|
169
|
+
|
170
|
+
There are two fairly simple ways to avoid this:
|
171
|
+
|
172
|
+
* Use validations to ensure that slugs don't begin with numbers.
|
173
|
+
* Use explicit finders like +find_by_id+ to always find by the numeric id, or
|
174
|
+
+find_by_slug+ to always find using the friendly id.
|
175
|
+
|
176
|
+
===== Concurrency Issues
|
177
|
+
|
178
|
+
FriendlyId uses a before_validation callback to generate and set the slug. This
|
179
|
+
means that if you create two model instances before saving them, it's possible
|
180
|
+
they will generate the same slug, and the second save will fail.
|
181
|
+
|
182
|
+
This can happen in two fairly normal cases: the first, when a model using nested
|
183
|
+
attributes creates more than one record for a model that uses friendly_id. The
|
184
|
+
second, in concurrent code, either in threads or multiple processes.
|
185
|
+
|
186
|
+
To solve the nested attributes issue, I recommend simply avoiding them when
|
187
|
+
creating more than one nested record for a model that uses FriendlyId. See {this
|
188
|
+
Github issue}[https://github.com/norman/friendly_id/issues/185] for discussion.
|
189
|
+
|
190
|
+
To solve the concurrency issue, I recommend locking the model's table against
|
191
|
+
inserts while when saving the record. See {this Github
|
192
|
+
issue}[https://github.com/norman/friendly_id/issues/180] for discussion.
|
193
|
+
|
194
|
+
=end
|
195
|
+
module Slugged
|
196
|
+
|
197
|
+
# Sets up behavior and configuration options for FriendlyId's slugging
|
198
|
+
# feature.
|
199
|
+
def self.included(model_class)
|
200
|
+
model_class.friendly_id_config.instance_eval do
|
201
|
+
self.class.send :include, Configuration
|
202
|
+
self.slug_generator_class ||= Class.new(SlugGenerator)
|
203
|
+
defaults[:slug_column] ||= 'slug'
|
204
|
+
defaults[:sequence_separator] ||= '--'
|
205
|
+
end
|
206
|
+
model_class.before_validation :set_slug
|
207
|
+
end
|
208
|
+
|
209
|
+
# Process the given value to make it suitable for use as a slug.
|
210
|
+
#
|
211
|
+
# This method is not intended to be invoked directly; FriendlyId uses it
|
212
|
+
# internaly to process strings into slugs.
|
213
|
+
#
|
214
|
+
# However, if FriendlyId's default slug generation doesn't suite your needs,
|
215
|
+
# you can override this method in your model class to control exactly how
|
216
|
+
# slugs are generated.
|
217
|
+
#
|
218
|
+
# === Example
|
219
|
+
#
|
220
|
+
# class Person < ActiveRecord::Base
|
221
|
+
# friendly_id :name_and_location
|
222
|
+
#
|
223
|
+
# def name_and_location
|
224
|
+
# "#{name} from #{location}"
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# # Use default slug, but upper case and with underscores
|
228
|
+
# def normalize_friendly_id(string)
|
229
|
+
# super.upcase.gsub("-", "_")
|
230
|
+
# end
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# bob = Person.create! :name => "Bob Smith", :location => "New York City"
|
234
|
+
# bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
|
235
|
+
#
|
236
|
+
# === More Resources
|
237
|
+
#
|
238
|
+
# You might want to look into Babosa[https://github.com/norman/babosa],
|
239
|
+
# which is the slugging library used by FriendlyId prior to version 4, which
|
240
|
+
# offers some specialized functionality missing from Active Support.
|
241
|
+
#
|
242
|
+
# @param [#to_s] value The value used as the basis of the slug.
|
243
|
+
# @return The candidate slug text, without a sequence.
|
244
|
+
def normalize_friendly_id(value)
|
245
|
+
# Fix to number based slugs which get mistaken as id's
|
246
|
+
value = value.to_s.parameterize
|
247
|
+
is_number = true if Float(value) rescue false
|
248
|
+
if is_number
|
249
|
+
return "#{rand_slug}#{value}"
|
250
|
+
end
|
251
|
+
value
|
252
|
+
end
|
253
|
+
|
254
|
+
# Generate random 10 character string for use in number error situations
|
255
|
+
# Instead of rejecting id/number based slug
|
256
|
+
def rand_slug
|
257
|
+
(0...10).map{ ('a'..'z').to_a[rand(26)] }.join
|
258
|
+
end
|
259
|
+
|
260
|
+
# Whether to generate a new slug.
|
261
|
+
#
|
262
|
+
# You can override this method in your model if, for example, you only want
|
263
|
+
# slugs to be generated once, and then never updated.
|
264
|
+
def should_generate_new_friendly_id?
|
265
|
+
base = send(friendly_id_config.base)
|
266
|
+
slug_value = send(friendly_id_config.slug_column)
|
267
|
+
|
268
|
+
# If the slug base is nil, and the slug field is nil, then we're going to
|
269
|
+
# leave the slug column NULL.
|
270
|
+
return false if base.nil? && slug_value.nil?
|
271
|
+
# Otherwise, if this is a new record, we're definitely going to try to
|
272
|
+
# create a new slug.
|
273
|
+
return true if new_record?
|
274
|
+
slug_base = normalize_friendly_id(base)
|
275
|
+
separator = Regexp.escape friendly_id_config.sequence_separator
|
276
|
+
# If the slug base (with and without sequence) is different from either the current
|
277
|
+
# friendly id or the slug value, then we'll generate a new friendly_id.
|
278
|
+
compare = (current_friendly_id || slug_value)
|
279
|
+
slug_base != compare && slug_base != compare.try(:sub, /#{separator}[\d]*\z/, '')
|
280
|
+
end
|
281
|
+
|
282
|
+
# Sets the slug.
|
283
|
+
# FIXME: This method sucks and the logic is pretty dubious.
|
284
|
+
def set_slug(normalized_slug = nil)
|
285
|
+
if normalized_slug || should_generate_new_friendly_id?
|
286
|
+
normalized_slug ||= normalize_friendly_id send(friendly_id_config.base)
|
287
|
+
generator = friendly_id_config.slug_generator_class.new self, normalized_slug
|
288
|
+
send "#{friendly_id_config.slug_column}=", generator.generate
|
289
|
+
end
|
290
|
+
end
|
291
|
+
private :set_slug
|
292
|
+
|
293
|
+
# This module adds the +:slug_column+, and +:sequence_separator+, and
|
294
|
+
# +:slug_generator_class+ configuration options to
|
295
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
296
|
+
module Configuration
|
297
|
+
attr_writer :slug_column, :sequence_separator
|
298
|
+
attr_accessor :slug_generator_class
|
299
|
+
|
300
|
+
# Makes FriendlyId use the slug column for querying.
|
301
|
+
# @return String The slug column.
|
302
|
+
def query_field
|
303
|
+
slug_column
|
304
|
+
end
|
305
|
+
|
306
|
+
# The string used to separate a slug base from a numeric sequence.
|
307
|
+
#
|
308
|
+
# By default, +--+ is used to separate the slug from the sequence.
|
309
|
+
# FriendlyId uses two dashes to distinguish sequences from slugs with
|
310
|
+
# numbers in their name.
|
311
|
+
#
|
312
|
+
# You can change the default separator by setting the
|
313
|
+
# {FriendlyId::Slugged::Configuration#sequence_separator
|
314
|
+
# sequence_separator} configuration option.
|
315
|
+
#
|
316
|
+
# For obvious reasons, you should avoid setting it to "+-+" unless you're
|
317
|
+
# sure you will never want to have a friendly id with a number in it.
|
318
|
+
# @return String The sequence separator string. Defaults to "+--+".
|
319
|
+
def sequence_separator
|
320
|
+
@sequence_separator or defaults[:sequence_separator]
|
321
|
+
end
|
322
|
+
|
323
|
+
# The column that will be used to store the generated slug.
|
324
|
+
def slug_column
|
325
|
+
@slug_column or defaults[:slug_column]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|