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