ultra-clean-lib 0.0.1
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 +7 -0
- data/friendly_id-5.7.0/Changelog.md +273 -0
- data/friendly_id-5.7.0/MIT-LICENSE +19 -0
- data/friendly_id-5.7.0/README.md +176 -0
- data/friendly_id-5.7.0/lib/friendly_id/base.rb +275 -0
- data/friendly_id-5.7.0/lib/friendly_id/candidates.rb +71 -0
- data/friendly_id-5.7.0/lib/friendly_id/configuration.rb +111 -0
- data/friendly_id-5.7.0/lib/friendly_id/finder_methods.rb +123 -0
- data/friendly_id-5.7.0/lib/friendly_id/finders.rb +92 -0
- data/friendly_id-5.7.0/lib/friendly_id/history.rb +146 -0
- data/friendly_id-5.7.0/lib/friendly_id/initializer.rb +107 -0
- data/friendly_id-5.7.0/lib/friendly_id/migration.rb +21 -0
- data/friendly_id-5.7.0/lib/friendly_id/object_utils.rb +76 -0
- data/friendly_id-5.7.0/lib/friendly_id/reserved.rb +50 -0
- data/friendly_id-5.7.0/lib/friendly_id/scoped.rb +175 -0
- data/friendly_id-5.7.0/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
- data/friendly_id-5.7.0/lib/friendly_id/sequentially_slugged.rb +40 -0
- data/friendly_id-5.7.0/lib/friendly_id/simple_i18n.rb +114 -0
- data/friendly_id-5.7.0/lib/friendly_id/slug.rb +16 -0
- data/friendly_id-5.7.0/lib/friendly_id/slug_generator.rb +38 -0
- data/friendly_id-5.7.0/lib/friendly_id/slugged.rb +436 -0
- data/friendly_id-5.7.0/lib/friendly_id/version.rb +3 -0
- data/friendly_id-5.7.0/lib/friendly_id.rb +114 -0
- data/friendly_id-5.7.0/lib/generators/friendly_id_generator.rb +26 -0
- data/ultra-clean-lib.gemspec +12 -0
- metadata +65 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
module FriendlyId
|
|
2
|
+
# @guide begin
|
|
3
|
+
#
|
|
4
|
+
# ## History: Avoiding 404's When Slugs Change
|
|
5
|
+
#
|
|
6
|
+
# FriendlyId's {FriendlyId::History History} module adds the ability to store a
|
|
7
|
+
# log of a model's slugs, so that when its friendly id changes, it's still
|
|
8
|
+
# possible to perform finds by the old id.
|
|
9
|
+
#
|
|
10
|
+
# The primary use case for this is avoiding broken URLs.
|
|
11
|
+
#
|
|
12
|
+
# ### Setup
|
|
13
|
+
#
|
|
14
|
+
# In order to use this module, you must add a table to your database schema to
|
|
15
|
+
# store the slug records. FriendlyId provides a generator for this purpose:
|
|
16
|
+
#
|
|
17
|
+
# rails generate friendly_id
|
|
18
|
+
# rake db:migrate
|
|
19
|
+
#
|
|
20
|
+
# This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
|
|
21
|
+
# model.
|
|
22
|
+
#
|
|
23
|
+
# ### Considerations
|
|
24
|
+
#
|
|
25
|
+
# Because recording slug history requires creating additional database records,
|
|
26
|
+
# this module has an impact on the performance of the associated model's `create`
|
|
27
|
+
# method.
|
|
28
|
+
#
|
|
29
|
+
# ### Example
|
|
30
|
+
#
|
|
31
|
+
# class Post < ActiveRecord::Base
|
|
32
|
+
# extend FriendlyId
|
|
33
|
+
# friendly_id :title, :use => :history
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# class PostsController < ApplicationController
|
|
37
|
+
#
|
|
38
|
+
# before_filter :find_post
|
|
39
|
+
#
|
|
40
|
+
# ...
|
|
41
|
+
#
|
|
42
|
+
# def find_post
|
|
43
|
+
# @post = Post.friendly.find params[:id]
|
|
44
|
+
#
|
|
45
|
+
# # If an old id or a numeric id was used to find the record, then
|
|
46
|
+
# # the request slug will not match the current slug, and we should do
|
|
47
|
+
# # a 301 redirect to the new path
|
|
48
|
+
# if params[:id] != @post.slug
|
|
49
|
+
# return redirect_to @post, :status => :moved_permanently
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# @guide end
|
|
55
|
+
module History
|
|
56
|
+
module Configuration
|
|
57
|
+
def dependent_value
|
|
58
|
+
dependent.nil? ? :destroy : dependent
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.setup(model_class)
|
|
63
|
+
model_class.instance_eval do
|
|
64
|
+
friendly_id_config.use :slugged
|
|
65
|
+
friendly_id_config.class.send :include, History::Configuration
|
|
66
|
+
friendly_id_config.finder_methods = FriendlyId::History::FinderMethods
|
|
67
|
+
FriendlyId::Finders.setup(model_class) if friendly_id_config.uses? :finders
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Configures the model instance to use the History add-on.
|
|
72
|
+
def self.included(model_class)
|
|
73
|
+
model_class.class_eval do
|
|
74
|
+
has_many :slugs, -> { order(id: :desc) }, **{
|
|
75
|
+
as: :sluggable,
|
|
76
|
+
dependent: @friendly_id_config.dependent_value,
|
|
77
|
+
class_name: Slug.to_s
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
after_save :create_slug
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
module FinderMethods
|
|
85
|
+
include ::FriendlyId::FinderMethods
|
|
86
|
+
|
|
87
|
+
def exists_by_friendly_id?(id)
|
|
88
|
+
super || joins(:slugs).where(slug_history_clause(parse_friendly_id(id))).exists?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def first_by_friendly_id(id)
|
|
94
|
+
super || slug_table_record(parse_friendly_id(id))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def slug_table_record(id)
|
|
98
|
+
select(quoted_table_name + ".*").joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def slug_history_clause(id)
|
|
102
|
+
Slug.arel_table[:sluggable_type].eq(base_class.to_s).and(Slug.arel_table[:slug].eq(id))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# If we're updating, don't consider historic slugs for the same record
|
|
109
|
+
# to be conflicts. This will allow a record to revert to a previously
|
|
110
|
+
# used slug.
|
|
111
|
+
def scope_for_slug_generator
|
|
112
|
+
relation = super.joins(:slugs)
|
|
113
|
+
unless new_record?
|
|
114
|
+
relation = relation.merge(Slug.where("sluggable_id <> ?", id))
|
|
115
|
+
end
|
|
116
|
+
if friendly_id_config.uses?(:scoped)
|
|
117
|
+
relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
|
|
118
|
+
end
|
|
119
|
+
relation
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def create_slug
|
|
123
|
+
return unless friendly_id
|
|
124
|
+
return if history_is_up_to_date?
|
|
125
|
+
# Allow reversion back to a previously used slug
|
|
126
|
+
relation = slugs.where(slug: friendly_id)
|
|
127
|
+
if friendly_id_config.uses?(:scoped)
|
|
128
|
+
relation = relation.where(scope: serialized_scope)
|
|
129
|
+
end
|
|
130
|
+
relation.destroy_all unless relation.empty?
|
|
131
|
+
slugs.create! do |record|
|
|
132
|
+
record.slug = friendly_id
|
|
133
|
+
record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def history_is_up_to_date?
|
|
138
|
+
latest_history = slugs.first
|
|
139
|
+
check = latest_history.try(:slug) == friendly_id
|
|
140
|
+
if friendly_id_config.uses?(:scoped)
|
|
141
|
+
check &&= latest_history.scope == serialized_scope
|
|
142
|
+
end
|
|
143
|
+
check
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# FriendlyId Global Configuration
|
|
2
|
+
#
|
|
3
|
+
# Use this to set up shared configuration options for your entire application.
|
|
4
|
+
# Any of the configuration options shown here can also be applied to single
|
|
5
|
+
# models by passing arguments to the `friendly_id` class method or defining
|
|
6
|
+
# methods in your model.
|
|
7
|
+
#
|
|
8
|
+
# To learn more, check out the guide:
|
|
9
|
+
#
|
|
10
|
+
# http://norman.github.io/friendly_id/file.Guide.html
|
|
11
|
+
|
|
12
|
+
FriendlyId.defaults do |config|
|
|
13
|
+
# ## Reserved Words
|
|
14
|
+
#
|
|
15
|
+
# Some words could conflict with Rails's routes when used as slugs, or are
|
|
16
|
+
# undesirable to allow as slugs. Edit this list as needed for your app.
|
|
17
|
+
config.use :reserved
|
|
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 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
|
|
27
|
+
|
|
28
|
+
# ## Friendly Finders
|
|
29
|
+
#
|
|
30
|
+
# Uncomment this to use friendly finders in all models. By default, if
|
|
31
|
+
# you wish to find a record by its friendly id, you must do:
|
|
32
|
+
#
|
|
33
|
+
# MyModel.friendly.find('foo')
|
|
34
|
+
#
|
|
35
|
+
# If you uncomment this, you can do:
|
|
36
|
+
#
|
|
37
|
+
# MyModel.find('foo')
|
|
38
|
+
#
|
|
39
|
+
# This is significantly more convenient but may not be appropriate for
|
|
40
|
+
# all applications, so you must explicitly opt-in to this behavior. You can
|
|
41
|
+
# always also configure it on a per-model basis if you prefer.
|
|
42
|
+
#
|
|
43
|
+
# Something else to consider is that using the :finders addon boosts
|
|
44
|
+
# performance because it will avoid Rails-internal code that makes runtime
|
|
45
|
+
# calls to `Module.extend`.
|
|
46
|
+
#
|
|
47
|
+
# config.use :finders
|
|
48
|
+
#
|
|
49
|
+
# ## Slugs
|
|
50
|
+
#
|
|
51
|
+
# Most applications will use the :slugged module everywhere. If you wish
|
|
52
|
+
# to do so, uncomment the following line.
|
|
53
|
+
#
|
|
54
|
+
# config.use :slugged
|
|
55
|
+
#
|
|
56
|
+
# By default, FriendlyId's :slugged addon expects the slug column to be named
|
|
57
|
+
# 'slug', but you can change it if you wish.
|
|
58
|
+
#
|
|
59
|
+
# config.slug_column = 'slug'
|
|
60
|
+
#
|
|
61
|
+
# By default, slug has no size limit, but you can change it if you wish.
|
|
62
|
+
#
|
|
63
|
+
# config.slug_limit = 255
|
|
64
|
+
#
|
|
65
|
+
# When FriendlyId can not generate a unique ID from your base method, it appends
|
|
66
|
+
# a UUID, separated by a single dash. You can configure the character used as the
|
|
67
|
+
# separator. If you're upgrading from FriendlyId 4, you may wish to replace this
|
|
68
|
+
# with two dashes.
|
|
69
|
+
#
|
|
70
|
+
# config.sequence_separator = '-'
|
|
71
|
+
#
|
|
72
|
+
# Note that you must use the :slugged addon **prior** to the line which
|
|
73
|
+
# configures the sequence separator, or else FriendlyId will raise an undefined
|
|
74
|
+
# method error.
|
|
75
|
+
#
|
|
76
|
+
# ## Tips and Tricks
|
|
77
|
+
#
|
|
78
|
+
# ### Controlling when slugs are generated
|
|
79
|
+
#
|
|
80
|
+
# As of FriendlyId 5.0, new slugs are generated only when the slug field is
|
|
81
|
+
# nil, but if you're using a column as your base method can change this
|
|
82
|
+
# behavior by overriding the `should_generate_new_friendly_id?` method that
|
|
83
|
+
# FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
|
|
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.
|
|
89
|
+
#
|
|
90
|
+
# config.use :slugged
|
|
91
|
+
# config.use Module.new {
|
|
92
|
+
# def should_generate_new_friendly_id?
|
|
93
|
+
# slug.blank? || <your_column_name_here>_changed?
|
|
94
|
+
# end
|
|
95
|
+
# }
|
|
96
|
+
#
|
|
97
|
+
# FriendlyId uses Rails's `parameterize` method to generate slugs, but for
|
|
98
|
+
# languages that don't use the Roman alphabet, that's not usually sufficient.
|
|
99
|
+
# Here we use the Babosa library to transliterate Russian Cyrillic slugs to
|
|
100
|
+
# ASCII. If you use this, don't forget to add "babosa" to your Gemfile.
|
|
101
|
+
#
|
|
102
|
+
# config.use Module.new {
|
|
103
|
+
# def normalize_friendly_id(text)
|
|
104
|
+
# text.to_slug.normalize! :transliterations => [:russian, :latin]
|
|
105
|
+
# end
|
|
106
|
+
# }
|
|
107
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
|
9
|
+
def change
|
|
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
|
|
15
|
+
t.datetime :created_at
|
|
16
|
+
end
|
|
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
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module FriendlyId
|
|
2
|
+
# Instances of these classes will never be considered a friendly id.
|
|
3
|
+
# @see FriendlyId::ObjectUtils#friendly_id
|
|
4
|
+
UNFRIENDLY_CLASSES = [
|
|
5
|
+
Array,
|
|
6
|
+
FalseClass,
|
|
7
|
+
Hash,
|
|
8
|
+
NilClass,
|
|
9
|
+
Numeric,
|
|
10
|
+
Symbol,
|
|
11
|
+
TrueClass
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Utility methods for determining whether any object is a friendly id.
|
|
15
|
+
#
|
|
16
|
+
# Monkey-patching Object is a somewhat extreme measure not to be taken lightly
|
|
17
|
+
# by libraries, but in this case I decided to do it because to me, it feels
|
|
18
|
+
# cleaner than adding a module method to {FriendlyId}. I've given the methods
|
|
19
|
+
# names that unambigously refer to the library of their origin, which should
|
|
20
|
+
# be sufficient to avoid conflicts with other libraries.
|
|
21
|
+
module ObjectUtils
|
|
22
|
+
# True if the id is definitely friendly, false if definitely unfriendly,
|
|
23
|
+
# else nil.
|
|
24
|
+
#
|
|
25
|
+
# An object is considired "definitely unfriendly" if its class is or
|
|
26
|
+
# inherits from ActiveRecord::Base, Array, Hash, NilClass, Numeric, or
|
|
27
|
+
# Symbol.
|
|
28
|
+
#
|
|
29
|
+
# An object is considered "definitely friendly" if it responds to +to_i+,
|
|
30
|
+
# and its value when cast to an integer and then back to a string is
|
|
31
|
+
# different from its value when merely cast to a string:
|
|
32
|
+
#
|
|
33
|
+
# 123.friendly_id? #=> false
|
|
34
|
+
# :id.friendly_id? #=> false
|
|
35
|
+
# {:name => 'joe'}.friendly_id? #=> false
|
|
36
|
+
# ['name = ?', 'joe'].friendly_id? #=> false
|
|
37
|
+
# nil.friendly_id? #=> false
|
|
38
|
+
# "123".friendly_id? #=> nil
|
|
39
|
+
# "abc123".friendly_id? #=> true
|
|
40
|
+
def friendly_id?
|
|
41
|
+
true if respond_to?(:to_i) && to_i.to_s != to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# True if the id is definitely unfriendly, false if definitely friendly,
|
|
45
|
+
# else nil.
|
|
46
|
+
def unfriendly_id?
|
|
47
|
+
val = friendly_id?
|
|
48
|
+
!val unless val.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module UnfriendlyUtils
|
|
53
|
+
def friendly_id?
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unfriendly_id?
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.mark_as_unfriendly(klass)
|
|
63
|
+
klass.send(:include, FriendlyId::UnfriendlyUtils)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Object.send :include, FriendlyId::ObjectUtils
|
|
68
|
+
|
|
69
|
+
# Considered unfriendly if object is an instance of an unfriendly class or
|
|
70
|
+
# one of its descendants.
|
|
71
|
+
|
|
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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module FriendlyId
|
|
2
|
+
# @guide begin
|
|
3
|
+
#
|
|
4
|
+
# ## Reserved Words
|
|
5
|
+
#
|
|
6
|
+
# The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
|
|
7
|
+
# words from use as FriendlyId slugs.
|
|
8
|
+
#
|
|
9
|
+
# With Ruby on Rails, FriendlyId's generator generates an initializer that
|
|
10
|
+
# reserves some words such as "new" and "edit" using {FriendlyId.defaults
|
|
11
|
+
# FriendlyId.defaults}.
|
|
12
|
+
#
|
|
13
|
+
# Note that the error messages for fields will appear on the field
|
|
14
|
+
# `:friendly_id`. If you are using Rails's scaffolded form errors display, then
|
|
15
|
+
# it will have no field to highlight. If you'd like to change this so that
|
|
16
|
+
# scaffolding works as expected, one way to accomplish this is to move the error
|
|
17
|
+
# message to a different field. For example:
|
|
18
|
+
#
|
|
19
|
+
# class Person < ActiveRecord::Base
|
|
20
|
+
# extend FriendlyId
|
|
21
|
+
# friendly_id :name, use: :slugged
|
|
22
|
+
#
|
|
23
|
+
# after_validation :move_friendly_id_error_to_name
|
|
24
|
+
#
|
|
25
|
+
# def move_friendly_id_error_to_name
|
|
26
|
+
# errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @guide end
|
|
31
|
+
module Reserved
|
|
32
|
+
# When included, this module adds configuration options to the model class's
|
|
33
|
+
# friendly_id_config.
|
|
34
|
+
def self.included(model_class)
|
|
35
|
+
model_class.class_eval do
|
|
36
|
+
friendly_id_config.class.send :include, Reserved::Configuration
|
|
37
|
+
validates_exclusion_of :friendly_id, in: ->(_) {
|
|
38
|
+
friendly_id_config.reserved_words || []
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# This module adds the `:reserved_words` configuration option to
|
|
44
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
|
45
|
+
module Configuration
|
|
46
|
+
attr_accessor :reserved_words
|
|
47
|
+
attr_accessor :treat_reserved_as_conflict
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require "friendly_id/slugged"
|
|
2
|
+
|
|
3
|
+
module FriendlyId
|
|
4
|
+
# @guide begin
|
|
5
|
+
#
|
|
6
|
+
# ## Unique Slugs by Scope
|
|
7
|
+
#
|
|
8
|
+
# The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
|
|
9
|
+
# within a scope.
|
|
10
|
+
#
|
|
11
|
+
# This allows, for example, two restaurants in different cities to have the slug
|
|
12
|
+
# `joes-diner`:
|
|
13
|
+
#
|
|
14
|
+
# class Restaurant < ActiveRecord::Base
|
|
15
|
+
# extend FriendlyId
|
|
16
|
+
# belongs_to :city
|
|
17
|
+
# friendly_id :name, :use => :scoped, :scope => :city
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# class City < ActiveRecord::Base
|
|
21
|
+
# extend FriendlyId
|
|
22
|
+
# has_many :restaurants
|
|
23
|
+
# friendly_id :name, :use => :slugged
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
|
|
27
|
+
# City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
|
|
28
|
+
#
|
|
29
|
+
# Without :scoped in this case, one of the restaurants would have the slug
|
|
30
|
+
# `joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
|
|
31
|
+
#
|
|
32
|
+
# The value for the `:scope` option can be the name of a `belongs_to` relation, or
|
|
33
|
+
# a column.
|
|
34
|
+
#
|
|
35
|
+
# Additionally, the `:scope` option can receive an array of scope values:
|
|
36
|
+
#
|
|
37
|
+
# class Cuisine < ActiveRecord::Base
|
|
38
|
+
# extend FriendlyId
|
|
39
|
+
# has_many :restaurants
|
|
40
|
+
# friendly_id :name, :use => :slugged
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# class City < ActiveRecord::Base
|
|
44
|
+
# extend FriendlyId
|
|
45
|
+
# has_many :restaurants
|
|
46
|
+
# friendly_id :name, :use => :slugged
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# class Restaurant < ActiveRecord::Base
|
|
50
|
+
# extend FriendlyId
|
|
51
|
+
# belongs_to :city
|
|
52
|
+
# friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# All supplied values will be used to determine scope.
|
|
56
|
+
#
|
|
57
|
+
# ### Finding Records by Friendly ID
|
|
58
|
+
#
|
|
59
|
+
# If you are using scopes your friendly ids may not be unique, so a simple find
|
|
60
|
+
# like:
|
|
61
|
+
#
|
|
62
|
+
# Restaurant.friendly.find("joes-diner")
|
|
63
|
+
#
|
|
64
|
+
# may return the wrong record. In these cases it's best to query through the
|
|
65
|
+
# relation:
|
|
66
|
+
#
|
|
67
|
+
# @city.restaurants.friendly.find("joes-diner")
|
|
68
|
+
#
|
|
69
|
+
# Alternatively, you could pass the scope value as a query parameter:
|
|
70
|
+
#
|
|
71
|
+
# Restaurant.where(:city_id => @city.id).friendly.find("joes-diner")
|
|
72
|
+
#
|
|
73
|
+
#
|
|
74
|
+
# ### Finding All Records That Match a Scoped ID
|
|
75
|
+
#
|
|
76
|
+
# Query the slug column directly:
|
|
77
|
+
#
|
|
78
|
+
# Restaurant.where(:slug => "joes-diner")
|
|
79
|
+
#
|
|
80
|
+
# ### Routes for Scoped Models
|
|
81
|
+
#
|
|
82
|
+
# Recall that FriendlyId is a database-centric library, and does not set up any
|
|
83
|
+
# routes for scoped models. You must do this yourself in your application. Here's
|
|
84
|
+
# an example of one way to set this up:
|
|
85
|
+
#
|
|
86
|
+
# # in routes.rb
|
|
87
|
+
# resources :cities do
|
|
88
|
+
# resources :restaurants
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# # in views
|
|
92
|
+
# <%= link_to 'Show', [@city, @restaurant] %>
|
|
93
|
+
#
|
|
94
|
+
# # in controllers
|
|
95
|
+
# @city = City.friendly.find(params[:city_id])
|
|
96
|
+
# @restaurant = @city.restaurants.friendly.find(params[:id])
|
|
97
|
+
#
|
|
98
|
+
# # URLs:
|
|
99
|
+
# http://example.org/cities/seattle/restaurants/joes-diner
|
|
100
|
+
# http://example.org/cities/chicago/restaurants/joes-diner
|
|
101
|
+
#
|
|
102
|
+
# @guide end
|
|
103
|
+
module Scoped
|
|
104
|
+
# FriendlyId::Config.use will invoke this method when present, to allow
|
|
105
|
+
# loading dependent modules prior to overriding them when necessary.
|
|
106
|
+
def self.setup(model_class)
|
|
107
|
+
model_class.friendly_id_config.use :slugged
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Sets up behavior and configuration options for FriendlyId's scoped slugs
|
|
111
|
+
# feature.
|
|
112
|
+
def self.included(model_class)
|
|
113
|
+
model_class.class_eval do
|
|
114
|
+
friendly_id_config.class.send :include, Configuration
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def serialized_scope
|
|
119
|
+
friendly_id_config.scope_columns.sort.map { |column| "#{column}:#{send(column)}" }.join(",")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def scope_for_slug_generator
|
|
123
|
+
if friendly_id_config.uses?(:History)
|
|
124
|
+
return super
|
|
125
|
+
end
|
|
126
|
+
relation = self.class.base_class.unscoped.friendly
|
|
127
|
+
friendly_id_config.scope_columns.each do |column|
|
|
128
|
+
relation = relation.where(column => send(column))
|
|
129
|
+
end
|
|
130
|
+
primary_key_name = self.class.primary_key
|
|
131
|
+
relation.where(self.class.arel_table[primary_key_name].not_eq(send(primary_key_name)))
|
|
132
|
+
end
|
|
133
|
+
private :scope_for_slug_generator
|
|
134
|
+
|
|
135
|
+
def slug_generator
|
|
136
|
+
friendly_id_config.slug_generator_class.new(scope_for_slug_generator, friendly_id_config)
|
|
137
|
+
end
|
|
138
|
+
private :slug_generator
|
|
139
|
+
|
|
140
|
+
def should_generate_new_friendly_id?
|
|
141
|
+
(changed & friendly_id_config.scope_columns).any? || super
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# This module adds the `:scope` configuration option to
|
|
145
|
+
# {FriendlyId::Configuration FriendlyId::Configuration}.
|
|
146
|
+
module Configuration
|
|
147
|
+
# Gets the scope value.
|
|
148
|
+
#
|
|
149
|
+
# When setting this value, the argument should be a symbol referencing a
|
|
150
|
+
# `belongs_to` relation, or a column.
|
|
151
|
+
#
|
|
152
|
+
# @return Symbol The scope value
|
|
153
|
+
attr_accessor :scope
|
|
154
|
+
|
|
155
|
+
# Gets the scope columns.
|
|
156
|
+
#
|
|
157
|
+
# Checks to see if the `:scope` option passed to
|
|
158
|
+
# {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
|
|
159
|
+
# the realtion's foreign key. Otherwise it assumes the option value was
|
|
160
|
+
# the name of column and returns it cast to a String.
|
|
161
|
+
#
|
|
162
|
+
# @return String The scope column
|
|
163
|
+
def scope_columns
|
|
164
|
+
[@scope].flatten.map { |s| (reflection_foreign_key(s) or s).to_s }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def reflection_foreign_key(scope)
|
|
170
|
+
reflection = model_class.reflections[scope] || model_class.reflections[scope.to_s]
|
|
171
|
+
reflection.try(:foreign_key)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module FriendlyId
|
|
2
|
+
module SequentiallySlugged
|
|
3
|
+
class Calculator
|
|
4
|
+
attr_accessor :scope, :slug, :slug_column, :sequence_separator
|
|
5
|
+
|
|
6
|
+
def initialize(scope, slug, slug_column, sequence_separator, base_class)
|
|
7
|
+
@scope = scope
|
|
8
|
+
@slug = slug
|
|
9
|
+
table_name = scope.connection.quote_table_name(base_class.arel_table.name)
|
|
10
|
+
@slug_column = "#{table_name}.#{scope.connection.quote_column_name(slug_column)}"
|
|
11
|
+
@sequence_separator = sequence_separator
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_slug
|
|
15
|
+
slug + sequence_separator + next_sequence_number.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def conflict_query
|
|
21
|
+
base = "#{slug_column} = ? OR #{slug_column} LIKE ?"
|
|
22
|
+
# Awful hack for SQLite3, which does not pick up '\' as the escape character
|
|
23
|
+
# without this.
|
|
24
|
+
base << " ESCAPE '\\'" if /sqlite/i.match?(scope.connection.adapter_name)
|
|
25
|
+
base
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def next_sequence_number
|
|
29
|
+
last_sequence_number ? last_sequence_number + 1 : 2
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def last_sequence_number
|
|
33
|
+
# Reject slug_conflicts that doesn't come from the first_candidate
|
|
34
|
+
# Map all sequence numbers and take the maximum
|
|
35
|
+
slug_conflicts
|
|
36
|
+
.reject { |slug_conflict| !regexp.match(slug_conflict) }
|
|
37
|
+
.map { |slug_conflict| regexp.match(slug_conflict)[1].to_i }
|
|
38
|
+
.max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return the unnumbered (shortest) slug first, followed by the numbered ones
|
|
42
|
+
# in ascending order.
|
|
43
|
+
def ordering_query
|
|
44
|
+
"#{sql_length}(#{slug_column}) ASC, #{slug_column} ASC"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def regexp
|
|
48
|
+
/#{slug}#{sequence_separator}(\d+)\z/
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def sequential_slug_matcher
|
|
52
|
+
# Underscores (matching a single character) and percent signs (matching
|
|
53
|
+
# any number of characters) need to be escaped. While this looks like
|
|
54
|
+
# an excessive number of backslashes, it is correct.
|
|
55
|
+
"#{slug}#{sequence_separator}".gsub(/[_%]/, '\\\\\&') + "%"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def slug_conflicts
|
|
59
|
+
scope
|
|
60
|
+
.where(conflict_query, slug, sequential_slug_matcher)
|
|
61
|
+
.order(Arel.sql(ordering_query)).pluck(Arel.sql(slug_column))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sql_length
|
|
65
|
+
/sqlserver/i.match?(scope.connection.adapter_name) ? "LEN" : "LENGTH"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "sequentially_slugged/calculator"
|
|
2
|
+
|
|
3
|
+
module FriendlyId
|
|
4
|
+
module SequentiallySlugged
|
|
5
|
+
def self.setup(model_class)
|
|
6
|
+
model_class.friendly_id_config.use :slugged
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def resolve_friendly_id_conflict(candidate_slugs)
|
|
10
|
+
candidate = candidate_slugs.first
|
|
11
|
+
return if candidate.nil?
|
|
12
|
+
|
|
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
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def slug_base_class
|
|
25
|
+
if friendly_id_config.uses?(:history)
|
|
26
|
+
Slug
|
|
27
|
+
else
|
|
28
|
+
self.class.base_class
|
|
29
|
+
end
|
|
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
|
|
39
|
+
end
|
|
40
|
+
end
|