friendly_id 3.0.6 → 3.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|