friendly_id4 4.0.0.beta6 → 4.0.0.pre
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/README.md +47 -68
- data/Rakefile +13 -108
- data/lib/friendly_id.rb +97 -117
- data/lib/friendly_id/scoped.rb +17 -116
- data/lib/friendly_id/slugged.rb +89 -183
- data/lib/friendly_id/test.rb +23 -0
- data/lib/friendly_id/test/generic.rb +84 -0
- data/lib/friendly_id/version.rb +9 -0
- data/test/core_test.rb +54 -16
- data/test/scoped_test.rb +39 -35
- data/test/slugged_test.rb +45 -64
- data/test/test_helper.rb +23 -0
- metadata +61 -125
- data/.gemtest +0 -0
- data/.gitignore +0 -11
- data/.yardopts +0 -4
- data/WhatsNew.md +0 -142
- data/bench.rb +0 -63
- data/friendly_id.gemspec +0 -31
- data/lib/friendly_id/base.rb +0 -134
- data/lib/friendly_id/configuration.rb +0 -78
- data/lib/friendly_id/finder_methods.rb +0 -20
- data/lib/friendly_id/history.rb +0 -64
- data/lib/friendly_id/migration.rb +0 -18
- data/lib/friendly_id/model.rb +0 -22
- data/lib/friendly_id/object_utils.rb +0 -40
- data/lib/friendly_id/reserved.rb +0 -46
- data/lib/friendly_id/slug.rb +0 -6
- data/lib/friendly_id/slug_sequencer.rb +0 -82
- data/lib/generators/friendly_id_generator.rb +0 -24
- data/test/base_test.rb +0 -54
- data/test/config/mysql.yml +0 -5
- data/test/config/mysql2.yml +0 -5
- data/test/config/postgres.yml +0 -6
- data/test/config/sqlite3.yml +0 -3
- data/test/configuration_test.rb +0 -27
- data/test/helper.rb +0 -90
- data/test/history_test.rb +0 -55
- data/test/object_utils_test.rb +0 -26
- data/test/reserved_test.rb +0 -26
- data/test/schema.rb +0 -56
- data/test/shared.rb +0 -118
- data/test/sti_test.rb +0 -48
data/lib/friendly_id/scoped.rb
CHANGED
@@ -2,130 +2,31 @@ require "friendly_id/slugged"
|
|
2
2
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
|
-
|
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
|
5
|
+
# This module adds scopes to in-table slugs.
|
80
6
|
module Scoped
|
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
|
87
|
-
raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
|
88
|
-
include Slugged unless self < Slugged
|
89
|
-
friendly_id_config.class.send :include, Configuration
|
90
|
-
friendly_id_config.slug_sequencer_class.send :include, SlugSequencer
|
91
|
-
end
|
7
|
+
def self.included(klass)
|
8
|
+
klass.send :include, Slugged unless klass.include? Slugged
|
92
9
|
end
|
10
|
+
end
|
93
11
|
|
94
|
-
|
95
|
-
|
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
|
104
|
-
attr_accessor :scope
|
12
|
+
class SlugSequencer
|
13
|
+
alias conflict_without_scope conflict
|
105
14
|
|
106
|
-
|
107
|
-
|
108
|
-
#
|
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
|
-
#
|
113
|
-
# @return String The scope column
|
114
|
-
def scope_column
|
115
|
-
(model_class.reflections[@scope].try(:association_foreign_key) || @scope).to_s
|
116
|
-
end
|
15
|
+
def conflict_with_scope
|
16
|
+
column = friendly_id_config.scope_column
|
17
|
+
conflicts.where("#{column} = ?", sluggable.send(column)).first
|
117
18
|
end
|
118
19
|
|
119
|
-
|
120
|
-
|
121
|
-
|
20
|
+
def conflict
|
21
|
+
friendly_id_config.scope ? conflict_with_scope : conflict_without_scope
|
22
|
+
end
|
23
|
+
end
|
122
24
|
|
123
|
-
|
25
|
+
class Configuration
|
26
|
+
attr_accessor :scope
|
124
27
|
|
125
|
-
|
126
|
-
|
127
|
-
conflicts.where("#{column} = ?", sluggable.send(column)).first
|
128
|
-
end
|
28
|
+
def scope_column
|
29
|
+
klass.reflections[@scope].try(:association_foreign_key) || @scope.to_s
|
129
30
|
end
|
130
31
|
end
|
131
32
|
end
|
data/lib/friendly_id/slugged.rb
CHANGED
@@ -1,221 +1,127 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
require "friendly_id/slug_sequencer"
|
3
|
-
|
4
1
|
module FriendlyId
|
5
|
-
=begin
|
6
|
-
This module adds in-table slugs to a model.
|
7
|
-
|
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
2
|
|
48
|
-
|
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"
|
3
|
+
# This module adds in-table slugs to an ActiveRecord model.
|
4
|
+
module Slugged
|
57
5
|
|
58
|
-
|
6
|
+
# @NOTE AR-specific code here
|
7
|
+
def self.included(klass)
|
8
|
+
klass.before_save :set_slug
|
9
|
+
klass.friendly_id_config.use_slugs = true
|
10
|
+
end
|
59
11
|
|
60
|
-
|
61
|
-
|
12
|
+
# @NOTE AS-specific code here
|
13
|
+
def normalize_friendly_id(value)
|
14
|
+
value.to_s.parameterize
|
15
|
+
end
|
62
16
|
|
63
|
-
|
64
|
-
car2 = Car.create :title => "Peugot 206"
|
17
|
+
private
|
65
18
|
|
66
|
-
|
67
|
-
|
19
|
+
def set_slug
|
20
|
+
send "#{friendly_id_config.slug_column}=", SlugSequencer.new(self).to_s
|
21
|
+
end
|
22
|
+
end
|
68
23
|
|
69
|
-
|
24
|
+
class Configuration
|
25
|
+
attr_writer :slug_column, :sequence_separator, :use_slugs
|
70
26
|
|
71
|
-
|
72
|
-
sequence_separator
|
27
|
+
DEFAULTS[:slug_column] = 'slug'
|
28
|
+
DEFAULTS[:sequence_separator] = '--'
|
73
29
|
|
74
|
-
|
30
|
+
def query_field
|
31
|
+
use_slugs? ? slug_column : base
|
32
|
+
end
|
75
33
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
FriendlyId uses.
|
34
|
+
def sequence_separator
|
35
|
+
@sequence_separator || DEFAULTS[:sequence_separator]
|
36
|
+
end
|
80
37
|
|
81
|
-
|
38
|
+
def slug_column
|
39
|
+
@slug_column || DEFAULTS[:slug_column]
|
40
|
+
end
|
82
41
|
|
83
|
-
|
84
|
-
|
85
|
-
def name_and_location
|
86
|
-
"#{name} from #{location}"
|
42
|
+
def use_slugs?
|
43
|
+
@use_slugs
|
87
44
|
end
|
88
45
|
end
|
89
46
|
|
90
|
-
|
91
|
-
|
47
|
+
# This class offers functionality to check slug strings for uniqueness and,
|
48
|
+
# if necessary, append a sequence to ensure it.
|
49
|
+
class SlugSequencer
|
50
|
+
attr_reader :sluggable
|
92
51
|
|
93
|
-
|
52
|
+
def initialize(sluggable)
|
53
|
+
@sluggable = sluggable
|
54
|
+
end
|
94
55
|
|
95
|
-
|
96
|
-
|
56
|
+
def base
|
57
|
+
sluggable.send friendly_id_config.base
|
58
|
+
end
|
97
59
|
|
98
|
-
|
60
|
+
def changed?
|
61
|
+
base != current_friendly_id.try(:sub, /--[\d]*\z/, '')
|
62
|
+
end
|
99
63
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
locale when replacing Latin characters:
|
64
|
+
def column
|
65
|
+
friendly_id_config.query_field
|
66
|
+
end
|
104
67
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
transliterate:
|
109
|
-
rule:
|
110
|
-
ü: "ue"
|
111
|
-
ö: "oe"
|
112
|
-
etc...
|
68
|
+
def conflict?
|
69
|
+
!! conflict
|
70
|
+
end
|
113
71
|
|
114
|
-
|
115
|
-
|
72
|
+
def conflict
|
73
|
+
unless defined? @conflict
|
74
|
+
@conflict = conflicts.first
|
75
|
+
end
|
76
|
+
@conflict
|
77
|
+
end
|
116
78
|
|
117
|
-
|
118
|
-
|
119
|
-
|
79
|
+
# @NOTE AR-specific code here
|
80
|
+
def conflicts
|
81
|
+
pkey = sluggable.class.arel_table.primary_key.name
|
82
|
+
value = sluggable.send pkey
|
83
|
+
scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
|
84
|
+
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
|
85
|
+
scope = scope.order("#{column} DESC")
|
86
|
+
end
|
120
87
|
|
121
|
-
|
122
|
-
|
123
|
-
def self.included(model_class)
|
124
|
-
model_class.instance_eval do
|
125
|
-
friendly_id_config.class.send :include, Configuration
|
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)
|
129
|
-
before_validation :set_slug
|
130
|
-
end
|
88
|
+
def current_friendly_id
|
89
|
+
sluggable.instance_variable_get(:@_current_friendly_id)
|
131
90
|
end
|
132
91
|
|
133
|
-
|
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.
|
168
|
-
def normalize_friendly_id(value)
|
169
|
-
value.to_s.parameterize
|
92
|
+
def friendly_id_config
|
93
|
+
sluggable.friendly_id_config
|
170
94
|
end
|
171
95
|
|
172
|
-
|
173
|
-
|
174
|
-
# @see FriendlyId::SlugSequencer
|
175
|
-
def slug_sequencer
|
176
|
-
friendly_id_config.slug_sequencer_class.new(self)
|
96
|
+
def new_record?
|
97
|
+
sluggable.new_record?
|
177
98
|
end
|
178
99
|
|
179
|
-
|
180
|
-
|
181
|
-
send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
|
100
|
+
def normalized
|
101
|
+
@normalized ||= sluggable.normalize_friendly_id(base)
|
182
102
|
end
|
183
|
-
private :set_slug
|
184
103
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
module Configuration
|
189
|
-
attr_writer :slug_column, :sequence_separator
|
190
|
-
attr_accessor :slug_sequencer_class
|
104
|
+
def separator
|
105
|
+
friendly_id_config.sequence_separator
|
106
|
+
end
|
191
107
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
108
|
+
def next
|
109
|
+
sequence = conflict.slug.split(separator)[1].to_i
|
110
|
+
next_sequence = sequence == 0 ? 2 : sequence.next
|
111
|
+
"#{normalized}#{separator}#{next_sequence}"
|
112
|
+
end
|
197
113
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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 "+--+".
|
211
|
-
def sequence_separator
|
212
|
-
@sequence_separator or defaults[:sequence_separator]
|
114
|
+
def to_s
|
115
|
+
if changed? or new_record?
|
116
|
+
conflict? ? self.next : normalized
|
117
|
+
else
|
118
|
+
sluggable.friendly_id
|
213
119
|
end
|
120
|
+
end
|
214
121
|
|
215
|
-
|
216
|
-
|
217
|
-
@slug_column or defaults[:slug_column]
|
218
|
-
end
|
122
|
+
def wildcard
|
123
|
+
"#{normalized}#{separator}%"
|
219
124
|
end
|
220
125
|
end
|
126
|
+
|
221
127
|
end
|