friendly_id 5.2.4 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/stale.yml +17 -0
  6. data/.github/workflows/test.yml +58 -0
  7. data/Changelog.md +41 -0
  8. data/Gemfile +10 -11
  9. data/README.md +42 -15
  10. data/Rakefile +24 -27
  11. data/bench.rb +30 -27
  12. data/certs/parndt.pem +27 -0
  13. data/friendly_id.gemspec +28 -27
  14. data/gemfiles/Gemfile.rails-5.2.rb +11 -16
  15. data/gemfiles/Gemfile.rails-6.0.rb +22 -0
  16. data/gemfiles/Gemfile.rails-6.1.rb +22 -0
  17. data/gemfiles/Gemfile.rails-7.0.rb +22 -0
  18. data/guide.rb +5 -5
  19. data/lib/friendly_id/base.rb +61 -68
  20. data/lib/friendly_id/candidates.rb +9 -11
  21. data/lib/friendly_id/configuration.rb +8 -8
  22. data/lib/friendly_id/finder_methods.rb +72 -13
  23. data/lib/friendly_id/finders.rb +64 -67
  24. data/lib/friendly_id/history.rb +72 -66
  25. data/lib/friendly_id/initializer.rb +5 -5
  26. data/lib/friendly_id/migration.rb +10 -11
  27. data/lib/friendly_id/object_utils.rb +2 -2
  28. data/lib/friendly_id/reserved.rb +28 -32
  29. data/lib/friendly_id/scoped.rb +105 -103
  30. data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
  31. data/lib/friendly_id/sequentially_slugged.rb +21 -58
  32. data/lib/friendly_id/simple_i18n.rb +75 -69
  33. data/lib/friendly_id/slug.rb +1 -2
  34. data/lib/friendly_id/slug_generator.rb +1 -3
  35. data/lib/friendly_id/slugged.rb +236 -239
  36. data/lib/friendly_id/version.rb +1 -1
  37. data/lib/friendly_id.rb +41 -45
  38. data/lib/generators/friendly_id_generator.rb +9 -9
  39. data/test/base_test.rb +10 -13
  40. data/test/benchmarks/finders.rb +28 -26
  41. data/test/benchmarks/object_utils.rb +13 -13
  42. data/test/candidates_test.rb +17 -18
  43. data/test/configuration_test.rb +7 -11
  44. data/test/core_test.rb +1 -2
  45. data/test/databases.yml +7 -4
  46. data/test/finders_test.rb +52 -5
  47. data/test/generator_test.rb +16 -26
  48. data/test/helper.rb +33 -20
  49. data/test/history_test.rb +116 -72
  50. data/test/numeric_slug_test.rb +31 -0
  51. data/test/object_utils_test.rb +0 -2
  52. data/test/reserved_test.rb +9 -11
  53. data/test/schema.rb +5 -4
  54. data/test/scoped_test.rb +26 -15
  55. data/test/sequentially_slugged_test.rb +107 -33
  56. data/test/shared.rb +17 -18
  57. data/test/simple_i18n_test.rb +23 -13
  58. data/test/slugged_test.rb +254 -78
  59. data/test/sti_test.rb +19 -21
  60. data.tar.gz.sig +0 -0
  61. metadata +49 -19
  62. metadata.gz.sig +1 -0
  63. data/.travis.yml +0 -57
  64. data/gemfiles/Gemfile.rails-4.0.rb +0 -30
  65. data/gemfiles/Gemfile.rails-4.1.rb +0 -29
  66. data/gemfiles/Gemfile.rails-4.2.rb +0 -28
  67. data/gemfiles/Gemfile.rails-5.0.rb +0 -28
  68. data/gemfiles/Gemfile.rails-5.1.rb +0 -27
@@ -1,73 +1,70 @@
1
1
  module FriendlyId
2
- =begin
3
- ## Performing Finds with FriendlyId
4
-
5
- FriendlyId offers enhanced finders which will search for your record by
6
- friendly id, and fall back to the numeric id if necessary. This makes it easy
7
- to add FriendlyId to an existing application with minimal code modification.
8
-
9
- By default, these methods are available only on the `friendly` scope:
10
-
11
- Restaurant.friendly.find('plaza-diner') #=> works
12
- Restaurant.friendly.find(23) #=> also works
13
- Restaurant.find(23) #=> still works
14
- Restaurant.find('plaza-diner') #=> will not work
15
-
16
- ### Restoring FriendlyId 4.0-style finders
17
-
18
- Prior to version 5.0, FriendlyId overrode the default finder methods to perform
19
- friendly finds all the time. This required modifying parts of Rails that did
20
- not have a public API, which was harder to maintain and at times caused
21
- compatiblity problems. In 5.0 we decided to change the library's defaults and add
22
- the friendly finder methods only to the `friendly` scope in order to boost
23
- compatiblity. However, you can still opt-in to original functionality very
24
- easily by using the `:finders` addon:
25
-
26
- class Restaurant < ActiveRecord::Base
27
- extend FriendlyId
28
-
29
- scope :active, -> {where(:active => true)}
30
-
31
- friendly_id :name, :use => [:slugged, :finders]
32
- end
33
-
34
- Restaurant.friendly.find('plaza-diner') #=> works
35
- Restaurant.find('plaza-diner') #=> now also works
36
- Restaurant.active.find('plaza-diner') #=> now also works
37
-
38
- ### Updating your application to use FriendlyId's finders
39
-
40
- Unless you've chosen to use the `:finders` addon, be sure to modify the finders
41
- in your controllers to use the `friendly` scope. For example:
42
-
43
- # before
44
- def set_restaurant
45
- @restaurant = Restaurant.find(params[:id])
46
- end
47
-
48
- # after
49
- def set_restaurant
50
- @restaurant = Restaurant.friendly.find(params[:id])
51
- end
52
-
53
- #### Active Admin
54
-
55
- Unless you use the `:finders` addon, you should modify your admin controllers
56
- for models that use FriendlyId with something similar to the following:
57
-
58
- controller do
59
- def find_resource
60
- scoped_collection.friendly.find(params[:id])
61
- end
62
- end
63
-
64
- =end
2
+ # ## Performing Finds with FriendlyId
3
+ #
4
+ # FriendlyId offers enhanced finders which will search for your record by
5
+ # friendly id, and fall back to the numeric id if necessary. This makes it easy
6
+ # to add FriendlyId to an existing application with minimal code modification.
7
+ #
8
+ # By default, these methods are available only on the `friendly` scope:
9
+ #
10
+ # Restaurant.friendly.find('plaza-diner') #=> works
11
+ # Restaurant.friendly.find(23) #=> also works
12
+ # Restaurant.find(23) #=> still works
13
+ # Restaurant.find('plaza-diner') #=> will not work
14
+ #
15
+ ### Restoring FriendlyId 4.0-style finders
16
+ #
17
+ # Prior to version 5.0, FriendlyId overrode the default finder methods to perform
18
+ # friendly finds all the time. This required modifying parts of Rails that did
19
+ # not have a public API, which was harder to maintain and at times caused
20
+ # compatiblity problems. In 5.0 we decided to change the library's defaults and add
21
+ # the friendly finder methods only to the `friendly` scope in order to boost
22
+ # compatiblity. However, you can still opt-in to original functionality very
23
+ # easily by using the `:finders` addon:
24
+ #
25
+ # class Restaurant < ActiveRecord::Base
26
+ # extend FriendlyId
27
+ #
28
+ # scope :active, -> {where(:active => true)}
29
+ #
30
+ # friendly_id :name, :use => [:slugged, :finders]
31
+ # end
32
+ #
33
+ # Restaurant.friendly.find('plaza-diner') #=> works
34
+ # Restaurant.find('plaza-diner') #=> now also works
35
+ # Restaurant.active.find('plaza-diner') #=> now also works
36
+ #
37
+ ### Updating your application to use FriendlyId's finders
38
+ #
39
+ # Unless you've chosen to use the `:finders` addon, be sure to modify the finders
40
+ # in your controllers to use the `friendly` scope. For example:
41
+ #
42
+ # # before
43
+ # def set_restaurant
44
+ # @restaurant = Restaurant.find(params[:id])
45
+ # end
46
+ #
47
+ # # after
48
+ # def set_restaurant
49
+ # @restaurant = Restaurant.friendly.find(params[:id])
50
+ # end
51
+ #
52
+ #### Active Admin
53
+ #
54
+ # Unless you use the `:finders` addon, you should modify your admin controllers
55
+ # for models that use FriendlyId with something similar to the following:
56
+ #
57
+ # controller do
58
+ # def find_resource
59
+ # scoped_collection.friendly.find(params[:id])
60
+ # end
61
+ # end
62
+ #
65
63
  module Finders
66
-
67
64
  module ClassMethods
68
65
  if (ActiveRecord::VERSION::MAJOR == 4) && (ActiveRecord::VERSION::MINOR == 0)
69
66
  def relation_delegate_class(klass)
70
- relation_class_name = :"#{klass.to_s.gsub('::', '_')}_#{self.to_s.gsub('::', '_')}"
67
+ relation_class_name = :"#{klass.to_s.gsub("::", "_")}_#{to_s.gsub("::", "_")}"
71
68
  klass.const_get(relation_class_name)
72
69
  end
73
70
  end
@@ -76,13 +73,13 @@ for models that use FriendlyId with something similar to the following:
76
73
  def self.setup(model_class)
77
74
  model_class.instance_eval do
78
75
  relation.class.send(:include, friendly_id_config.finder_methods)
79
- if (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 2) || ActiveRecord::VERSION::MAJOR == 5
76
+ if (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 2) || ActiveRecord::VERSION::MAJOR >= 5
80
77
  model_class.send(:extend, friendly_id_config.finder_methods)
81
78
  end
82
79
  end
83
80
 
84
81
  # Support for friendly finds on associations for Rails 4.0.1 and above.
85
- if ::ActiveRecord.const_defined?('AssociationRelation')
82
+ if ::ActiveRecord.const_defined?("AssociationRelation")
86
83
  model_class.extend(ClassMethods)
87
84
  association_relation_delegate_class = model_class.relation_delegate_class(::ActiveRecord::AssociationRelation)
88
85
  association_relation_delegate_class.send(:include, model_class.friendly_id_config.finder_methods)
@@ -1,59 +1,55 @@
1
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
- Because recording slug history requires creating additional database records,
27
- this module has an impact on the performance of the associated model's `create`
28
- method.
29
-
30
- ### Example
31
-
32
- class Post < ActiveRecord::Base
33
- extend FriendlyId
34
- friendly_id :title, :use => :history
35
- end
36
-
37
- class PostsController < ApplicationController
38
-
39
- before_filter :find_post
40
-
41
- ...
42
-
43
- def find_post
44
- @post = Post.friendly.find params[:id]
45
-
46
- # If an old id or a numeric id was used to find the record, then
47
- # the request path will not match the post_path, and we should do
48
- # a 301 redirect that uses the current friendly id.
49
- if request.path != post_path(@post)
50
- return redirect_to @post, :status => :moved_permanently
51
- end
52
- end
53
- end
54
- =end
2
+ #
3
+ ## History: Avoiding 404's When Slugs Change
4
+ #
5
+ # FriendlyId's {FriendlyId::History History} module adds the ability to store a
6
+ # log of a model's slugs, so that when its friendly id changes, it's still
7
+ # possible to perform finds by the old id.
8
+ #
9
+ # The primary use case for this is avoiding broken URLs.
10
+ #
11
+ ### Setup
12
+ #
13
+ # In order to use this module, you must add a table to your database schema to
14
+ # store the slug records. FriendlyId provides a generator for this purpose:
15
+ #
16
+ # rails generate friendly_id
17
+ # rake db:migrate
18
+ #
19
+ # This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
20
+ # model.
21
+ #
22
+ ### Considerations
23
+ #
24
+ # Because recording slug history requires creating additional database records,
25
+ # this module has an impact on the performance of the associated model's `create`
26
+ # method.
27
+ #
28
+ ### Example
29
+ #
30
+ # class Post < ActiveRecord::Base
31
+ # extend FriendlyId
32
+ # friendly_id :title, :use => :history
33
+ # end
34
+ #
35
+ # class PostsController < ApplicationController
36
+ #
37
+ # before_filter :find_post
38
+ #
39
+ # ...
40
+ #
41
+ # def find_post
42
+ # @post = Post.friendly.find params[:id]
43
+ #
44
+ # # If an old id or a numeric id was used to find the record, then
45
+ # # the request slug will not match the current slug, and we should do
46
+ # # a 301 redirect to the new path
47
+ # if params[:id] != @post.slug
48
+ # return redirect_to @post, :status => :moved_permanently
49
+ # end
50
+ # end
51
+ # end
55
52
  module History
56
-
57
53
  module Configuration
58
54
  def dependent_value
59
55
  dependent.nil? ? :destroy : dependent
@@ -72,10 +68,10 @@ method.
72
68
  # Configures the model instance to use the History add-on.
73
69
  def self.included(model_class)
74
70
  model_class.class_eval do
75
- has_many :slugs, -> {order(id: :desc)}, {
76
- :as => :sluggable,
77
- :dependent => @friendly_id_config.dependent_value,
78
- :class_name => Slug.to_s
71
+ has_many :slugs, -> { order(id: :desc) }, **{
72
+ as: :sluggable,
73
+ dependent: @friendly_id_config.dependent_value,
74
+ class_name: Slug.to_s
79
75
  }
80
76
 
81
77
  after_save :create_slug
@@ -96,7 +92,7 @@ method.
96
92
  end
97
93
 
98
94
  def slug_table_record(id)
99
- select(quoted_table_name + '.*').joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
95
+ select(quoted_table_name + ".*").joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
100
96
  end
101
97
 
102
98
  def slug_history_clause(id)
@@ -110,9 +106,10 @@ method.
110
106
  # to be conflicts. This will allow a record to revert to a previously
111
107
  # used slug.
112
108
  def scope_for_slug_generator
113
- relation = super
114
- return relation if new_record?
115
- relation = relation.joins(:slugs).merge(Slug.where('sluggable_id <> ?', id))
109
+ relation = super.joins(:slugs)
110
+ unless new_record?
111
+ relation = relation.merge(Slug.where("sluggable_id <> ?", id))
112
+ end
116
113
  if friendly_id_config.uses?(:scoped)
117
114
  relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
118
115
  end
@@ -121,17 +118,26 @@ method.
121
118
 
122
119
  def create_slug
123
120
  return unless friendly_id
124
- return if slugs.first.try(:slug) == friendly_id
121
+ return if history_is_up_to_date?
125
122
  # Allow reversion back to a previously used slug
126
- relation = slugs.where(:slug => friendly_id)
123
+ relation = slugs.where(slug: friendly_id)
127
124
  if friendly_id_config.uses?(:scoped)
128
- relation = relation.where(:scope => serialized_scope)
125
+ relation = relation.where(scope: serialized_scope)
129
126
  end
130
- relation.delete_all
127
+ relation.destroy_all unless relation.empty?
131
128
  slugs.create! do |record|
132
129
  record.slug = friendly_id
133
130
  record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
134
131
  end
135
132
  end
133
+
134
+ def history_is_up_to_date?
135
+ latest_history = slugs.first
136
+ check = latest_history.try(:slug) == friendly_id
137
+ if friendly_id_config.uses?(:scoped)
138
+ check &&= latest_history.scope == serialized_scope
139
+ end
140
+ check
141
+ end
136
142
  end
137
143
  end
@@ -16,10 +16,10 @@ FriendlyId.defaults do |config|
16
16
  # undesirable to allow as slugs. Edit this list as needed for your app.
17
17
  config.use :reserved
18
18
 
19
- config.reserved_words = %w(new edit index session login logout users admin
20
- stylesheets assets javascripts images)
21
-
22
- # This adds an option to to treat reserved words as conflicts rather than exceptions.
19
+ config.reserved_words = %w[new edit index session login logout users admin
20
+ stylesheets assets javascripts images]
21
+
22
+ # This adds an option to treat reserved words as conflicts rather than exceptions.
23
23
  # When there is no good candidate, a UUID will be appended, matching the existing
24
24
  # conflict behavior.
25
25
 
@@ -37,7 +37,7 @@ FriendlyId.defaults do |config|
37
37
  # MyModel.find('foo')
38
38
  #
39
39
  # This is significantly more convenient but may not be appropriate for
40
- # all applications, so you must explicity opt-in to this behavior. You can
40
+ # all applications, so you must explicitly opt-in to this behavior. You can
41
41
  # always also configure it on a per-model basis if you prefer.
42
42
  #
43
43
  # Something else to consider is that using the :finders addon boosts
@@ -1,22 +1,21 @@
1
- migration_class =
1
+ MIGRATION_CLASS =
2
2
  if ActiveRecord::VERSION::MAJOR >= 5
3
- ActiveRecord::Migration[4.2]
3
+ ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
4
4
  else
5
5
  ActiveRecord::Migration
6
6
  end
7
7
 
8
- class CreateFriendlyIdSlugs < migration_class
8
+ class CreateFriendlyIdSlugs < MIGRATION_CLASS
9
9
  def change
10
10
  create_table :friendly_id_slugs do |t|
11
- t.string :slug, :null => false
12
- t.integer :sluggable_id, :null => false
13
- t.string :sluggable_type, :limit => 50
14
- t.string :scope
11
+ t.string :slug, null: false
12
+ t.integer :sluggable_id, null: false
13
+ t.string :sluggable_type, limit: 50
14
+ t.string :scope
15
15
  t.datetime :created_at
16
16
  end
17
- add_index :friendly_id_slugs, :sluggable_id
18
- add_index :friendly_id_slugs, [:slug, :sluggable_type], length: { slug: 140, sluggable_type: 50 }
19
- add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
20
- add_index :friendly_id_slugs, :sluggable_type
17
+ add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
18
+ add_index :friendly_id_slugs, [:slug, :sluggable_type], length: {slug: 140, sluggable_type: 50}
19
+ add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
21
20
  end
22
21
  end
@@ -19,7 +19,6 @@ module FriendlyId
19
19
  # names that unambigously refer to the library of their origin, which should
20
20
  # be sufficient to avoid conflicts with other libraries.
21
21
  module ObjectUtils
22
-
23
22
  # True if the id is definitely friendly, false if definitely unfriendly,
24
23
  # else nil.
25
24
  #
@@ -45,7 +44,8 @@ module FriendlyId
45
44
  # True if the id is definitely unfriendly, false if definitely friendly,
46
45
  # else nil.
47
46
  def unfriendly_id?
48
- val = friendly_id? ; !val unless val.nil?
47
+ val = friendly_id?
48
+ !val unless val.nil?
49
49
  end
50
50
  end
51
51
 
@@ -1,42 +1,38 @@
1
1
  module FriendlyId
2
-
3
- =begin
4
-
5
- ## Reserved Words
6
-
7
- The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
8
- words from use as FriendlyId slugs.
9
-
10
- With Ruby on Rails, FriendlyId's generator generates an initializer that
11
- reserves some words such as "new" and "edit" using {FriendlyId.defaults
12
- FriendlyId.defaults}.
13
-
14
- Note that the error messages for fields will appear on the field
15
- `:friendly_id`. If you are using Rails's scaffolded form errors display, then
16
- it will have no field to highlight. If you'd like to change this so that
17
- scaffolding works as expected, one way to accomplish this is to move the error
18
- message to a different field. For example:
19
-
20
- class Person < ActiveRecord::Base
21
- extend FriendlyId
22
- friendly_id :name, use: :slugged
23
-
24
- after_validation :move_friendly_id_error_to_name
25
-
26
- def move_friendly_id_error_to_name
27
- errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
28
- end
29
- end
30
-
31
- =end
2
+ #
3
+ ## Reserved Words
4
+ #
5
+ # The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
6
+ # words from use as FriendlyId slugs.
7
+ #
8
+ # With Ruby on Rails, FriendlyId's generator generates an initializer that
9
+ # reserves some words such as "new" and "edit" using {FriendlyId.defaults
10
+ # FriendlyId.defaults}.
11
+ #
12
+ # Note that the error messages for fields will appear on the field
13
+ # `:friendly_id`. If you are using Rails's scaffolded form errors display, then
14
+ # it will have no field to highlight. If you'd like to change this so that
15
+ # scaffolding works as expected, one way to accomplish this is to move the error
16
+ # message to a different field. For example:
17
+ #
18
+ # class Person < ActiveRecord::Base
19
+ # extend FriendlyId
20
+ # friendly_id :name, use: :slugged
21
+ #
22
+ # after_validation :move_friendly_id_error_to_name
23
+ #
24
+ # def move_friendly_id_error_to_name
25
+ # errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
26
+ # end
27
+ # end
28
+ #
32
29
  module Reserved
33
-
34
30
  # When included, this module adds configuration options to the model class's
35
31
  # friendly_id_config.
36
32
  def self.included(model_class)
37
33
  model_class.class_eval do
38
34
  friendly_id_config.class.send :include, Reserved::Configuration
39
- validates_exclusion_of :friendly_id, :in => ->(_) {
35
+ validates_exclusion_of :friendly_id, in: ->(_) {
40
36
  friendly_id_config.reserved_words || []
41
37
  }
42
38
  end