geothird_friendly_id 4.0.9.1
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 +43 -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 +18 -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/friendly_id.rb +114 -0
- data/lib/generators/friendly_id_generator.rb +17 -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/Gemfile.lock +37 -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 +247 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module FriendlyId
|
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.
|
9
|
+
module ObjectUtils
|
10
|
+
|
11
|
+
# True is the id is definitely friendly, false if definitely unfriendly,
|
12
|
+
# else nil.
|
13
|
+
#
|
14
|
+
# An object is considired "definitely unfriendly" if its class is or
|
15
|
+
# inherits from ActiveRecord::Base, Array, Hash, NilClass, Numeric, or
|
16
|
+
# Symbol.
|
17
|
+
#
|
18
|
+
# An object is considered "definitely friendly" if it responds to +to_i+,
|
19
|
+
# and its value when cast to an integer and then back to a string is
|
20
|
+
# different from its value when merely cast to a string:
|
21
|
+
#
|
22
|
+
# 123.friendly_id? #=> false
|
23
|
+
# :id.friendly_id? #=> false
|
24
|
+
# {:name => 'joe'}.friendly_id? #=> false
|
25
|
+
# ['name = ?', 'joe'].friendly_id? #=> false
|
26
|
+
# nil.friendly_id? #=> false
|
27
|
+
# "123".friendly_id? #=> nil
|
28
|
+
# "abc123".friendly_id? #=> true
|
29
|
+
def friendly_id?
|
30
|
+
# Considered unfriendly if this is an instance of an unfriendly class or
|
31
|
+
# one of its descendants.
|
32
|
+
unfriendly_classes = [ActiveRecord::Base, Array, Hash, NilClass, Numeric,
|
33
|
+
Symbol, TrueClass, FalseClass]
|
34
|
+
|
35
|
+
if unfriendly_classes.detect {|klass| self.class <= klass}
|
36
|
+
false
|
37
|
+
elsif respond_to?(:to_i) && to_i.to_s != to_s
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# True if the id is definitely unfriendly, false if definitely friendly,
|
43
|
+
# else nil.
|
44
|
+
def unfriendly_id?
|
45
|
+
val = friendly_id? ; !val unless val.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Object.send :include, FriendlyId::ObjectUtils
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
== Reserved Words
|
6
|
+
|
7
|
+
The {FriendlyId::Reserved Reserved} module adds the ability to exlude a list of
|
8
|
+
words from use as FriendlyId slugs.
|
9
|
+
|
10
|
+
By default, FriendlyId reserves the words "new" and "edit" when this module is
|
11
|
+
included. You can configure this globally by using {FriendlyId.defaults
|
12
|
+
FriendlyId.defaults}:
|
13
|
+
|
14
|
+
FriendlyId.defaults do |config|
|
15
|
+
config.use :reserved
|
16
|
+
# Reserve words for English and Spanish URLs
|
17
|
+
config.reserved_words = %w(new edit nueva nuevo editar)
|
18
|
+
end
|
19
|
+
|
20
|
+
Note that the error message will appear on the field +:friendly_id+. If you are
|
21
|
+
using Rails's scaffolded form errors display, then it will have no field to
|
22
|
+
highlight. If you'd like to change this so that scaffolding works as expected,
|
23
|
+
one way to accomplish this is to move the error message to a different field.
|
24
|
+
For example:
|
25
|
+
|
26
|
+
class Person < ActiveRecord::Base
|
27
|
+
extend FriendlyId
|
28
|
+
friendly_id :name, use: :slugged
|
29
|
+
|
30
|
+
after_validation :move_friendly_id_error_to_name
|
31
|
+
|
32
|
+
def move_friendly_id_error_to_name
|
33
|
+
errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
=end
|
38
|
+
module Reserved
|
39
|
+
|
40
|
+
# When included, this module adds configuration options to the model class's
|
41
|
+
# friendly_id_config.
|
42
|
+
def self.included(model_class)
|
43
|
+
model_class.class_eval do
|
44
|
+
friendly_id_config.class.send :include, Reserved::Configuration
|
45
|
+
friendly_id_config.defaults[:reserved_words] ||= ["new", "edit"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# This module adds the +:reserved_words+ configuration option to
|
50
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
51
|
+
module Configuration
|
52
|
+
attr_writer :reserved_words
|
53
|
+
|
54
|
+
# Overrides {FriendlyId::Configuration#base} to add a validation to the
|
55
|
+
# model class.
|
56
|
+
def base=(base)
|
57
|
+
super
|
58
|
+
reserved_words = model_class.friendly_id_config.reserved_words
|
59
|
+
model_class.validates_exclusion_of :friendly_id, :in => reserved_words
|
60
|
+
end
|
61
|
+
|
62
|
+
# An array of words forbidden as slugs.
|
63
|
+
def reserved_words
|
64
|
+
@reserved_words ||= @defaults[:reserved_words]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -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.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
|
+
self.table_name = "friendly_id_slugs"
|
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.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
|