friendly_id 3.0.6 → 3.1.0.pre
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.
- data/Changelog.md +10 -0
- data/Contributors.md +1 -0
- data/Guide.md +24 -35
- data/README.md +3 -3
- data/Rakefile +0 -5
- data/extras/bench.rb +5 -1
- data/extras/prof.rb +9 -4
- data/lib/friendly_id.rb +35 -7
- data/lib/friendly_id/active_record.rb +32 -17
- data/lib/friendly_id/active_record_adapter/configuration.rb +1 -0
- data/lib/friendly_id/active_record_adapter/finders.rb +136 -130
- data/lib/friendly_id/active_record_adapter/relation.rb +129 -0
- data/lib/friendly_id/active_record_adapter/simple_model.rb +2 -62
- data/lib/friendly_id/active_record_adapter/slug.rb +3 -2
- data/lib/friendly_id/active_record_adapter/slugged_model.rb +13 -145
- data/lib/friendly_id/configuration.rb +1 -1
- data/lib/friendly_id/railtie.rb +2 -2
- data/lib/friendly_id/slug_string.rb +22 -392
- data/lib/friendly_id/slugged.rb +7 -3
- data/lib/friendly_id/test.rb +1 -2
- data/lib/friendly_id/version.rb +3 -3
- data/test/active_record_adapter/ar_test_helper.rb +14 -6
- data/test/active_record_adapter/cached_slug_test.rb +10 -0
- data/test/active_record_adapter/core.rb +15 -0
- data/test/active_record_adapter/slugged.rb +0 -1
- data/test/active_record_adapter/support/models.rb +4 -0
- data/test/active_record_adapter/tasks_test.rb +1 -1
- data/test/friendly_id_test.rb +12 -16
- data/test/test_helper.rb +9 -10
- metadata +18 -16
- data/lib/friendly_id/finders.rb +0 -109
- data/test/slug_string_test.rb +0 -88
@@ -0,0 +1,129 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module ActiveRecordAdapter
|
3
|
+
module Relation
|
4
|
+
|
5
|
+
attr :friendly_id_scope
|
6
|
+
|
7
|
+
# This method overrides Active Record's default in order to allow the :scope option to
|
8
|
+
# be passed to finds.
|
9
|
+
def apply_finder_options(options)
|
10
|
+
@friendly_id_scope = options.delete(:scope)
|
11
|
+
@friendly_id_scope = @friendly_id_scope.to_param if @friendly_id_scope.respond_to?(:to_param)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def find_one(id)
|
18
|
+
begin
|
19
|
+
return super if !@klass.uses_friendly_id? or id.unfriendly_id?
|
20
|
+
return find_one_using_cached_slug(id) if friendly_id_config.cache_column?
|
21
|
+
return find_one_using_slug(id) if friendly_id_config.use_slugs?
|
22
|
+
record = where(friendly_id_config.column => id).first
|
23
|
+
if record
|
24
|
+
record.friendly_id_status.name = name
|
25
|
+
record
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
rescue ActiveRecord::RecordNotFound => error
|
30
|
+
uses_friendly_id? && friendly_id_config.scope? ? raise_scoped_error(error) : raise(error)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_some(ids)
|
35
|
+
return super unless @klass.uses_friendly_id?
|
36
|
+
ids = ids.compact.uniq.map {|id| id.respond_to?(:friendly_id_config) ? id.id.to_i : id}
|
37
|
+
records = friendly_records(*ids.partition {|id| id.friendly_id?}).each do |record|
|
38
|
+
record.friendly_id_status.name = ids
|
39
|
+
end
|
40
|
+
validate_expected_size!(ids, records)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def find_one_using_slug(id)
|
46
|
+
name, seq = id.to_s.parse_friendly_id
|
47
|
+
slug = Slug.where(:name => name, :sequence => seq, :scope => friendly_id_scope,
|
48
|
+
:sluggable_type => @klass.base_class.to_s).first
|
49
|
+
if slug
|
50
|
+
record = find_one(slug.sluggable_id.to_i)
|
51
|
+
record.friendly_id_status.name = name
|
52
|
+
record.friendly_id_status.sequence = seq
|
53
|
+
record.friendly_id_status.slug = slug
|
54
|
+
record
|
55
|
+
else
|
56
|
+
find_one_without_friendly(id)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_one_using_cached_slug(id)
|
61
|
+
record = where(friendly_id_config.cache_column => id).first
|
62
|
+
if record
|
63
|
+
name, seq = id.to_s.parse_friendly_id
|
64
|
+
record.friendly_id_status.name = name
|
65
|
+
record.friendly_id_status.sequence = seq
|
66
|
+
record
|
67
|
+
else
|
68
|
+
find_one_using_slug(id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def raise_scoped_error(error)
|
73
|
+
scope_message = friendly_id_scope || "expected, but none given"
|
74
|
+
message = "%s, scope: %s" % [error.message, scope_message]
|
75
|
+
raise ActiveRecord::RecordNotFound, message
|
76
|
+
end
|
77
|
+
|
78
|
+
def friendly_records(friendly_ids, unfriendly_ids)
|
79
|
+
use_slugs = friendly_id_config.use_slugs? && !friendly_id_config.cache_column?
|
80
|
+
column = friendly_id_config.cache_column || friendly_id_config.column
|
81
|
+
friendly = use_slugs ? slugged_conditions(friendly_ids) : arel_table[column].in(friendly_ids)
|
82
|
+
unfriendly = arel_table[primary_key].in unfriendly_ids
|
83
|
+
if friendly_ids.present? && unfriendly_ids.present?
|
84
|
+
clause = friendly.or(unfriendly)
|
85
|
+
elsif friendly_ids.present?
|
86
|
+
clause = friendly
|
87
|
+
elsif unfriendly_ids.present?
|
88
|
+
clause = unfriendly
|
89
|
+
end
|
90
|
+
use_slugs ? joins(:slugs).where(clause) : where(clause)
|
91
|
+
end
|
92
|
+
|
93
|
+
def slugged_conditions(ids)
|
94
|
+
return if ids.empty?
|
95
|
+
slugs = Slug.arel_table
|
96
|
+
conditions = lambda do |id|
|
97
|
+
name, seq = id.parse_friendly_id
|
98
|
+
slugs[:name].eq(name).and(slugs[:sequence].eq(seq)).and(slugs[:scope].eq(friendly_id_scope))
|
99
|
+
end
|
100
|
+
ids.inject(nil) {|clause, id| clause ? clause.or(conditions.call(id)) : conditions.call(id) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate_expected_size!(ids, result)
|
104
|
+
expected_size =
|
105
|
+
if @limit_value && ids.size > @limit_value
|
106
|
+
@limit_value
|
107
|
+
else
|
108
|
+
ids.size
|
109
|
+
end
|
110
|
+
|
111
|
+
# 11 ids with limit 3, offset 9 should give 2 results.
|
112
|
+
if @offset_value && (ids.size - @offset_value < expected_size)
|
113
|
+
expected_size = ids.size - @offset_value
|
114
|
+
end
|
115
|
+
|
116
|
+
if result.size == expected_size
|
117
|
+
result
|
118
|
+
else
|
119
|
+
conditions = arel.send(:where_clauses).join(', ')
|
120
|
+
conditions = " [WHERE #{conditions}]" if conditions.present?
|
121
|
+
|
122
|
+
error = "Couldn't find all #{@klass.name.pluralize} with IDs "
|
123
|
+
error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
124
|
+
raise ActiveRecord::RecordNotFound, error
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -3,66 +3,6 @@ module FriendlyId
|
|
3
3
|
|
4
4
|
module SimpleModel
|
5
5
|
|
6
|
-
# Some basic methods common to {MultipleFinder} and {SingleFinder}.
|
7
|
-
module SimpleFinder
|
8
|
-
|
9
|
-
# The column used to store the friendly_id.
|
10
|
-
def column
|
11
|
-
"#{table_name}.#{friendly_id_config.column}"
|
12
|
-
end
|
13
|
-
|
14
|
-
# The model's fully-qualified and quoted primary key.
|
15
|
-
def primary_key
|
16
|
-
"#{quoted_table_name}.#{model_class.send :primary_key}"
|
17
|
-
end
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
class MultipleFinder
|
22
|
-
|
23
|
-
include FriendlyId::ActiveRecordAdapter::Finders::Multiple
|
24
|
-
include SimpleFinder
|
25
|
-
|
26
|
-
def find
|
27
|
-
@results = model_class.scoped(:conditions => conditions).all(options).uniq
|
28
|
-
raise(::ActiveRecord::RecordNotFound, error_message) if @results.size != expected_size
|
29
|
-
friendly_results.each { |result| result.friendly_id_status.name = result.to_param }
|
30
|
-
@results
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def conditions
|
36
|
-
["#{primary_key} IN (?) OR #{column} IN (?)", unfriendly_ids, friendly_ids]
|
37
|
-
end
|
38
|
-
|
39
|
-
def friendly_results
|
40
|
-
results.select { |result| friendly_ids.include? result.to_param }
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
class SingleFinder
|
46
|
-
|
47
|
-
include FriendlyId::Finders::Base
|
48
|
-
include FriendlyId::Finders::Single
|
49
|
-
include SimpleFinder
|
50
|
-
|
51
|
-
def find
|
52
|
-
result = model_class.scoped(find_options).first(options)
|
53
|
-
raise ::ActiveRecord::RecordNotFound.new if friendly? && !result
|
54
|
-
result.friendly_id_status.name = id if result
|
55
|
-
result
|
56
|
-
end
|
57
|
-
|
58
|
-
private
|
59
|
-
|
60
|
-
def find_options
|
61
|
-
@find_options ||= {:conditions => {column => id}}
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
6
|
def self.included(base)
|
67
7
|
base.class_eval do
|
68
8
|
column = friendly_id_config.column
|
@@ -70,7 +10,7 @@ module FriendlyId
|
|
70
10
|
validates_presence_of column, :unless => :skip_friendly_id_validations
|
71
11
|
validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
|
72
12
|
after_update :update_scopes
|
73
|
-
extend FriendlyId::ActiveRecordAdapter::
|
13
|
+
extend FriendlyId::ActiveRecordAdapter::Finders unless FriendlyId.on_ar3?
|
74
14
|
end
|
75
15
|
end
|
76
16
|
|
@@ -120,4 +60,4 @@ module FriendlyId
|
|
120
60
|
|
121
61
|
end
|
122
62
|
end
|
123
|
-
end
|
63
|
+
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# A Slug is a unique, human-friendly identifier for an ActiveRecord.
|
2
2
|
class Slug < ::ActiveRecord::Base
|
3
3
|
|
4
|
+
def self.named_scope(*args, &block) scope(*args, &block) end if FriendlyId.on_ar3?
|
4
5
|
table_name = "slugs"
|
5
6
|
belongs_to :sluggable, :polymorphic => true
|
6
7
|
before_save :enable_name_reversion, :set_sequence
|
7
8
|
validate :validate_name
|
8
|
-
|
9
|
+
named_scope :similar_to, lambda {|slug| {:conditions => {
|
9
10
|
:name => slug.name,
|
10
11
|
:scope => slug.scope,
|
11
12
|
:sluggable_type => slug.sluggable_type
|
@@ -63,4 +64,4 @@ class Slug < ::ActiveRecord::Base
|
|
63
64
|
self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
|
64
65
|
end
|
65
66
|
|
66
|
-
end
|
67
|
+
end
|
@@ -2,145 +2,6 @@ module FriendlyId
|
|
2
2
|
module ActiveRecordAdapter
|
3
3
|
module SluggedModel
|
4
4
|
|
5
|
-
module SluggedFinder
|
6
|
-
# Whether :include => :slugs has been passed as an option.
|
7
|
-
def slugs_included?
|
8
|
-
[*(options[:include] or [])].flatten.include?(:slugs)
|
9
|
-
end
|
10
|
-
|
11
|
-
def handle_friendly_result
|
12
|
-
raise ::ActiveRecord::RecordNotFound.new unless @result
|
13
|
-
@result.friendly_id_status.friendly_id = id
|
14
|
-
end
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
class MultipleFinder
|
19
|
-
|
20
|
-
include FriendlyId::ActiveRecordAdapter::Finders::Multiple
|
21
|
-
include SluggedFinder
|
22
|
-
|
23
|
-
attr_reader :slugs
|
24
|
-
|
25
|
-
def find
|
26
|
-
@results = model_class.scoped(find_options).all(options).uniq
|
27
|
-
raise ::ActiveRecord::RecordNotFound, error_message if @results.size != expected_size
|
28
|
-
@results.each {|result| result.friendly_id_status.name = slug_for(result)}
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def find_conditions
|
34
|
-
"%s IN (%s)" % [
|
35
|
-
"#{quoted_table_name}.#{primary_key}",
|
36
|
-
(unfriendly_ids + sluggable_ids).join(",")
|
37
|
-
]
|
38
|
-
end
|
39
|
-
|
40
|
-
def find_options
|
41
|
-
{:select => "#{quoted_table_name}.*", :conditions => find_conditions,
|
42
|
-
:joins => slugs_included? ? options[:joins] : :slugs}
|
43
|
-
end
|
44
|
-
|
45
|
-
def sluggable_ids
|
46
|
-
@sluggable_ids ||= slugs.map(&:sluggable_id)
|
47
|
-
end
|
48
|
-
|
49
|
-
def slugs
|
50
|
-
@slugs ||= friendly_ids.map do |friendly_id|
|
51
|
-
name, sequence = friendly_id.parse_friendly_id(friendly_id_config.sequence_separator)
|
52
|
-
Slug.first :conditions => {
|
53
|
-
:name => name,
|
54
|
-
:scope => scope,
|
55
|
-
:sequence => sequence,
|
56
|
-
:sluggable_type => base_class.name
|
57
|
-
}
|
58
|
-
end.compact
|
59
|
-
end
|
60
|
-
|
61
|
-
def slug_for(result)
|
62
|
-
slugs.detect {|slug| result.id == slug.sluggable_id}
|
63
|
-
end
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
# Performs a find a single friendly_id using the cached_slug column,
|
68
|
-
# if available. This is significantly faster, and can be used in all
|
69
|
-
# circumstances unless the +:scope+ argument is present.
|
70
|
-
class CachedMultipleFinder < SimpleModel::MultipleFinder
|
71
|
-
# The column used to store the cached slug.
|
72
|
-
def column
|
73
|
-
"#{table_name}.#{friendly_id_config.cache_column}"
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
class SingleFinder
|
78
|
-
|
79
|
-
include FriendlyId::Finders::Base
|
80
|
-
include FriendlyId::Finders::Single
|
81
|
-
include SluggedFinder
|
82
|
-
|
83
|
-
def find
|
84
|
-
@result = model_class.scoped(find_options).first(options)
|
85
|
-
handle_friendly_result if @result or friendly_id_config.scope?
|
86
|
-
@result
|
87
|
-
rescue ::ActiveRecord::RecordNotFound => @error
|
88
|
-
friendly_id_config.scope? ? raise_scoped_error : (raise @error)
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def find_options
|
94
|
-
slug_table = Slug.table_name
|
95
|
-
{
|
96
|
-
:select => "#{model_class.quoted_table_name}.*",
|
97
|
-
:joins => slugs_included? ? options[:joins] : :slugs,
|
98
|
-
:conditions => {
|
99
|
-
"#{slug_table}.name" => name,
|
100
|
-
"#{slug_table}.scope" => scope,
|
101
|
-
"#{slug_table}.sequence" => sequence
|
102
|
-
}
|
103
|
-
}
|
104
|
-
end
|
105
|
-
|
106
|
-
def raise_scoped_error
|
107
|
-
scope_message = scope || "expected, but none given"
|
108
|
-
message = "%s, scope: %s" % [@error.message, scope_message]
|
109
|
-
raise ::ActiveRecord::RecordNotFound, message
|
110
|
-
end
|
111
|
-
|
112
|
-
end
|
113
|
-
|
114
|
-
# Performs a find for multiple friendly_ids using the cached_slug column,
|
115
|
-
# if available. This is significantly faster, and can be used in all
|
116
|
-
# circumstances unless the +:scope+ argument is present.
|
117
|
-
class CachedSingleFinder < SimpleModel::SingleFinder
|
118
|
-
|
119
|
-
include SluggedFinder
|
120
|
-
|
121
|
-
def find
|
122
|
-
@result = model_class.scoped(find_options).first(options)
|
123
|
-
if @result
|
124
|
-
handle_friendly_result
|
125
|
-
@result
|
126
|
-
else
|
127
|
-
uncached_find
|
128
|
-
end
|
129
|
-
rescue ActiveRecord::RecordNotFound
|
130
|
-
uncached_find
|
131
|
-
end
|
132
|
-
|
133
|
-
def uncached_find
|
134
|
-
SingleFinder.new(id, model_class, options).find
|
135
|
-
end
|
136
|
-
|
137
|
-
# The column used to store the cached slug.
|
138
|
-
def column
|
139
|
-
"#{table_name}.#{friendly_id_config.cache_column}"
|
140
|
-
end
|
141
|
-
|
142
|
-
end
|
143
|
-
|
144
5
|
def self.included(base)
|
145
6
|
base.class_eval do
|
146
7
|
has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
|
@@ -150,7 +11,7 @@ module FriendlyId
|
|
150
11
|
after_update :update_scope
|
151
12
|
after_update :update_dependent_scopes
|
152
13
|
protect_friendly_id_attributes
|
153
|
-
extend FriendlyId::ActiveRecordAdapter::
|
14
|
+
extend FriendlyId::ActiveRecordAdapter::Finders unless FriendlyId.on_ar3?
|
154
15
|
end
|
155
16
|
end
|
156
17
|
|
@@ -160,7 +21,8 @@ module FriendlyId
|
|
160
21
|
slugs.find_by_name_and_sequence(name, sequence)
|
161
22
|
end
|
162
23
|
|
163
|
-
# Returns the friendly id, or if none is available, the numeric id.
|
24
|
+
# Returns the friendly id, or if none is available, the numeric id. Note that this
|
25
|
+
# method will use the cached_slug value if present, unlike {#friendly_id}.
|
164
26
|
def to_param
|
165
27
|
friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
|
166
28
|
end
|
@@ -184,7 +46,8 @@ module FriendlyId
|
|
184
46
|
# Build the new slug using the generated friendly id.
|
185
47
|
def build_a_slug
|
186
48
|
return unless new_slug_needed?
|
187
|
-
@slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self)
|
49
|
+
@slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self),
|
50
|
+
:sluggable => self
|
188
51
|
@new_friendly_id = @slug.to_friendly_id
|
189
52
|
end
|
190
53
|
|
@@ -208,9 +71,14 @@ module FriendlyId
|
|
208
71
|
|
209
72
|
def update_scope
|
210
73
|
return unless slug && scope_changed?
|
211
|
-
|
212
|
-
|
213
|
-
|
74
|
+
self.class.transaction do
|
75
|
+
slug.scope = send(friendly_id_config.scope).to_param
|
76
|
+
similar = Slug.similar_to(slug)
|
77
|
+
if !similar.empty?
|
78
|
+
slug.sequence = similar.first.sequence.succ
|
79
|
+
end
|
80
|
+
slug.save!
|
81
|
+
end
|
214
82
|
end
|
215
83
|
|
216
84
|
# Update the slugs for any model that is using this model as its
|
@@ -46,7 +46,7 @@ module FriendlyId
|
|
46
46
|
# The class that's using the configuration.
|
47
47
|
attr_reader :configured_class
|
48
48
|
|
49
|
-
# The maximum allowed length for a friendly_id string. This is checked *after* a
|
49
|
+
# The maximum allowed byte length for a friendly_id string. This is checked *after* a
|
50
50
|
# string is processed by FriendlyId to remove spaces, special characters, etc.
|
51
51
|
attr_accessor :max_length
|
52
52
|
|
data/lib/friendly_id/railtie.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module FriendlyId
|
2
2
|
class Railtie < Rails::Railtie
|
3
|
-
|
3
|
+
|
4
4
|
initializer "friendly_id.configure_rails_initialization" do |app|
|
5
5
|
# Experimental Sequel support. See: http://github.com/norman/friendly_id_sequel
|
6
6
|
if app.config.generators.rails[:orm] == :sequel
|
@@ -11,7 +11,7 @@ module FriendlyId
|
|
11
11
|
require "friendly_id/active_record"
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
rake_tasks do
|
16
16
|
load "tasks/friendly_id.rake"
|
17
17
|
end
|