friendly_id 5.2.4 → 5.5.0

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 (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