friendly_id 5.4.2 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/test.yml +34 -25
  5. data/Changelog.md +6 -0
  6. data/Gemfile +9 -13
  7. data/README.md +21 -0
  8. data/Rakefile +24 -27
  9. data/bench.rb +30 -27
  10. data/certs/parndt.pem +25 -23
  11. data/friendly_id.gemspec +26 -29
  12. data/gemfiles/Gemfile.rails-5.2.rb +11 -16
  13. data/gemfiles/Gemfile.rails-6.0.rb +11 -16
  14. data/gemfiles/Gemfile.rails-6.1.rb +22 -0
  15. data/gemfiles/Gemfile.rails-7.0.rb +22 -0
  16. data/guide.rb +5 -5
  17. data/lib/friendly_id/base.rb +57 -60
  18. data/lib/friendly_id/candidates.rb +9 -11
  19. data/lib/friendly_id/configuration.rb +6 -7
  20. data/lib/friendly_id/finder_methods.rb +26 -11
  21. data/lib/friendly_id/finders.rb +63 -66
  22. data/lib/friendly_id/history.rb +59 -63
  23. data/lib/friendly_id/initializer.rb +4 -4
  24. data/lib/friendly_id/migration.rb +6 -6
  25. data/lib/friendly_id/object_utils.rb +2 -2
  26. data/lib/friendly_id/reserved.rb +28 -32
  27. data/lib/friendly_id/scoped.rb +97 -102
  28. data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
  29. data/lib/friendly_id/sequentially_slugged.rb +17 -64
  30. data/lib/friendly_id/simple_i18n.rb +75 -69
  31. data/lib/friendly_id/slug.rb +1 -2
  32. data/lib/friendly_id/slug_generator.rb +1 -3
  33. data/lib/friendly_id/slugged.rb +234 -238
  34. data/lib/friendly_id/version.rb +1 -1
  35. data/lib/friendly_id.rb +41 -45
  36. data/lib/generators/friendly_id_generator.rb +9 -9
  37. data/test/base_test.rb +10 -13
  38. data/test/benchmarks/finders.rb +28 -26
  39. data/test/benchmarks/object_utils.rb +13 -13
  40. data/test/candidates_test.rb +17 -18
  41. data/test/configuration_test.rb +7 -11
  42. data/test/core_test.rb +1 -2
  43. data/test/databases.yml +4 -3
  44. data/test/finders_test.rb +52 -5
  45. data/test/generator_test.rb +16 -26
  46. data/test/helper.rb +29 -22
  47. data/test/history_test.rb +70 -74
  48. data/test/numeric_slug_test.rb +4 -4
  49. data/test/object_utils_test.rb +0 -2
  50. data/test/reserved_test.rb +9 -11
  51. data/test/schema.rb +5 -4
  52. data/test/scoped_test.rb +18 -20
  53. data/test/sequentially_slugged_test.rb +65 -50
  54. data/test/shared.rb +15 -16
  55. data/test/simple_i18n_test.rb +22 -12
  56. data/test/slugged_test.rb +102 -121
  57. data/test/sti_test.rb +19 -21
  58. data.tar.gz.sig +0 -0
  59. metadata +35 -30
  60. metadata.gz.sig +1 -1
@@ -1,3 +1,5 @@
1
+ require_relative "sequentially_slugged/calculator"
2
+
1
3
  module FriendlyId
2
4
  module SequentiallySlugged
3
5
  def self.setup(model_class)
@@ -7,71 +9,14 @@ module FriendlyId
7
9
  def resolve_friendly_id_conflict(candidate_slugs)
8
10
  candidate = candidate_slugs.first
9
11
  return if candidate.nil?
10
- SequentialSlugCalculator.new(scope_for_slug_generator,
11
- candidate,
12
- friendly_id_config.slug_column,
13
- friendly_id_config.sequence_separator,
14
- slug_base_class).next_slug
15
- end
16
-
17
- class SequentialSlugCalculator
18
- attr_accessor :scope, :slug, :slug_column, :sequence_separator
19
-
20
- def initialize(scope, slug, slug_column, sequence_separator, base_class)
21
- @scope = scope
22
- @slug = slug
23
- table_name = scope.connection.quote_table_name(base_class.arel_table.name)
24
- @slug_column = "#{table_name}.#{scope.connection.quote_column_name(slug_column)}"
25
- @sequence_separator = sequence_separator
26
- end
27
-
28
- def next_slug
29
- slug + sequence_separator + next_sequence_number.to_s
30
- end
31
-
32
- private
33
-
34
- def next_sequence_number
35
- last_sequence_number ? last_sequence_number + 1 : 2
36
- end
37
-
38
- def last_sequence_number
39
- regexp = /#{slug}#{sequence_separator}(\d+)\z/
40
- # Reject slug_conflicts that doesn't come from the first_candidate
41
- # Map all sequence numbers and take the maximum
42
- slug_conflicts.reject{ |slug_conflict| !regexp.match(slug_conflict) }.map do |slug_conflict|
43
- regexp.match(slug_conflict)[1].to_i
44
- end.max
45
- end
46
12
 
47
- def slug_conflicts
48
- scope.
49
- where(conflict_query, slug, sequential_slug_matcher).
50
- order(Arel.sql(ordering_query)).pluck(Arel.sql(slug_column))
51
- end
52
-
53
- def conflict_query
54
- base = "#{slug_column} = ? OR #{slug_column} LIKE ?"
55
- # Awful hack for SQLite3, which does not pick up '\' as the escape character
56
- # without this.
57
- base << " ESCAPE '\\'" if scope.connection.adapter_name =~ /sqlite/i
58
- base
59
- end
60
-
61
- def sequential_slug_matcher
62
- # Underscores (matching a single character) and percent signs (matching
63
- # any number of characters) need to be escaped. While this looks like
64
- # an excessive number of backslashes, it is correct.
65
- "#{slug}#{sequence_separator}".gsub(/[_%]/, '\\\\\&') + '%'
66
- end
67
-
68
- # Return the unnumbered (shortest) slug first, followed by the numbered ones
69
- # in ascending order.
70
- def ordering_query
71
- length_command = "LENGTH"
72
- length_command = "LEN" if scope.connection.adapter_name =~ /sqlserver/i
73
- "#{length_command}(#{slug_column}) ASC, #{slug_column} ASC"
74
- end
13
+ Calculator.new(
14
+ scope_for_slug_generator,
15
+ candidate,
16
+ slug_column,
17
+ friendly_id_config.sequence_separator,
18
+ slug_base_class
19
+ ).next_slug
75
20
  end
76
21
 
77
22
  private
@@ -83,5 +28,13 @@ module FriendlyId
83
28
  self.class.base_class
84
29
  end
85
30
  end
31
+
32
+ def slug_column
33
+ if friendly_id_config.uses?(:history)
34
+ :slug
35
+ else
36
+ friendly_id_config.slug_column
37
+ end
38
+ end
86
39
  end
87
40
  end
@@ -1,75 +1,75 @@
1
1
  require "i18n"
2
2
 
3
3
  module FriendlyId
4
-
5
- =begin
6
-
7
- ## Translating Slugs Using Simple I18n
8
-
9
- The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
10
- FriendlyId.
11
-
12
- In order to use this module, your model must have a slug column for each locale.
13
- By default FriendlyId looks for columns named, for example, "slug_en",
14
- "slug_es", etc. The first part of the name can be configured by passing the
15
- `:slug_column` option if you choose. Note that the column for the default locale
16
- must also include the locale in its name.
17
-
18
- This module is most suitable to applications that need to support few locales.
19
- If you need to support two or more locales, you may wish to use the
20
- friendly_id_globalize gem instead.
21
-
22
- ### Example migration
23
-
24
- def self.up
25
- create_table :posts do |t|
26
- t.string :title
27
- t.string :slug_en
28
- t.string :slug_es
29
- t.text :body
30
- end
31
- add_index :posts, :slug_en
32
- add_index :posts, :slug_es
33
- end
34
-
35
- ### Finds
36
-
37
- Finds will take into consideration the current locale:
38
-
39
- I18n.locale = :es
40
- Post.friendly.find("la-guerra-de-las-galaxias")
41
- I18n.locale = :en
42
- Post.friendly.find("star-wars")
43
-
44
- To find a slug by an explicit locale, perform the find inside a block
45
- passed to I18n's `with_locale` method:
46
-
47
- I18n.with_locale(:es) do
48
- Post.friendly.find("la-guerra-de-las-galaxias")
49
- end
50
-
51
- ### Creating Records
52
-
53
- When new records are created, the slug is generated for the current locale only.
54
-
55
- ### Translating Slugs
56
-
57
- To translate an existing record's friendly_id, use
58
- {FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
59
- you add is properly escaped, transliterated and sequenced:
60
-
61
- post = Post.create :name => "Star Wars"
62
- post.set_friendly_id("La guerra de las galaxias", :es)
63
-
64
- If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
65
- current locale:
66
-
67
- I18n.with_locale(:es) do
68
- post.set_friendly_id("La guerra de las galaxias")
69
- end
70
- =end
4
+ #
5
+ ## Translating Slugs Using Simple I18n
6
+ #
7
+ # The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
8
+ # FriendlyId.
9
+ #
10
+ # In order to use this module, your model must have a slug column for each locale.
11
+ # By default FriendlyId looks for columns named, for example, "slug_en",
12
+ # "slug_es", "slug_pt_br", etc. The first part of the name can be configured by
13
+ # passing the `:slug_column` option if you choose. Note that the column for the
14
+ # default locale must also include the locale in its name.
15
+ #
16
+ # This module is most suitable to applications that need to support few locales.
17
+ # If you need to support two or more locales, you may wish to use the
18
+ # friendly_id_globalize gem instead.
19
+ #
20
+ ### Example migration
21
+ #
22
+ # def self.up
23
+ # create_table :posts do |t|
24
+ # t.string :title
25
+ # t.string :slug_en
26
+ # t.string :slug_es
27
+ # t.string :slug_pt_br
28
+ # t.text :body
29
+ # end
30
+ # add_index :posts, :slug_en
31
+ # add_index :posts, :slug_es
32
+ # add_index :posts, :slug_pt_br
33
+ # end
34
+ #
35
+ ### Finds
36
+ #
37
+ # Finds will take into consideration the current locale:
38
+ #
39
+ # I18n.locale = :es
40
+ # Post.friendly.find("la-guerra-de-las-galaxias")
41
+ # I18n.locale = :en
42
+ # Post.friendly.find("star-wars")
43
+ # I18n.locale = :"pt-BR"
44
+ # Post.friendly.find("guerra-das-estrelas")
45
+ #
46
+ # To find a slug by an explicit locale, perform the find inside a block
47
+ # passed to I18n's `with_locale` method:
48
+ #
49
+ # I18n.with_locale(:es) do
50
+ # Post.friendly.find("la-guerra-de-las-galaxias")
51
+ # end
52
+ #
53
+ ### Creating Records
54
+ #
55
+ # When new records are created, the slug is generated for the current locale only.
56
+ #
57
+ ### Translating Slugs
58
+ #
59
+ # To translate an existing record's friendly_id, use
60
+ # {FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
61
+ # you add is properly escaped, transliterated and sequenced:
62
+ #
63
+ # post = Post.create :name => "Star Wars"
64
+ # post.set_friendly_id("La guerra de las galaxias", :es)
65
+ #
66
+ # If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
67
+ # current locale:
68
+ #
69
+ # I18n.with_locale(:es) do
70
+ # post.set_friendly_id("La guerra de las galaxias")
71
+ # end
71
72
  module SimpleI18n
72
-
73
73
  # FriendlyId::Config.use will invoke this method when present, to allow
74
74
  # loading dependent modules prior to overriding them when necessary.
75
75
  def self.setup(model_class)
@@ -98,7 +98,13 @@ current locale:
98
98
 
99
99
  module Configuration
100
100
  def slug_column
101
- "#{super}_#{I18n.locale}"
101
+ "#{super}_#{locale_suffix}"
102
+ end
103
+
104
+ private
105
+
106
+ def locale_suffix
107
+ I18n.locale.to_s.underscore
102
108
  end
103
109
  end
104
110
  end
@@ -3,7 +3,7 @@ module FriendlyId
3
3
  #
4
4
  # @see FriendlyId::History
5
5
  class Slug < ActiveRecord::Base
6
- belongs_to :sluggable, :polymorphic => true
6
+ belongs_to :sluggable, polymorphic: true
7
7
 
8
8
  def sluggable
9
9
  sluggable_type.constantize.unscoped { super }
@@ -12,6 +12,5 @@ module FriendlyId
12
12
  def to_param
13
13
  slug
14
14
  end
15
-
16
15
  end
17
16
  end
@@ -2,7 +2,6 @@ module FriendlyId
2
2
  # The default slug generator offers functionality to check slug candidates for
3
3
  # availability.
4
4
  class SlugGenerator
5
-
6
5
  def initialize(scope, config)
7
6
  @scope = scope
8
7
  @config = config
@@ -17,9 +16,8 @@ module FriendlyId
17
16
  end
18
17
 
19
18
  def generate(candidates)
20
- candidates.each {|c| return c if available?(c)}
19
+ candidates.each { |c| return c if available?(c) }
21
20
  nil
22
21
  end
23
-
24
22
  end
25
23
  end