geothird_friendly_id 4.0.9.1
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 +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
|