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.
@@ -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::FinderMethods
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
- send FriendlyId::ActiveRecordAdapter::Compat.scope_method, :similar_to, lambda {|slug| {:conditions => {
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::FinderMethods
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
- slug.update_attributes :scope => send(friendly_id_config.scope).to_param
212
- rescue ActiveRecord::StatementInvalid
213
- slug.update_attributes :sequence => Slug.similar_to(slug).first.sequence.succ
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
 
@@ -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