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,291 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
=begin
|
3
|
+
|
4
|
+
== Setting Up FriendlyId in Your Model
|
5
|
+
|
6
|
+
To use FriendlyId in your ActiveRecord models, you must first either extend or
|
7
|
+
include the FriendlyId module (it makes no difference), then invoke the
|
8
|
+
{FriendlyId::Base#friendly_id friendly_id} method to configure your desired
|
9
|
+
options:
|
10
|
+
|
11
|
+
class Foo < ActiveRecord::Base
|
12
|
+
include FriendlyId
|
13
|
+
friendly_id :bar, :use => [:slugged, :simple_i18n]
|
14
|
+
end
|
15
|
+
|
16
|
+
The most important option is `:use`, which you use to tell FriendlyId which
|
17
|
+
addons it should use. See the documentation for this method for a list of all
|
18
|
+
available addons, or skim through the rest of the docs to get a high-level
|
19
|
+
overview.
|
20
|
+
|
21
|
+
=== The Default Setup: Simple Models
|
22
|
+
|
23
|
+
The simplest way to use FriendlyId is with a model that has a uniquely indexed
|
24
|
+
column with no spaces or special characters, and that is seldom or never
|
25
|
+
updated. The most common example of this is a user name:
|
26
|
+
|
27
|
+
class User < ActiveRecord::Base
|
28
|
+
extend FriendlyId
|
29
|
+
friendly_id :login
|
30
|
+
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
31
|
+
end
|
32
|
+
|
33
|
+
@user = User.find "joe" # the old User.find(1) still works, too
|
34
|
+
@user.to_param # returns "joe"
|
35
|
+
redirect_to @user # the URL will be /users/joe
|
36
|
+
|
37
|
+
In this case, FriendlyId assumes you want to use the column as-is; it will never
|
38
|
+
modify the value of the column, and your application should ensure that the
|
39
|
+
value is unique and admissible in a URL:
|
40
|
+
|
41
|
+
class City < ActiveRecord::Base
|
42
|
+
extend FriendlyId
|
43
|
+
friendly_id :name
|
44
|
+
end
|
45
|
+
|
46
|
+
@city.find "Viña del Mar"
|
47
|
+
redirect_to @city # the URL will be /cities/Viña%20del%20Mar
|
48
|
+
|
49
|
+
Writing the code to process an arbitrary string into a good identifier for use
|
50
|
+
in a URL can be repetitive and surprisingly tricky, so for this reason it's
|
51
|
+
often better and easier to use {FriendlyId::Slugged slugs}.
|
52
|
+
|
53
|
+
=end
|
54
|
+
module Base
|
55
|
+
|
56
|
+
# Configure FriendlyId's behavior in a model.
|
57
|
+
#
|
58
|
+
# class Post < ActiveRecord::Base
|
59
|
+
# extend FriendlyId
|
60
|
+
# friendly_id :title, :use => :slugged
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# When given the optional block, this method will yield the class's instance
|
64
|
+
# of {FriendlyId::Configuration} to the block before evaluating other
|
65
|
+
# arguments, so configuration values set in the block may be overwritten by
|
66
|
+
# the arguments. This order was chosen to allow passing the same proc to
|
67
|
+
# multiple models, while being able to override the values it sets. Here is
|
68
|
+
# a contrived example:
|
69
|
+
#
|
70
|
+
# $friendly_id_config_proc = Proc.new do |config|
|
71
|
+
# config.base = :name
|
72
|
+
# config.use :slugged
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# class Foo < ActiveRecord::Base
|
76
|
+
# extend FriendlyId
|
77
|
+
# friendly_id &$friendly_id_config_proc
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# class Bar < ActiveRecord::Base
|
81
|
+
# extend FriendlyId
|
82
|
+
# friendly_id :title, &$friendly_id_config_proc
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# However, it's usually better to use {FriendlyId.defaults} for this:
|
86
|
+
#
|
87
|
+
# FriendlyId.defaults do |config|
|
88
|
+
# config.base = :name
|
89
|
+
# config.use :slugged
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# class Foo < ActiveRecord::Base
|
93
|
+
# extend FriendlyId
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# class Bar < ActiveRecord::Base
|
97
|
+
# extend FriendlyId
|
98
|
+
# friendly_id :title
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# In general you should use the block syntax either because of your personal
|
102
|
+
# aesthetic preference, or because you need to share some functionality
|
103
|
+
# between multiple models that can't be well encapsulated by
|
104
|
+
# {FriendlyId.defaults}.
|
105
|
+
#
|
106
|
+
# === Order Method Calls in a Block vs Ordering Options
|
107
|
+
#
|
108
|
+
# When calling this method without a block, you may set the hash options in
|
109
|
+
# any order.
|
110
|
+
#
|
111
|
+
# However, when using block-style invocation, be sure to call
|
112
|
+
# FriendlyId::Configuration's {FriendlyId::Configuration#use use} method
|
113
|
+
# *prior* to the associated configuration options, because it will include
|
114
|
+
# modules into your class, and these modules in turn may add required
|
115
|
+
# configuration options to the +@friendly_id_configuraton+'s class:
|
116
|
+
#
|
117
|
+
# class Person < ActiveRecord::Base
|
118
|
+
# friendly_id do |config|
|
119
|
+
# # This will work
|
120
|
+
# config.use :slugged
|
121
|
+
# config.sequence_separator = ":"
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# class Person < ActiveRecord::Base
|
126
|
+
# friendly_id do |config|
|
127
|
+
# # This will fail
|
128
|
+
# config.sequence_separator = ":"
|
129
|
+
# config.use :slugged
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# === Including Your Own Modules
|
134
|
+
#
|
135
|
+
# Because :use can accept a name or a Module, {FriendlyId.defaults defaults}
|
136
|
+
# can be a convenient place to set up behavior common to all classes using
|
137
|
+
# FriendlyId. You can include any module, or more conveniently, define one
|
138
|
+
# on-the-fly. For example, let's say you want to make
|
139
|
+
# Babosa[http://github.com/norman/babosa] the default slugging library in
|
140
|
+
# place of Active Support, and transliterate all slugs from Russian Cyrillic
|
141
|
+
# to ASCII:
|
142
|
+
#
|
143
|
+
# require "babosa"
|
144
|
+
#
|
145
|
+
# FriendlyId.defaults do |config|
|
146
|
+
# config.base = :name
|
147
|
+
# config.use :slugged
|
148
|
+
# config.use Module.new {
|
149
|
+
# def normalize_friendly_id(text)
|
150
|
+
# text.to_slug.normalize(:transliterations => [:russian, :latin])
|
151
|
+
# end
|
152
|
+
# }
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
#
|
156
|
+
# @option options [Symbol,Module] :use The addon or name of an addon to use.
|
157
|
+
# By default, FriendlyId provides {FriendlyId::Slugged :slugged},
|
158
|
+
# {FriendlyId::History :history}, {FriendlyId::Reserved :reserved}, and
|
159
|
+
# {FriendlyId::Scoped :scoped}, {FriendlyId::SimpleI18n :simple_i18n},
|
160
|
+
# and {FriendlyId::Globalize :globalize}.
|
161
|
+
#
|
162
|
+
# @option options [Array] :reserved_words Available when using +:reserved+,
|
163
|
+
# which is loaded by default. Sets an array of words banned for use as
|
164
|
+
# the basis of a friendly_id. By default this includes "edit" and "new".
|
165
|
+
#
|
166
|
+
# @option options [Symbol] :scope Available when using +:scoped+.
|
167
|
+
# Sets the relation or column used to scope generated friendly ids. This
|
168
|
+
# option has no default value.
|
169
|
+
#
|
170
|
+
# @option options [Symbol] :sequence_separator Available when using +:slugged+.
|
171
|
+
# Configures the sequence of characters used to separate a slug from a
|
172
|
+
# sequence. Defaults to +--+.
|
173
|
+
#
|
174
|
+
# @option options [Symbol] :slug_column Available when using +:slugged+.
|
175
|
+
# Configures the name of the column where FriendlyId will store the slug.
|
176
|
+
# Defaults to +:slug+.
|
177
|
+
#
|
178
|
+
# @option options [Symbol] :slug_generator_class Available when using +:slugged+.
|
179
|
+
# Sets the class used to generate unique slugs. You should not specify this
|
180
|
+
# unless you're doing some extensive hacking on FriendlyId. Defaults to
|
181
|
+
# {FriendlyId::SlugGenerator}.
|
182
|
+
#
|
183
|
+
# @yield Provides access to the model class's friendly_id_config, which
|
184
|
+
# allows an alternate configuration syntax, and conditional configuration
|
185
|
+
# logic.
|
186
|
+
#
|
187
|
+
# @yieldparam config The model class's {FriendlyId::Configuration friendly_id_config}.
|
188
|
+
def friendly_id(base = nil, options = {}, &block)
|
189
|
+
yield friendly_id_config if block_given?
|
190
|
+
friendly_id_config.use options.delete :use
|
191
|
+
friendly_id_config.send :set, base ? options.merge(:base => base) : options
|
192
|
+
before_save {|rec| rec.instance_eval {@current_friendly_id = friendly_id}}
|
193
|
+
include Model
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns the model class's {FriendlyId::Configuration friendly_id_config}.
|
197
|
+
# @note In the case of Single Table Inheritance (STI), this method will
|
198
|
+
# duplicate the parent class's FriendlyId::Configuration and relation class
|
199
|
+
# on first access. If you're concerned about thread safety, then be sure
|
200
|
+
# to invoke {#friendly_id} in your class for each model.
|
201
|
+
def friendly_id_config
|
202
|
+
@friendly_id_config ||= base_class.friendly_id_config.dup.tap do |config|
|
203
|
+
config.model_class = self
|
204
|
+
@relation_class = base_class.send(:relation_class)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
# Gets an instance of an the relation class.
|
211
|
+
#
|
212
|
+
# With FriendlyId this will be a subclass of ActiveRecord::Relation, rather than
|
213
|
+
# Relation itself, in order to avoid tainting all Active Record models with
|
214
|
+
# FriendlyId.
|
215
|
+
#
|
216
|
+
# Note that this method is essentially copied and pasted from Rails 3.2.9.rc1,
|
217
|
+
# with the exception of changing the relation class. Obviously this is less than
|
218
|
+
# ideal, but I know of no better way to accomplish this.
|
219
|
+
# @see #relation_class
|
220
|
+
def relation #:nodoc:
|
221
|
+
relation = relation_class.new(self, arel_table)
|
222
|
+
|
223
|
+
if finder_needs_type_condition?
|
224
|
+
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
|
225
|
+
else
|
226
|
+
relation
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Gets (and if necessary, creates) a subclass of the model's relation class.
|
231
|
+
#
|
232
|
+
# Rather than including FriendlyId's overridden finder methods in
|
233
|
+
# ActiveRecord::Relation directly, FriendlyId adds them to a subclass
|
234
|
+
# specific to the AR model, and makes #relation return an instance of this
|
235
|
+
# class. By doing this, we ensure that only models that specifically extend
|
236
|
+
# FriendlyId have their finder methods overridden.
|
237
|
+
#
|
238
|
+
# Note that this method does not directly subclass ActiveRecord::Relation,
|
239
|
+
# but rather whatever class the @relation class instance variable is an
|
240
|
+
# instance of. In practice, this will almost always end up being
|
241
|
+
# ActiveRecord::Relation, but in case another plugin is using this same
|
242
|
+
# pattern to extend a model's finder functionality, FriendlyId will not
|
243
|
+
# replace it, but rather override it.
|
244
|
+
#
|
245
|
+
# This pattern can be seen as a poor man's "refinement"
|
246
|
+
# (http://timelessrepo.com/refinements-in-ruby), and while I **think** it
|
247
|
+
# will work quite well, I realize that it could cause unexpected issues,
|
248
|
+
# since the authors of Rails are probably not intending this kind of usage
|
249
|
+
# against a private API. If this ends up being problematic I will probably
|
250
|
+
# revert back to the old behavior of simply extending
|
251
|
+
# ActiveRecord::Relation.
|
252
|
+
def relation_class
|
253
|
+
@relation_class or begin
|
254
|
+
@relation_class = Class.new(relation_without_friendly_id.class) do
|
255
|
+
alias_method :find_one_without_friendly_id, :find_one
|
256
|
+
alias_method :exists_without_friendly_id?, :exists?
|
257
|
+
include FriendlyId::FinderMethods
|
258
|
+
end
|
259
|
+
# Set a name so that model instances can be marshalled. Use a
|
260
|
+
# ridiculously long name that will not conflict with anything.
|
261
|
+
# TODO: just use the constant, no need for the @relation_class variable.
|
262
|
+
const_set('FriendlyIdActiveRecordRelation', @relation_class)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Instance methods that will be added to all classes using FriendlyId.
|
268
|
+
module Model
|
269
|
+
|
270
|
+
attr_reader :current_friendly_id
|
271
|
+
|
272
|
+
# Convenience method for accessing the class method of the same name.
|
273
|
+
def friendly_id_config
|
274
|
+
self.class.friendly_id_config
|
275
|
+
end
|
276
|
+
|
277
|
+
# Get the instance's friendly_id.
|
278
|
+
def friendly_id
|
279
|
+
send friendly_id_config.query_field
|
280
|
+
end
|
281
|
+
|
282
|
+
# Either the friendly_id, or the numeric id cast to a string.
|
283
|
+
def to_param
|
284
|
+
if diff = changes[friendly_id_config.query_field]
|
285
|
+
diff.first || diff.second
|
286
|
+
else
|
287
|
+
friendly_id.presence || super
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# The configuration paramters passed to +friendly_id+ will be stored in
|
3
|
+
# this object.
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
# The base column or method used by FriendlyId as the basis of a friendly id
|
7
|
+
# or slug.
|
8
|
+
#
|
9
|
+
# For models that don't use FriendlyId::Slugged, the base is the column that
|
10
|
+
# is used as the FriendlyId directly. For models using FriendlyId::Slugged,
|
11
|
+
# the base is a column or method whose value is used as the basis of the
|
12
|
+
# slug.
|
13
|
+
#
|
14
|
+
# For example, if you have a model representing blog posts and that uses
|
15
|
+
# slugs, you likely will want to use the "title" attribute as the base, and
|
16
|
+
# FriendlyId will take care of transforming the human-readable title into
|
17
|
+
# something suitable for use in a URL.
|
18
|
+
#
|
19
|
+
# @param [Symbol] A symbol referencing a column or method in the model. This
|
20
|
+
# value is usually set by passing it as the first argument to
|
21
|
+
# {FriendlyId::Base#friendly_id friendly_id}:
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# class Book < ActiveRecord::Base
|
25
|
+
# extend FriendlyId
|
26
|
+
# friendly_id :name
|
27
|
+
# end
|
28
|
+
attr_accessor :base
|
29
|
+
|
30
|
+
# The default configuration options.
|
31
|
+
attr_reader :defaults
|
32
|
+
|
33
|
+
# The model class that this configuration belongs to.
|
34
|
+
# @return ActiveRecord::Base
|
35
|
+
attr_accessor :model_class
|
36
|
+
|
37
|
+
def initialize(model_class, values = nil)
|
38
|
+
@model_class = model_class
|
39
|
+
@defaults = {}
|
40
|
+
set values
|
41
|
+
end
|
42
|
+
|
43
|
+
# Lets you specify the modules to use with FriendlyId.
|
44
|
+
#
|
45
|
+
# This method is invoked by {FriendlyId::Base#friendly_id friendly_id} when
|
46
|
+
# passing the +:use+ option, or when using {FriendlyId::Base#friendly_id
|
47
|
+
# friendly_id} with a block.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# class Book < ActiveRecord::Base
|
51
|
+
# extend FriendlyId
|
52
|
+
# friendly_id :name, :use => :slugged
|
53
|
+
# end
|
54
|
+
# @param [#to_s,Module] *modules Arguments should be Modules, or symbols or
|
55
|
+
# strings that correspond with the name of a module inside the FriendlyId
|
56
|
+
# namespace. By default FriendlyId provides +:slugged+, +:history+,
|
57
|
+
# +:simple_i18n+, +:globalize+, and +:scoped+.
|
58
|
+
def use(*modules)
|
59
|
+
modules.to_a.flatten.compact.map do |object|
|
60
|
+
mod = object.kind_of?(Module) ? object : FriendlyId.const_get(object.to_s.classify)
|
61
|
+
model_class.send(:include, mod)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# The column that FriendlyId will use to find the record when querying by
|
66
|
+
# friendly id.
|
67
|
+
#
|
68
|
+
# This method is generally only used internally by FriendlyId.
|
69
|
+
# @return String
|
70
|
+
def query_field
|
71
|
+
base.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def set(values)
|
77
|
+
values and values.each {|name, value| self.send "#{name}=", value}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# These methods will be added to the model's {FriendlyId::Base#relation_class relation_class}.
|
3
|
+
module FinderMethods
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
# FriendlyId overrides this method to make it possible to use friendly id's
|
8
|
+
# identically to numeric ids in finders.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# person = Person.find(123)
|
12
|
+
# person = Person.find("joe")
|
13
|
+
#
|
14
|
+
# @see FriendlyId::ObjectUtils
|
15
|
+
def find_one(id)
|
16
|
+
return super if id.unfriendly_id?
|
17
|
+
where(@klass.friendly_id_config.query_field => id).first or super
|
18
|
+
end
|
19
|
+
|
20
|
+
# FriendlyId overrides this method to make it possible to use friendly id's
|
21
|
+
# identically to numeric ids in finders.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# person = Person.exists?(123)
|
25
|
+
# person = Person.exists?("joe")
|
26
|
+
# person = Person.exists?({:name => 'joe'})
|
27
|
+
# person = Person.exists?(['name = ?', 'joe'])
|
28
|
+
#
|
29
|
+
# @see FriendlyId::ObjectUtils
|
30
|
+
def exists?(id = false)
|
31
|
+
return super if id.unfriendly_id?
|
32
|
+
super @klass.friendly_id_config.query_field => id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
|
5
|
+
=begin
|
6
|
+
|
7
|
+
== Translating Slugs Using Globalize
|
8
|
+
|
9
|
+
The {FriendlyId::Globalize Globalize} module lets you use
|
10
|
+
Globalize[https://github.com/svenfuchs/globalize3] to translate slugs. This
|
11
|
+
module is most suitable for applications that need to be localized to many
|
12
|
+
languages. If your application only needs to be localized to one or two
|
13
|
+
languages, you may wish to consider the {FriendlyId::SimpleI18n SimpleI18n}
|
14
|
+
module.
|
15
|
+
|
16
|
+
In order to use this module, your model's table and translation table must both
|
17
|
+
have a slug column, and your model must set the +slug+ field as translatable
|
18
|
+
with Globalize:
|
19
|
+
|
20
|
+
class Post < ActiveRecord::Base
|
21
|
+
translates :title, :slug
|
22
|
+
extend FriendlyId
|
23
|
+
friendly_id :title, :use => :globalize
|
24
|
+
end
|
25
|
+
|
26
|
+
=== Finds
|
27
|
+
|
28
|
+
Finds will take the current locale into consideration:
|
29
|
+
|
30
|
+
I18n.locale = :it
|
31
|
+
Post.find("guerre-stellari")
|
32
|
+
I18n.locale = :en
|
33
|
+
Post.find("star-wars")
|
34
|
+
|
35
|
+
Additionally, finds will fall back to the default locale:
|
36
|
+
|
37
|
+
I18n.locale = :it
|
38
|
+
Post.find("star-wars")
|
39
|
+
|
40
|
+
To find a slug by an explicit locale, perform the find inside a block
|
41
|
+
passed to I18n's +with_locale+ method:
|
42
|
+
|
43
|
+
I18n.with_locale(:it) { Post.find("guerre-stellari") }
|
44
|
+
|
45
|
+
=== Creating Records
|
46
|
+
|
47
|
+
When new records are created, the slug is generated for the current locale only.
|
48
|
+
|
49
|
+
=== Translating Slugs
|
50
|
+
|
51
|
+
To translate an existing record's friendly_id, use
|
52
|
+
{FriendlyId::Globalize::Model#set_friendly_id}. This will ensure that the slug
|
53
|
+
you add is properly escaped, transliterated and sequenced:
|
54
|
+
|
55
|
+
post = Post.create :name => "Star Wars"
|
56
|
+
post.set_friendly_id("Guerre stellari", :it)
|
57
|
+
|
58
|
+
If you don't pass in a locale argument, FriendlyId::Globalize will just use the
|
59
|
+
current locale:
|
60
|
+
|
61
|
+
I18n.with_locale(:it) { post.set_friendly_id("Guerre stellari") }
|
62
|
+
|
63
|
+
=end
|
64
|
+
module Globalize
|
65
|
+
|
66
|
+
def self.included(model_class)
|
67
|
+
model_class.instance_eval do
|
68
|
+
friendly_id_config.use :slugged
|
69
|
+
relation_class.send :include, FinderMethods
|
70
|
+
include Model
|
71
|
+
# Check if slug field is enabled to be translated with Globalize
|
72
|
+
unless respond_to?('translated_attribute_names') || translated_attribute_names.exclude?(friendly_id_config.query_field.to_sym)
|
73
|
+
puts "\n[FriendlyId] You need to translate '#{friendly_id_config.query_field}' field with Globalize (add 'translates :#{friendly_id_config.query_field}' in your model '#{self.class.name}')\n\n"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module Model
|
79
|
+
def set_friendly_id(text, locale)
|
80
|
+
I18n.with_locale(locale || I18n.locale) do
|
81
|
+
set_slug(normalize_friendly_id(text))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
module FinderMethods
|
87
|
+
# FriendlyId overrides this method to make it possible to use friendly id's
|
88
|
+
# identically to numeric ids in finders.
|
89
|
+
#
|
90
|
+
# @example
|
91
|
+
# person = Person.find(123)
|
92
|
+
# person = Person.find("joe")
|
93
|
+
#
|
94
|
+
# @see FriendlyId::ObjectUtils
|
95
|
+
def find_one(id)
|
96
|
+
return super if id.unfriendly_id?
|
97
|
+
found = where(@klass.friendly_id_config.query_field => id).first
|
98
|
+
found = includes(:translations).
|
99
|
+
where(translation_class.arel_table[:locale].in([I18n.locale, I18n.default_locale])).
|
100
|
+
where(translation_class.arel_table[@klass.friendly_id_config.query_field].eq(id)).first if found.nil?
|
101
|
+
|
102
|
+
if found
|
103
|
+
# Reload the translations for the found records.
|
104
|
+
found.tap { |f| f.translations.reload }
|
105
|
+
else
|
106
|
+
# if locale is not translated fallback to default locale
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
protected :find_one
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
== History: Avoiding 404's When Slugs Change
|
6
|
+
|
7
|
+
FriendlyId's {FriendlyId::History History} module adds the ability to store a
|
8
|
+
log of a model's slugs, so that when its friendly id changes, it's still
|
9
|
+
possible to perform finds by the old id.
|
10
|
+
|
11
|
+
The primary use case for this is avoiding broken URLs.
|
12
|
+
|
13
|
+
=== Setup
|
14
|
+
|
15
|
+
In order to use this module, you must add a table to your database schema to
|
16
|
+
store the slug records. FriendlyId provides a generator for this purpose:
|
17
|
+
|
18
|
+
rails generate friendly_id
|
19
|
+
rake db:migrate
|
20
|
+
|
21
|
+
This will add a table named +friendly_id_slugs+, used by the {FriendlyId::Slug}
|
22
|
+
model.
|
23
|
+
|
24
|
+
=== Considerations
|
25
|
+
|
26
|
+
This module is incompatible with the +:scoped+ module.
|
27
|
+
|
28
|
+
Because recording slug history requires creating additional database records,
|
29
|
+
this module has an impact on the performance of the associated model's +create+
|
30
|
+
method.
|
31
|
+
|
32
|
+
=== Example
|
33
|
+
|
34
|
+
class Post < ActiveRecord::Base
|
35
|
+
extend FriendlyId
|
36
|
+
friendly_id :title, :use => :history
|
37
|
+
end
|
38
|
+
|
39
|
+
class PostsController < ApplicationController
|
40
|
+
|
41
|
+
before_filter :find_post
|
42
|
+
|
43
|
+
...
|
44
|
+
|
45
|
+
def find_post
|
46
|
+
@post = Post.find params[:id]
|
47
|
+
|
48
|
+
# If an old id or a numeric id was used to find the record, then
|
49
|
+
# the request path will not match the post_path, and we should do
|
50
|
+
# a 301 redirect that uses the current friendly id.
|
51
|
+
if request.path != post_path(@post)
|
52
|
+
return redirect_to @post, :status => :moved_permanently
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
=end
|
57
|
+
module History
|
58
|
+
|
59
|
+
# Configures the model instance to use the History add-on.
|
60
|
+
def self.included(model_class)
|
61
|
+
model_class.instance_eval do
|
62
|
+
raise "FriendlyId::History is incompatible with FriendlyId::Scoped" if self < Scoped
|
63
|
+
@friendly_id_config.use :slugged
|
64
|
+
has_many :slugs, :as => :sluggable, :dependent => :destroy,
|
65
|
+
:class_name => Slug.to_s, :order => "#{Slug.quoted_table_name}.id DESC"
|
66
|
+
after_save :create_slug
|
67
|
+
relation_class.send :include, FinderMethods
|
68
|
+
friendly_id_config.slug_generator_class.send :include, SlugGenerator
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def create_slug
|
75
|
+
return unless friendly_id
|
76
|
+
return if slugs.first.try(:slug) == friendly_id
|
77
|
+
# Allow reversion back to a previously used slug
|
78
|
+
relation = slugs.where(:slug => friendly_id)
|
79
|
+
result = relation.select("id").lock(true).all
|
80
|
+
relation.delete_all unless result.empty?
|
81
|
+
slugs.create! do |record|
|
82
|
+
record.slug = friendly_id
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Adds a finder that explictly uses slugs from the slug table.
|
87
|
+
module FinderMethods
|
88
|
+
|
89
|
+
# Search for a record in the slugs table using the specified slug.
|
90
|
+
def find_one(id)
|
91
|
+
return super(id) if id.unfriendly_id?
|
92
|
+
where(@klass.friendly_id_config.query_field => id).first or
|
93
|
+
with_old_friendly_id(id) {|x| find_one_without_friendly_id(x)} or
|
94
|
+
find_one_without_friendly_id(id)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Search for a record in the slugs table using the specified slug.
|
98
|
+
def exists?(id = false)
|
99
|
+
return super if id.unfriendly_id?
|
100
|
+
exists_without_friendly_id?(@klass.friendly_id_config.query_field => id) or
|
101
|
+
with_old_friendly_id(id) {|x| exists_without_friendly_id?(x)} or
|
102
|
+
exists_without_friendly_id?(id)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Accepts a slug, and yields a corresponding sluggable_id into the block.
|
108
|
+
def with_old_friendly_id(slug, &block)
|
109
|
+
sql = "SELECT sluggable_id FROM #{Slug.quoted_table_name} WHERE sluggable_type = %s AND slug = %s"
|
110
|
+
sql = sql % [@klass.base_class.to_s, slug].map {|x| connection.quote(x)}
|
111
|
+
sluggable_id = connection.select_values(sql).first
|
112
|
+
yield sluggable_id if sluggable_id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# This module overrides {FriendlyId::SlugGenerator#conflicts} to consider
|
117
|
+
# all historic slugs for that model.
|
118
|
+
module SlugGenerator
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def conflicts
|
123
|
+
sluggable_class = friendly_id_config.model_class.base_class
|
124
|
+
pkey = sluggable_class.primary_key
|
125
|
+
value = sluggable.send pkey
|
126
|
+
|
127
|
+
scope = Slug.where("slug = ? OR slug LIKE ?", normalized, wildcard)
|
128
|
+
scope = scope.where(:sluggable_type => sluggable_class.to_s)
|
129
|
+
scope = scope.where("sluggable_id <> ?", value) unless sluggable.new_record?
|
130
|
+
scope.order("LENGTH(slug) DESC, slug DESC")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateFriendlyIdSlugs < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
create_table :friendly_id_slugs do |t|
|
5
|
+
t.string :slug, :null => false
|
6
|
+
t.integer :sluggable_id, :null => false
|
7
|
+
t.string :sluggable_type, :limit => 40
|
8
|
+
t.datetime :created_at
|
9
|
+
end
|
10
|
+
add_index :friendly_id_slugs, :sluggable_id
|
11
|
+
add_index :friendly_id_slugs, [:slug, :sluggable_type], :unique => true
|
12
|
+
add_index :friendly_id_slugs, :sluggable_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :friendly_id_slugs
|
17
|
+
end
|
18
|
+
end
|