friendly_id 5.2.2 → 5.4.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.tar.gz.sig +2 -0
- data/.github/stale.yml +17 -0
- data/.github/workflows/test.yml +60 -0
- data/Changelog.md +39 -1
- data/Gemfile +3 -0
- data/README.md +54 -164
- data/Rakefile +2 -2
- data/UPGRADING.md +115 -0
- data/certs/parndt.pem +25 -0
- data/friendly_id.gemspec +9 -5
- data/gemfiles/Gemfile.rails-5.0.rb +2 -2
- data/gemfiles/{Gemfile.rails-4.2.rb → Gemfile.rails-5.1.rb} +4 -5
- data/gemfiles/{Gemfile.rails-4.1.rb → Gemfile.rails-5.2.rb} +5 -7
- data/gemfiles/{Gemfile.rails-4.0.rb → Gemfile.rails-6.0.rb} +5 -8
- data/lib/friendly_id/base.rb +4 -8
- data/lib/friendly_id/candidates.rb +0 -2
- data/lib/friendly_id/configuration.rb +3 -2
- data/lib/friendly_id/finder_methods.rb +18 -7
- data/lib/friendly_id/finders.rb +1 -1
- data/lib/friendly_id/history.rb +21 -12
- data/lib/friendly_id/initializer.rb +11 -0
- data/lib/friendly_id/migration.rb +9 -3
- data/lib/friendly_id/object_utils.rb +9 -2
- data/lib/friendly_id/reserved.rb +1 -0
- data/lib/friendly_id/scoped.rb +9 -2
- data/lib/friendly_id/sequentially_slugged.rb +12 -2
- data/lib/friendly_id/slug.rb +4 -0
- data/lib/friendly_id/slug_generator.rb +6 -1
- data/lib/friendly_id/slugged.rb +3 -3
- data/lib/friendly_id/version.rb +1 -1
- data/test/databases.yml +6 -4
- data/test/finders_test.rb +24 -0
- data/test/helper.rb +13 -3
- data/test/history_test.rb +86 -7
- data/test/numeric_slug_test.rb +31 -0
- data/test/object_utils_test.rb +5 -3
- data/test/reserved_test.rb +10 -0
- data/test/schema.rb +19 -2
- data/test/scoped_test.rb +13 -0
- data/test/sequentially_slugged_test.rb +59 -0
- data/test/shared.rb +4 -4
- data/test/simple_i18n_test.rb +2 -2
- data/test/slugged_test.rb +168 -4
- metadata +48 -19
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -40
@@ -18,6 +18,12 @@ FriendlyId.defaults do |config|
|
|
18
18
|
|
19
19
|
config.reserved_words = %w(new edit index session login logout users admin
|
20
20
|
stylesheets assets javascripts images)
|
21
|
+
|
22
|
+
# This adds an option to treat reserved words as conflicts rather than exceptions.
|
23
|
+
# When there is no good candidate, a UUID will be appended, matching the existing
|
24
|
+
# conflict behavior.
|
25
|
+
|
26
|
+
# config.treat_reserved_as_conflict = true
|
21
27
|
|
22
28
|
# ## Friendly Finders
|
23
29
|
#
|
@@ -76,7 +82,12 @@ FriendlyId.defaults do |config|
|
|
76
82
|
# behavior by overriding the `should_generate_new_friendly_id?` method that
|
77
83
|
# FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
|
78
84
|
# more like 4.0.
|
85
|
+
# Note: Use(include) Slugged module in the config if using the anonymous module.
|
86
|
+
# If you have `friendly_id :name, use: slugged` in the model, Slugged module
|
87
|
+
# is included after the anonymous module defined in the initializer, so it
|
88
|
+
# overrides the `should_generate_new_friendly_id?` method from the anonymous module.
|
79
89
|
#
|
90
|
+
# config.use :slugged
|
80
91
|
# config.use Module.new {
|
81
92
|
# def should_generate_new_friendly_id?
|
82
93
|
# slug.blank? || <your_column_name_here>_changed?
|
@@ -1,4 +1,11 @@
|
|
1
|
-
|
1
|
+
MIGRATION_CLASS =
|
2
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
3
|
+
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
4
|
+
else
|
5
|
+
ActiveRecord::Migration
|
6
|
+
end
|
7
|
+
|
8
|
+
class CreateFriendlyIdSlugs < MIGRATION_CLASS
|
2
9
|
def change
|
3
10
|
create_table :friendly_id_slugs do |t|
|
4
11
|
t.string :slug, :null => false
|
@@ -7,9 +14,8 @@ class CreateFriendlyIdSlugs < ActiveRecord::Migration
|
|
7
14
|
t.string :scope
|
8
15
|
t.datetime :created_at
|
9
16
|
end
|
10
|
-
add_index :friendly_id_slugs, :sluggable_id
|
17
|
+
add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
|
11
18
|
add_index :friendly_id_slugs, [:slug, :sluggable_type], length: { slug: 140, sluggable_type: 50 }
|
12
19
|
add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
|
13
|
-
add_index :friendly_id_slugs, :sluggable_type
|
14
20
|
end
|
15
21
|
end
|
@@ -2,7 +2,6 @@ module FriendlyId
|
|
2
2
|
# Instances of these classes will never be considered a friendly id.
|
3
3
|
# @see FriendlyId::ObjectUtils#friendly_id
|
4
4
|
UNFRIENDLY_CLASSES = [
|
5
|
-
ActiveRecord::Base,
|
6
5
|
Array,
|
7
6
|
FalseClass,
|
8
7
|
Hash,
|
@@ -59,6 +58,10 @@ module FriendlyId
|
|
59
58
|
true
|
60
59
|
end
|
61
60
|
end
|
61
|
+
|
62
|
+
def self.mark_as_unfriendly(klass)
|
63
|
+
klass.send(:include, FriendlyId::UnfriendlyUtils)
|
64
|
+
end
|
62
65
|
end
|
63
66
|
|
64
67
|
Object.send :include, FriendlyId::ObjectUtils
|
@@ -66,4 +69,8 @@ Object.send :include, FriendlyId::ObjectUtils
|
|
66
69
|
# Considered unfriendly if object is an instance of an unfriendly class or
|
67
70
|
# one of its descendants.
|
68
71
|
|
69
|
-
FriendlyId::UNFRIENDLY_CLASSES.each { |klass|
|
72
|
+
FriendlyId::UNFRIENDLY_CLASSES.each { |klass| FriendlyId.mark_as_unfriendly(klass) }
|
73
|
+
|
74
|
+
ActiveSupport.on_load(:active_record) do
|
75
|
+
FriendlyId.mark_as_unfriendly(ActiveRecord::Base)
|
76
|
+
end
|
data/lib/friendly_id/reserved.rb
CHANGED
data/lib/friendly_id/scoped.rb
CHANGED
@@ -122,7 +122,10 @@ an example of one way to set this up:
|
|
122
122
|
end
|
123
123
|
|
124
124
|
def scope_for_slug_generator
|
125
|
-
|
125
|
+
if friendly_id_config.uses?(:History)
|
126
|
+
return super
|
127
|
+
end
|
128
|
+
relation = self.class.base_class.unscoped.friendly
|
126
129
|
friendly_id_config.scope_columns.each do |column|
|
127
130
|
relation = relation.where(column => send(column))
|
128
131
|
end
|
@@ -132,10 +135,14 @@ an example of one way to set this up:
|
|
132
135
|
private :scope_for_slug_generator
|
133
136
|
|
134
137
|
def slug_generator
|
135
|
-
friendly_id_config.slug_generator_class.new(scope_for_slug_generator)
|
138
|
+
friendly_id_config.slug_generator_class.new(scope_for_slug_generator, friendly_id_config)
|
136
139
|
end
|
137
140
|
private :slug_generator
|
138
141
|
|
142
|
+
def should_generate_new_friendly_id?
|
143
|
+
(changed & friendly_id_config.scope_columns).any? || super
|
144
|
+
end
|
145
|
+
|
139
146
|
# This module adds the `:scope` configuration option to
|
140
147
|
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
141
148
|
module Configuration
|
@@ -11,7 +11,7 @@ module FriendlyId
|
|
11
11
|
candidate,
|
12
12
|
friendly_id_config.slug_column,
|
13
13
|
friendly_id_config.sequence_separator,
|
14
|
-
|
14
|
+
slug_base_class).next_slug
|
15
15
|
end
|
16
16
|
|
17
17
|
class SequentialSlugCalculator
|
@@ -47,7 +47,7 @@ module FriendlyId
|
|
47
47
|
def slug_conflicts
|
48
48
|
scope.
|
49
49
|
where(conflict_query, slug, sequential_slug_matcher).
|
50
|
-
order(ordering_query).pluck(slug_column)
|
50
|
+
order(Arel.sql(ordering_query)).pluck(Arel.sql(slug_column))
|
51
51
|
end
|
52
52
|
|
53
53
|
def conflict_query
|
@@ -73,5 +73,15 @@ module FriendlyId
|
|
73
73
|
"#{length_command}(#{slug_column}) ASC, #{slug_column} ASC"
|
74
74
|
end
|
75
75
|
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def slug_base_class
|
80
|
+
if friendly_id_config.uses?(:history)
|
81
|
+
Slug
|
82
|
+
else
|
83
|
+
self.class.base_class
|
84
|
+
end
|
85
|
+
end
|
76
86
|
end
|
77
87
|
end
|
data/lib/friendly_id/slug.rb
CHANGED
@@ -3,11 +3,16 @@ module FriendlyId
|
|
3
3
|
# availability.
|
4
4
|
class SlugGenerator
|
5
5
|
|
6
|
-
def initialize(scope)
|
6
|
+
def initialize(scope, config)
|
7
7
|
@scope = scope
|
8
|
+
@config = config
|
8
9
|
end
|
9
10
|
|
10
11
|
def available?(slug)
|
12
|
+
if @config.uses?(::FriendlyId::Reserved) && @config.reserved_words.present? && @config.treat_reserved_as_conflict
|
13
|
+
return false if @config.reserved_words.include?(slug)
|
14
|
+
end
|
15
|
+
|
11
16
|
!@scope.exists_by_friendly_id?(slug)
|
12
17
|
end
|
13
18
|
|
data/lib/friendly_id/slugged.rb
CHANGED
@@ -67,7 +67,7 @@ app's behavior and requirements.
|
|
67
67
|
#### Formatting
|
68
68
|
|
69
69
|
By default, FriendlyId uses Active Support's
|
70
|
-
[
|
70
|
+
[parameterize](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize)
|
71
71
|
method to create slugs. This method will intelligently replace spaces with
|
72
72
|
dashes, and Unicode Latin characters with ASCII approximations:
|
73
73
|
|
@@ -370,12 +370,12 @@ Github issue](https://github.com/norman/friendly_id/issues/185) for discussion.
|
|
370
370
|
private :scope_for_slug_generator
|
371
371
|
|
372
372
|
def slug_generator
|
373
|
-
friendly_id_config.slug_generator_class.new(scope_for_slug_generator)
|
373
|
+
friendly_id_config.slug_generator_class.new(scope_for_slug_generator, friendly_id_config)
|
374
374
|
end
|
375
375
|
private :slug_generator
|
376
376
|
|
377
377
|
def unset_slug_if_invalid
|
378
|
-
if errors.present? && attribute_changed?(friendly_id_config.query_field.to_s)
|
378
|
+
if errors[friendly_id_config.query_field].present? && attribute_changed?(friendly_id_config.query_field.to_s)
|
379
379
|
diff = changes[friendly_id_config.query_field]
|
380
380
|
send "#{friendly_id_config.slug_column}=", diff.first
|
381
381
|
end
|
data/lib/friendly_id/version.rb
CHANGED
data/test/databases.yml
CHANGED
@@ -2,14 +2,16 @@ mysql:
|
|
2
2
|
adapter: mysql2
|
3
3
|
database: friendly_id_test
|
4
4
|
username: root
|
5
|
-
|
5
|
+
password: <%= ENV['MYSQL_PASSWORD'] %>
|
6
|
+
host: 127.0.0.1
|
7
|
+
port: 3306
|
6
8
|
encoding: utf8
|
7
9
|
|
8
10
|
postgres:
|
9
11
|
adapter: postgresql
|
10
|
-
host: localhost
|
11
|
-
port: 5432
|
12
|
-
username: postgres
|
12
|
+
host: <%= ENV.fetch('PGHOST') { 'localhost' } %>
|
13
|
+
port: <%= ENV.fetch('PGPORT') { '5432' } %>
|
14
|
+
username: <%= ENV.fetch('PGUSER') { 'postgres' } %>
|
13
15
|
database: friendly_id_test
|
14
16
|
encoding: utf8
|
15
17
|
|
data/test/finders_test.rb
CHANGED
@@ -26,4 +26,28 @@ class Finders < TestCaseClass
|
|
26
26
|
assert model_class.existing.find(record.friendly_id)
|
27
27
|
end
|
28
28
|
end
|
29
|
+
|
30
|
+
test 'should find capitalized records with finders as class methods' do
|
31
|
+
with_instance_of(model_class) do |record|
|
32
|
+
assert model_class.find(record.friendly_id.capitalize)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'should find capitalized records with finders on relations' do
|
37
|
+
with_instance_of(model_class) do |record|
|
38
|
+
assert model_class.existing.find(record.friendly_id.capitalize)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'should find upcased records with finders as class methods' do
|
43
|
+
with_instance_of(model_class) do |record|
|
44
|
+
assert model_class.find(record.friendly_id.upcase)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
test 'should find upcased records with finders on relations' do
|
49
|
+
with_instance_of(model_class) do |record|
|
50
|
+
assert model_class.existing.find(record.friendly_id.upcase)
|
51
|
+
end
|
52
|
+
end
|
29
53
|
end
|
data/test/helper.rb
CHANGED
@@ -27,6 +27,7 @@ end
|
|
27
27
|
require "mocha/setup"
|
28
28
|
require "active_record"
|
29
29
|
require 'active_support/core_ext/time/conversions'
|
30
|
+
require 'erb'
|
30
31
|
|
31
32
|
I18n.enforce_available_locales = false
|
32
33
|
|
@@ -38,6 +39,10 @@ if ENV["LOG"]
|
|
38
39
|
ActiveRecord::Base.logger = Logger.new($stdout)
|
39
40
|
end
|
40
41
|
|
42
|
+
if ActiveSupport::VERSION::STRING >= '4.2'
|
43
|
+
ActiveSupport.test_order = :random
|
44
|
+
end
|
45
|
+
|
41
46
|
module FriendlyId
|
42
47
|
module Test
|
43
48
|
|
@@ -65,7 +70,6 @@ module FriendlyId
|
|
65
70
|
|
66
71
|
def connect
|
67
72
|
version = ActiveRecord::VERSION::STRING
|
68
|
-
driver = FriendlyId::Test::Database.driver
|
69
73
|
engine = RUBY_ENGINE rescue "ruby"
|
70
74
|
|
71
75
|
ActiveRecord::Base.establish_connection config[driver]
|
@@ -82,11 +86,17 @@ module FriendlyId
|
|
82
86
|
end
|
83
87
|
|
84
88
|
def config
|
85
|
-
@config ||= YAML::load(
|
89
|
+
@config ||= YAML::load(
|
90
|
+
ERB.new(
|
91
|
+
File.read(File.expand_path("../databases.yml", __FILE__))
|
92
|
+
).result
|
93
|
+
)
|
86
94
|
end
|
87
95
|
|
88
96
|
def driver
|
89
|
-
(
|
97
|
+
_driver = ENV.fetch('DB', 'sqlite3').downcase
|
98
|
+
_driver = "postgres" if %w(postgresql pg).include?(_driver)
|
99
|
+
_driver
|
90
100
|
end
|
91
101
|
|
92
102
|
def in_memory?
|
data/test/history_test.rb
CHANGED
@@ -65,8 +65,7 @@ class HistoryTest < TestCaseClass
|
|
65
65
|
test "should not be read only when found by slug" do
|
66
66
|
with_instance_of(model_class) do |record|
|
67
67
|
refute model_class.friendly.find(record.friendly_id).readonly?
|
68
|
-
assert record.
|
69
|
-
assert record.update_attributes name: 'foo'
|
68
|
+
assert record.update name: 'foo'
|
70
69
|
end
|
71
70
|
end
|
72
71
|
|
@@ -93,6 +92,28 @@ class HistoryTest < TestCaseClass
|
|
93
92
|
end
|
94
93
|
end
|
95
94
|
|
95
|
+
test 'should maintain history even if current slug is not the most recent one' do
|
96
|
+
with_instance_of(model_class) do |record|
|
97
|
+
record.name = 'current'
|
98
|
+
assert record.save
|
99
|
+
|
100
|
+
# this feels like a hack. only thing i can get to work with the HistoryTestWithSti
|
101
|
+
# test cases. (Editorialist vs Journalist.)
|
102
|
+
sluggable_type = FriendlyId::Slug.first.sluggable_type
|
103
|
+
# create several slugs for record
|
104
|
+
# current slug does not have max id
|
105
|
+
FriendlyId::Slug.delete_all
|
106
|
+
FriendlyId::Slug.create(sluggable_type: sluggable_type, sluggable_id: record.id, slug: 'current')
|
107
|
+
FriendlyId::Slug.create(sluggable_type: sluggable_type, sluggable_id: record.id, slug: 'outdated')
|
108
|
+
|
109
|
+
record.reload
|
110
|
+
record.slug = nil
|
111
|
+
assert record.save
|
112
|
+
|
113
|
+
assert_equal 2, FriendlyId::Slug.count
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
96
117
|
test "should not create new slugs that match old slugs" do
|
97
118
|
transaction do
|
98
119
|
first_record = model_class.create! :name => "foo"
|
@@ -109,10 +130,10 @@ class HistoryTest < TestCaseClass
|
|
109
130
|
first_record = model_class.create! :name => "foo"
|
110
131
|
second_record = model_class.create! :name => 'another'
|
111
132
|
|
112
|
-
second_record.
|
133
|
+
second_record.update :name => 'foo', :slug => nil
|
113
134
|
assert_match(/foo-.*/, second_record.slug)
|
114
135
|
|
115
|
-
first_record.
|
136
|
+
first_record.update :name => 'another', :slug => nil
|
116
137
|
assert_match(/another-.*/, first_record.slug)
|
117
138
|
end
|
118
139
|
end
|
@@ -172,7 +193,7 @@ class HistoryTestWithAutomaticSlugRegeneration < HistoryTest
|
|
172
193
|
end
|
173
194
|
end
|
174
195
|
|
175
|
-
class DependentDestroyTest <
|
196
|
+
class DependentDestroyTest < TestCaseClass
|
176
197
|
|
177
198
|
include FriendlyId::Test
|
178
199
|
|
@@ -211,6 +232,37 @@ class DependentDestroyTest < HistoryTest
|
|
211
232
|
end
|
212
233
|
end
|
213
234
|
|
235
|
+
if ActiveRecord::VERSION::STRING >= '5.0'
|
236
|
+
class HistoryTestWithParanoidDeletes < HistoryTest
|
237
|
+
class ParanoidRecord < ActiveRecord::Base
|
238
|
+
extend FriendlyId
|
239
|
+
friendly_id :name, :use => :history, :dependent => false
|
240
|
+
|
241
|
+
default_scope { where(deleted_at: nil) }
|
242
|
+
end
|
243
|
+
|
244
|
+
def model_class
|
245
|
+
ParanoidRecord
|
246
|
+
end
|
247
|
+
|
248
|
+
test 'slug should have a sluggable even when soft deleted by a library' do
|
249
|
+
transaction do
|
250
|
+
assert FriendlyId::Slug.find_by_slug('paranoid').nil?
|
251
|
+
record = model_class.create(name: 'paranoid')
|
252
|
+
assert FriendlyId::Slug.find_by_slug('paranoid').present?
|
253
|
+
|
254
|
+
record.update deleted_at: Time.now
|
255
|
+
|
256
|
+
orphan_slug = FriendlyId::Slug.find_by_slug('paranoid')
|
257
|
+
assert orphan_slug.present?, 'Orphaned slug should exist'
|
258
|
+
|
259
|
+
assert orphan_slug.valid?, "Errors: #{orphan_slug.errors.full_messages}"
|
260
|
+
assert orphan_slug.sluggable.present?, 'Orphaned slug should still find corresponding paranoid sluggable'
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
214
266
|
class HistoryTestWithSti < HistoryTest
|
215
267
|
class Journalist < ActiveRecord::Base
|
216
268
|
extend FriendlyId
|
@@ -248,7 +300,7 @@ class HistoryTestWithFriendlyFinders < HistoryTest
|
|
248
300
|
begin
|
249
301
|
assert model_class.find(old_friendly_id)
|
250
302
|
assert model_class.exists?(old_friendly_id), "should exist? by old id for #{model_class.name}"
|
251
|
-
rescue ActiveRecord::RecordNotFound
|
303
|
+
rescue ActiveRecord::RecordNotFound
|
252
304
|
flunk "Could not find record by old id for #{model_class.name}"
|
253
305
|
end
|
254
306
|
end
|
@@ -346,6 +398,33 @@ class ScopedHistoryTest < TestCaseClass
|
|
346
398
|
end
|
347
399
|
end
|
348
400
|
|
401
|
+
test "should record history when scope changes" do
|
402
|
+
transaction do
|
403
|
+
city1 = City.create!
|
404
|
+
city2 = City.create!
|
405
|
+
with_instance_of(Restaurant) do |record|
|
406
|
+
record.name = "x"
|
407
|
+
record.slug = nil
|
408
|
+
|
409
|
+
record.city = city1
|
410
|
+
record.save!
|
411
|
+
assert_equal("city_id:#{city1.id}", record.slugs.reload.first.scope)
|
412
|
+
assert_equal("x", record.slugs.reload.first.slug)
|
413
|
+
|
414
|
+
record.city = city2
|
415
|
+
record.save!
|
416
|
+
assert_equal("city_id:#{city2.id}", record.slugs.reload.first.scope)
|
417
|
+
|
418
|
+
record.name = "y"
|
419
|
+
record.slug = nil
|
420
|
+
record.city = city1
|
421
|
+
record.save!
|
422
|
+
assert_equal("city_id:#{city1.id}", record.slugs.reload.first.scope)
|
423
|
+
assert_equal("y", record.slugs.reload.first.slug)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
349
428
|
test "should allow equal slugs in different scopes" do
|
350
429
|
transaction do
|
351
430
|
city = City.create!
|
@@ -356,4 +435,4 @@ class ScopedHistoryTest < TestCaseClass
|
|
356
435
|
assert_equal record.slug, second_record.slug
|
357
436
|
end
|
358
437
|
end
|
359
|
-
end
|
438
|
+
end
|