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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +20 -0
  5. data/.yardopts +4 -0
  6. data/Changelog.md +86 -0
  7. data/Gemfile +15 -0
  8. data/Guide.rdoc +553 -0
  9. data/MIT-LICENSE +19 -0
  10. data/README.md +150 -0
  11. data/Rakefile +108 -0
  12. data/WhatsNew.md +95 -0
  13. data/bench.rb +63 -0
  14. data/friendly_id.gemspec +43 -0
  15. data/gemfiles/Gemfile.rails-3.0.rb +21 -0
  16. data/gemfiles/Gemfile.rails-3.1.rb +22 -0
  17. data/gemfiles/Gemfile.rails-3.2.rb +22 -0
  18. data/geothird_friendly_id.gemspec +43 -0
  19. data/lib/friendly_id/base.rb +291 -0
  20. data/lib/friendly_id/configuration.rb +80 -0
  21. data/lib/friendly_id/finder_methods.rb +35 -0
  22. data/lib/friendly_id/globalize.rb +115 -0
  23. data/lib/friendly_id/history.rb +134 -0
  24. data/lib/friendly_id/migration.rb +18 -0
  25. data/lib/friendly_id/object_utils.rb +50 -0
  26. data/lib/friendly_id/reserved.rb +68 -0
  27. data/lib/friendly_id/scoped.rb +149 -0
  28. data/lib/friendly_id/simple_i18n.rb +95 -0
  29. data/lib/friendly_id/slug.rb +14 -0
  30. data/lib/friendly_id/slug_generator.rb +80 -0
  31. data/lib/friendly_id/slugged.rb +329 -0
  32. data/lib/friendly_id.rb +114 -0
  33. data/lib/generators/friendly_id_generator.rb +17 -0
  34. data/test/base_test.rb +72 -0
  35. data/test/compatibility/ancestry/Gemfile +8 -0
  36. data/test/compatibility/ancestry/ancestry_test.rb +34 -0
  37. data/test/compatibility/threading/Gemfile +8 -0
  38. data/test/compatibility/threading/Gemfile.lock +37 -0
  39. data/test/compatibility/threading/threading.rb +45 -0
  40. data/test/configuration_test.rb +48 -0
  41. data/test/core_test.rb +48 -0
  42. data/test/databases.yml +19 -0
  43. data/test/generator_test.rb +20 -0
  44. data/test/globalize_test.rb +57 -0
  45. data/test/helper.rb +87 -0
  46. data/test/history_test.rb +149 -0
  47. data/test/object_utils_test.rb +28 -0
  48. data/test/reserved_test.rb +40 -0
  49. data/test/schema.rb +79 -0
  50. data/test/scoped_test.rb +83 -0
  51. data/test/shared.rb +156 -0
  52. data/test/simple_i18n_test.rb +133 -0
  53. data/test/slugged_test.rb +280 -0
  54. data/test/sti_test.rb +77 -0
  55. 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