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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/stale.yml +17 -0
- data/.github/workflows/test.yml +58 -0
- data/Changelog.md +41 -0
- data/Gemfile +10 -11
- data/README.md +42 -15
- data/Rakefile +24 -27
- data/bench.rb +30 -27
- data/certs/parndt.pem +27 -0
- data/friendly_id.gemspec +28 -27
- data/gemfiles/Gemfile.rails-5.2.rb +11 -16
- data/gemfiles/Gemfile.rails-6.0.rb +22 -0
- data/gemfiles/Gemfile.rails-6.1.rb +22 -0
- data/gemfiles/Gemfile.rails-7.0.rb +22 -0
- data/guide.rb +5 -5
- data/lib/friendly_id/base.rb +61 -68
- data/lib/friendly_id/candidates.rb +9 -11
- data/lib/friendly_id/configuration.rb +8 -8
- data/lib/friendly_id/finder_methods.rb +72 -13
- data/lib/friendly_id/finders.rb +64 -67
- data/lib/friendly_id/history.rb +72 -66
- data/lib/friendly_id/initializer.rb +5 -5
- data/lib/friendly_id/migration.rb +10 -11
- data/lib/friendly_id/object_utils.rb +2 -2
- data/lib/friendly_id/reserved.rb +28 -32
- data/lib/friendly_id/scoped.rb +105 -103
- data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
- data/lib/friendly_id/sequentially_slugged.rb +21 -58
- data/lib/friendly_id/simple_i18n.rb +75 -69
- data/lib/friendly_id/slug.rb +1 -2
- data/lib/friendly_id/slug_generator.rb +1 -3
- data/lib/friendly_id/slugged.rb +236 -239
- data/lib/friendly_id/version.rb +1 -1
- data/lib/friendly_id.rb +41 -45
- data/lib/generators/friendly_id_generator.rb +9 -9
- data/test/base_test.rb +10 -13
- data/test/benchmarks/finders.rb +28 -26
- data/test/benchmarks/object_utils.rb +13 -13
- data/test/candidates_test.rb +17 -18
- data/test/configuration_test.rb +7 -11
- data/test/core_test.rb +1 -2
- data/test/databases.yml +7 -4
- data/test/finders_test.rb +52 -5
- data/test/generator_test.rb +16 -26
- data/test/helper.rb +33 -20
- data/test/history_test.rb +116 -72
- data/test/numeric_slug_test.rb +31 -0
- data/test/object_utils_test.rb +0 -2
- data/test/reserved_test.rb +9 -11
- data/test/schema.rb +5 -4
- data/test/scoped_test.rb +26 -15
- data/test/sequentially_slugged_test.rb +107 -33
- data/test/shared.rb +17 -18
- data/test/simple_i18n_test.rb +23 -13
- data/test/slugged_test.rb +254 -78
- data/test/sti_test.rb +19 -21
- data.tar.gz.sig +0 -0
- metadata +49 -19
- metadata.gz.sig +1 -0
- data/.travis.yml +0 -57
- data/gemfiles/Gemfile.rails-4.0.rb +0 -30
- data/gemfiles/Gemfile.rails-4.1.rb +0 -29
- data/gemfiles/Gemfile.rails-4.2.rb +0 -28
- data/gemfiles/Gemfile.rails-5.0.rb +0 -28
- data/gemfiles/Gemfile.rails-5.1.rb +0 -27
data/lib/friendly_id/finders.rb
CHANGED
@@ -1,73 +1,70 @@
|
|
1
1
|
module FriendlyId
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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(
|
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
|
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?(
|
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)
|
data/lib/friendly_id/history.rb
CHANGED
@@ -1,59 +1,55 @@
|
|
1
1
|
module FriendlyId
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
:
|
77
|
-
:
|
78
|
-
:
|
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 +
|
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
|
-
|
115
|
-
|
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
|
121
|
+
return if history_is_up_to_date?
|
125
122
|
# Allow reversion back to a previously used slug
|
126
|
-
relation = slugs.where(:
|
123
|
+
relation = slugs.where(slug: friendly_id)
|
127
124
|
if friendly_id_config.uses?(:scoped)
|
128
|
-
relation = relation.where(:
|
125
|
+
relation = relation.where(scope: serialized_scope)
|
129
126
|
end
|
130
|
-
relation.
|
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
|
20
|
-
stylesheets assets javascripts images
|
21
|
-
|
22
|
-
# This adds an option to
|
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
|
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
|
-
|
1
|
+
MIGRATION_CLASS =
|
2
2
|
if ActiveRecord::VERSION::MAJOR >= 5
|
3
|
-
ActiveRecord::Migration[
|
3
|
+
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
4
4
|
else
|
5
5
|
ActiveRecord::Migration
|
6
6
|
end
|
7
7
|
|
8
|
-
class CreateFriendlyIdSlugs <
|
8
|
+
class CreateFriendlyIdSlugs < MIGRATION_CLASS
|
9
9
|
def change
|
10
10
|
create_table :friendly_id_slugs do |t|
|
11
|
-
t.string
|
12
|
-
t.integer
|
13
|
-
t.string
|
14
|
-
t.string
|
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: {
|
19
|
-
add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {
|
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?
|
47
|
+
val = friendly_id?
|
48
|
+
!val unless val.nil?
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
data/lib/friendly_id/reserved.rb
CHANGED
@@ -1,42 +1,38 @@
|
|
1
1
|
module FriendlyId
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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, :
|
35
|
+
validates_exclusion_of :friendly_id, in: ->(_) {
|
40
36
|
friendly_id_config.reserved_words || []
|
41
37
|
}
|
42
38
|
end
|