geothird_friendly_id 4.0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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