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.
@@ -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