friendly_id 3.1.3 → 3.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.md CHANGED
@@ -6,6 +6,11 @@ suggestions, ideas and improvements to FriendlyId.
6
6
  * Table of Contents
7
7
  {:toc}
8
8
 
9
+ ## 3.1.4 (2010-08-30)
10
+
11
+ * Significantly improve performance of queries using slugs with no cache on AR3.
12
+ * Fix callbacks being invoked after setting cached slugs.
13
+
9
14
  ## 3.1.3 (2010-08-11)
10
15
 
11
16
  * Reverted approach to read-only fix from previous release.
data/Guide.md CHANGED
@@ -5,9 +5,9 @@
5
5
 
6
6
  ## Overview
7
7
 
8
- FriendlyId is a Ruby gem which allows you to work with human-friendly strings
9
- as if they were numeric ids for Active Record models. This facilitates replacing
10
- "unfriendly" URL's like
8
+ FriendlyId is an ORM-centric Ruby library that lets you work with human-friendly
9
+ strings as if they were numeric ids. Among other things, this facilitates
10
+ replacing "unfriendly" URL's like:
11
11
 
12
12
  http://example.com/states/4323454
13
13
 
@@ -15,6 +15,10 @@ with "friendly" ones such as:
15
15
 
16
16
  http://example.com/states/washington
17
17
 
18
+ FriendlyId is typically used with Rails and Active Record, but can also be used in
19
+ non-Rails applications, and with [Sequel](http://github.com/norman/friendly_id_sequel) and
20
+ [DataMapper](http://github.com/myabc/friendly_id_datamapper).
21
+
18
22
  ## Simple Models
19
23
 
20
24
  The simplest way to use FriendlyId is with a model that has a uniquely indexed
@@ -30,9 +34,9 @@ updated. The most common example of this is a user name or login column:
30
34
  @user.to_param # returns "joe"
31
35
  redirect_to @user # the URL will be /users/joe
32
36
 
33
- In this case, FriendlyId assumes you want to use the column as-is; FriendlyId
34
- will never modify the value of the column, and your application must ensure
35
- that the value is admissible in a URL:
37
+ In this case, FriendlyId assumes you want to use the column as-is; it will never
38
+ modify the value of the column, and your application should ensure that the value
39
+ is admissible in a URL:
36
40
 
37
41
  class City < ActiveRecord::Base
38
42
  has_friendly_id :name
@@ -60,8 +64,8 @@ wish to modify to make them more suitable for use in URL's.
60
64
  redirect_to @post # the URL will be /posts/this-is-the-first-post
61
65
 
62
66
  If you are unsure whether to use slugs, then your best bet is to use them,
63
- because FriendlyId provides many useful features that only work with slugs.
64
- These features are explained in detail {file:Guide.md#features below}.
67
+ because FriendlyId provides many useful features that only work with this
68
+ feature. These features are explained in detail {file:Guide.md#features below}.
65
69
 
66
70
  ## Installation
67
71
 
@@ -76,13 +80,13 @@ with Rails 2.3.x. and 3.0.
76
80
 
77
81
  After installing the gem, add an entry in environment.rb:
78
82
 
79
- config.gem "friendly_id", :version => "~> 2.3"
83
+ config.gem "friendly_id", :version => "~> 3.1"
80
84
 
81
85
  ### Rails 3.0
82
86
 
83
87
  After installing the gem, add an entry in the Gemfile:
84
88
 
85
- gem "friendly_id", "~> 3.0"
89
+ gem "friendly_id", "~> 3.1"
86
90
 
87
91
  ### As a Plugin
88
92
 
@@ -94,17 +98,24 @@ However, installing as a gem offers simpler version control than plugin
94
98
  installation. Whenever possible, install as a gem instead. Plugin support may
95
99
  eventually be removed in a future version.
96
100
 
101
+ ### Future Compatibility
102
+
103
+ FriendlyId will always remain compatible with the current release of Rails, and
104
+ at least one stable release behind. That means that support for 2.3.x will not be
105
+ dropped until a stable release of 3.1 is out, or possibly longer.
106
+
97
107
  ### Setup
98
108
 
99
109
  After installing either as a gem or plugin, run:
100
110
 
101
- ./script/generate friendly_id
111
+
112
+ rails generate friendly_id
102
113
  rake db:migrate
103
114
 
104
115
  This will install the Rake tasks and slug migration for FriendlyId. If you are
105
- not going to use slugs, you can do:
116
+ not going to use slugs, you can use the `skip-migration` option:
106
117
 
107
- ./script/generate friendly_id --skip-migration
118
+ rails generate friendly_id --skip-migration
108
119
 
109
120
  FriendlyId is now set up and ready for you to use.
110
121
 
@@ -147,7 +158,7 @@ dashes, and non-word characters other than "-" are removed.
147
158
 
148
159
  ### Replacing Accented Characters
149
160
 
150
- If your strings use Western characters, you can use the `:approximate_ascii` option to remove
161
+ If your strings use Latin characters, you can use the `:approximate_ascii` option to remove
151
162
  accents and other diacritics:
152
163
 
153
164
  class City < ActiveRecord::Base
@@ -169,7 +180,7 @@ There are special options for some languages:
169
180
 
170
181
  FriendlyId supports whatever languages are supported by
171
182
  [Babosa](https://github.com/norman/babosa); at the time of writing, this
172
- includes German, Spanish and Serbian.
183
+ includes Danish, German, Serbian and Spanish.
173
184
 
174
185
  ### Unicode Slugs
175
186
 
@@ -289,8 +300,8 @@ unique if necessary:
289
300
  ...
290
301
  etc.
291
302
 
292
- Note that the number is preceded by "--" to distinguish it from the
293
- rest of the slug. This is important to enable having slugs like:
303
+ Note that the number is preceded by "--" rather than "-" to distinguish it from
304
+ the rest of the slug. This is important to enable having slugs like:
294
305
 
295
306
  /cars/peugeot-206
296
307
  /cars/peugeot-206--2
@@ -298,7 +309,7 @@ rest of the slug. This is important to enable having slugs like:
298
309
  You can configure the separator string used by your model by setting the
299
310
  `:sequence_separator` option in `has_friendly_id`:
300
311
 
301
- has_friendly_id :title, :use_slug => true, :sequence_separator => ";"
312
+ has_friendly_id :title, :use_slug => true, :sequence_separator => ":"
302
313
 
303
314
  You can also override the default used in
304
315
  {FriendlyId::Configuration::DEFAULTS} to set the value for any model using
@@ -334,13 +345,12 @@ Checking the slugs table all the time has an impact on performance, so as of
334
345
  ### Automatic setup
335
346
 
336
347
  To enable slug caching, simply add a column named "cached_slug" to your model.
337
- Is also advised to index this column for performance reason.
338
348
  FriendlyId will automatically use this column if it detects it:
339
349
 
340
350
  class AddCachedSlugToUsers < ActiveRecord::Migration
341
351
  def self.up
342
352
  add_column :users, :cached_slug, :string
343
- add_index :users, :cached_slug
353
+ add_index :users, :cached_slug, :unique => true
344
354
  end
345
355
 
346
356
  def self.down
@@ -353,8 +363,7 @@ Then, redo the slugs:
353
363
  rake friendly_id:redo_slugs MODEL=User
354
364
 
355
365
  FriendlyId will automatically query against the cache column if it's available,
356
- which significantly improves the performance of many queries, particularly when
357
- passing an array of friendly ids to #find.
366
+ which will <a href="#some_benchmarks">improve the performance</a> of many queries.
358
367
 
359
368
  A few warnings when using this feature:
360
369
 
@@ -376,6 +385,8 @@ You can also use a different name for the column if you choose, via the
376
385
  has_friendly_id :name, :use_slug => true, :cache_column => 'my_cached_slug'
377
386
  end
378
387
 
388
+ Don't use "slug" or "slugs" because FriendlyId needs those names for its own
389
+ purposes.
379
390
 
380
391
  ## Nil slugs and skipping validations
381
392
 
@@ -504,10 +515,10 @@ Or even better, unless you're using a custom primary key:
504
515
  because sorting by a unique integer column is faster than sorting by a date
505
516
  column.
506
517
 
507
- ## MySQL 5.0 or less
518
+ ## MySQL MyISAM tables
508
519
 
509
- Currently, the default FriendlyId migration will not work with MySQL 5.0 or less
510
- because it creates and index that's too large. The easiest way to work around
520
+ Currently, the default FriendlyId migration will not work with MyISAM tables
521
+ because it creates an index that's too large. The easiest way to work around
511
522
  this is to change the generated migration to add limits on some column lengths.
512
523
  Please see [this issue](http://github.com/norman/friendly_id/issues#issue/50) in
513
524
  the FriendlyId issue tracker for more information.
@@ -537,29 +548,31 @@ enabled. But if it is, then your patches would be very welcome!
537
548
 
538
549
 
539
550
  activerecord (2.3.8)
540
- ruby 1.9.2dev (2010-07-06 revision 28549) [x86_64-darwin10.4.0]
541
- friendly_id (3.1.0)
551
+ ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
552
+ friendly_id (3.1.4)
553
+ sqlite3-ruby (1.3.1)
542
554
  sqlite3 3.6.12 in-memory database
543
555
 
544
556
  | DEFAULT | NO_SLUG | SLUG | CACHED_SLUG |
545
557
  ------------------------------------------------------------------------------------------------
546
- find model by id x1000 | 0.340 | 0.468 | 0.877 | 0.534 |
547
- find model using array of ids x1000 | 0.551 | 0.592 | 0.989 | 0.893 |
548
- find model using id, then to_param x1000 | 0.340 | 0.487 | 1.310 | 0.551 |
558
+ find model by id x1000 | 0.370 | 0.503 | 0.940 | 0.562 |
559
+ find model using array of ids x1000 | 0.612 | 0.615 | 1.054 | 0.957 |
560
+ find model using id, then to_param x1000 | 0.374 | 0.535 | 1.396 | 0.567 |
549
561
  ================================================================================================
550
- Total | 1.231 | 1.547 | 3.176 | 1.979 |
551
-
562
+ Total | 1.356 | 1.653 | 3.390 | 2.086 |
552
563
 
553
564
 
554
- activerecord (3.0.0.beta4)
555
- ruby 1.9.2dev (2010-07-06 revision 28549) [x86_64-darwin10.4.0]
556
- friendly_id (3.1.0)
565
+ activerecord (3.0.0)
566
+ ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
567
+ friendly_id (3.1.4)
568
+ sqlite3-ruby (1.3.1)
557
569
  sqlite3 3.6.12 in-memory database
558
570
 
559
571
  | DEFAULT | NO_SLUG | SLUG | CACHED_SLUG |
560
572
  ------------------------------------------------------------------------------------------------
561
- find model by id x1000 | 0.495 | 0.608 | 1.519 | 0.641 |
562
- find model using array of ids x1000 | 0.708 | 0.722 | 6.483 | 0.782 |
563
- find model using id, then to_param x1000 | 0.506 | 0.645 | 2.644 | 0.637 |
573
+ find model by id x1000 | 0.286 | 0.365 | 0.518 | 0.393 |
574
+ find model using array of ids x1000 | 0.329 | 0.441 | 0.709 | 0.475 |
575
+ find model using id, then to_param x1000 | 0.321 | 0.332 | 0.976 | 0.399 |
564
576
  ================================================================================================
565
- Total | 1.709 | 1.976 | 10.645 | 2.061 |
577
+ Total | 0.936 | 1.138 | 2.203 | 1.266 |
578
+
File without changes
data/README.md CHANGED
@@ -19,13 +19,21 @@ versioning, scoped slugs, reserved words, custom slug generators, and
19
19
  excellent Unicode support. For complete information on using FriendlyId,
20
20
  please see the [FriendlyId Guide](http://norman.github.com/friendly_id/file.Guide.html).
21
21
 
22
- FriendlyId is compatible with Active Record 2.3.x and 3.0.
22
+ FriendlyId is compatible with Active Record **2.3.x** and **3.0**.
23
+
24
+ ## Docs, Info and Support
25
+
26
+ * [FriendlyId Guide](http://norman.github.com/friendly_id/file.Guide.html)
27
+ * [API Docs](http://norman.github.com/friendly_id)
28
+ * [Google Group](http://groups.google.com/group/friendly_id)
29
+ * [Source Code](http://github.com/norman/friendly_id/)
30
+ * [Issue Tracker](http://github.com/norman/friendly_id/issues)
23
31
 
24
32
  ## Rails Quickstart
25
33
 
26
34
  gem install friendly_id
27
35
 
28
- rails my_app
36
+ rails new my_app
29
37
 
30
38
  cd my_app
31
39
 
@@ -44,7 +52,7 @@ FriendlyId is compatible with Active Record 2.3.x and 3.0.
44
52
 
45
53
  User.create! :name => "Joe Schmoe"
46
54
 
47
- ./script/server
55
+ rails server
48
56
 
49
57
  GET http://0.0.0.0:3000/users/joe-schmoe
50
58
 
@@ -56,15 +64,7 @@ in progress. To find out more, check out the Github projects:
56
64
  * [http://github.com/norman/friendly_id_sequel](http://github.com/norman/friendly_id_sequel)
57
65
  * [http://github.com/myabc/friendly_id_datamapper](http://github.com/myabc/friendly_id_datamapper)
58
66
 
59
- ## Docs, Info and Support
60
-
61
- * [FriendlyId Guide](http://norman.github.com/friendly_id/file.Guide.html)
62
- * [API Docs](http://norman.github.com/friendly_id)
63
- * [Google Group](http://groups.google.com/group/friendly_id)
64
- * [Source Code](http://github.com/norman/friendly_id/)
65
- * [Issue Tracker](http://github.com/norman/friendly_id/issues)
66
-
67
- ## Bugs:
67
+ ## Bugs
68
68
 
69
69
  Please report them on the [Github issue tracker](http://github.com/norman/friendly_id/issues)
70
70
  for this project.
@@ -79,7 +79,7 @@ If you have a bug to report, please include the following information:
79
79
  If you are able to, it helps even more if you can fork FriendlyId on Github,
80
80
  and add a test that reproduces the error you are experiencing.
81
81
 
82
- ## Credits:
82
+ ## Credits
83
83
 
84
84
  FriendlyId was created by Norman Clarke, Adrian Mugnolo, and Emilio Tagua.
85
85
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
1
3
  require "rake"
2
4
  require "rake/testtask"
3
5
  require "rake/gempackagetask"
data/extras/bench.rb CHANGED
@@ -37,4 +37,4 @@ RBench.run(TIMES) do
37
37
 
38
38
  summary 'Total'
39
39
 
40
- end
40
+ end
@@ -26,8 +26,8 @@ module FriendlyId
26
26
  end
27
27
 
28
28
  def find_one
29
- return find_one_using_cached_slug if cache_column?
30
- return find_one_using_slug if use_slugs?
29
+ return find_one_with_cached_slug if cache_column?
30
+ return find_one_with_slug if use_slugs?
31
31
  @result = scoped(:conditions => ["#{table_name}.#{fc.column} = ?", id]).first(options)
32
32
  assign_status
33
33
  end
@@ -56,12 +56,12 @@ module FriendlyId
56
56
 
57
57
  private
58
58
 
59
- def find_one_using_cached_slug
59
+ def find_one_with_cached_slug
60
60
  @result = scoped(:conditions => ["#{table_name}.#{cache_column} = ?", id]).first(options)
61
- assign_status or find_one_using_slug
61
+ assign_status or find_one_with_slug
62
62
  end
63
63
 
64
- def find_one_using_slug
64
+ def find_one_with_slug
65
65
  name, seq = id.to_s.parse_friendly_id
66
66
  scope = scoped(:joins => :slugs, :conditions => {:slugs => {:name => name, :sequence => seq}})
67
67
  scope = scope.scoped(:conditions => {:slugs => {:scope => scope_val}}) if fc.scope?
@@ -2,128 +2,170 @@ module FriendlyId
2
2
  module ActiveRecordAdapter
3
3
  module Relation
4
4
 
5
- attr :friendly_id_scope
5
+ class Find
6
+ extend Forwardable
6
7
 
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
8
+ attr :relation
9
+ attr :ids
10
+ alias id ids
14
11
 
15
- protected
12
+ def_delegators :relation, :arel, :arel_table, :friendly_id_scope,
13
+ :klass, :limit_value, :offset_value, :where
14
+ def_delegators :klass, :connection, :friendly_id_config
15
+ alias fc friendly_id_config
16
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
17
+ def initialize(relation, ids)
18
+ @relation = relation
19
+ @ids = ids
20
+ end
21
+
22
+ def find_one
23
+ if fc.cache_column?
24
+ find_one_with_cached_slug
25
+ elsif fc.use_slugs?
26
+ find_one_with_slug
26
27
  else
27
- super
28
+ find_one_without_slug
28
29
  end
29
- rescue ActiveRecord::RecordNotFound => error
30
- uses_friendly_id? && friendly_id_config.scope? ? raise_scoped_error(error) : raise(error)
31
30
  end
32
- end
33
31
 
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
32
+ def find_some
33
+ ids = @ids.compact.uniq.map {|id| id.respond_to?(:friendly_id_config) ? id.id.to_i : id}
34
+ friendly_ids, unfriendly_ids = ids.partition {|id| id.friendly_id?}
35
+ return if friendly_ids.empty?
36
+ records = friendly_records(friendly_ids, unfriendly_ids).each do |record|
37
+ record.friendly_id_status.name = ids
38
+ end
39
+ validate_expected_size!(ids, records)
39
40
  end
40
- validate_expected_size!(ids, records)
41
- end
42
41
 
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)
42
+ def raise_error(error)
43
+ raise(error) unless fc.scope
44
+ scope_message = friendly_id_scope || "expected, but none given"
45
+ message = "%s, scope: %s" % [error.message, scope_message]
46
+ raise ActiveRecord::RecordNotFound, message
57
47
  end
58
- end
59
48
 
60
- def find_one_using_cached_slug(id)
61
- record = where(friendly_id_config.cache_column => id).first
62
- if record
49
+ private
50
+
51
+ def assign_status
52
+ return unless @result
63
53
  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)
54
+ @result.friendly_id_status.name = name
55
+ @result.friendly_id_status.sequence = seq if fc.use_slugs?
56
+ @result
69
57
  end
70
- end
71
58
 
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
59
+ def find_one_without_slug
60
+ @result = where(fc.column => id).first
61
+ assign_status
62
+ end
77
63
 
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
64
+ def find_one_with_cached_slug
65
+ @result = where(fc.cache_column => id).first
66
+ assign_status or find_one_with_slug
89
67
  end
90
- use_slugs ? includes(:slugs).where(clause) : where(clause)
91
- end
92
68
 
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))
69
+ def find_one_with_slug
70
+ sluggable_id = sluggable_ids_for([id]).first
71
+ if sluggable_id
72
+ name, seq = id.to_s.parse_friendly_id
73
+ record = relation.send(:find_one_without_friendly, sluggable_id)
74
+ record.friendly_id_status.name = name
75
+ record.friendly_id_status.sequence = seq
76
+ record
77
+ else
78
+ relation.send(:find_one_without_friendly, id)
79
+ end
99
80
  end
100
- ids.inject(nil) {|clause, id| clause ? clause.or(conditions.call(id)) : conditions.call(id) }
101
- end
102
81
 
103
- def validate_expected_size!(ids, result)
104
- expected_size =
105
- if @limit_value && ids.size > @limit_value
106
- @limit_value
82
+ def friendly_records(friendly_ids, unfriendly_ids)
83
+ use_slugs_table = fc.use_slugs? && (friendly_id_scope || !fc.cache_column?)
84
+ return find_some_using_slug(friendly_ids, unfriendly_ids) if use_slugs_table
85
+ column = fc.cache_column || fc.column
86
+ friendly = arel_table[column].in(friendly_ids)
87
+ unfriendly = arel_table[relation.primary_key].in unfriendly_ids
88
+ if friendly_ids.present? && unfriendly_ids.present?
89
+ where(friendly.or(unfriendly))
107
90
  else
108
- ids.size
91
+ where(friendly)
92
+ end
93
+ end
94
+
95
+ def find_some_using_slug(friendly_ids, unfriendly_ids)
96
+ ids = [unfriendly_ids + sluggable_ids_for(friendly_ids)].flatten.uniq
97
+ where(arel_table[relation.primary_key].in(ids))
98
+ end
99
+
100
+ def sluggable_ids_for(ids)
101
+ return [] if ids.empty?
102
+ fragment = "(slugs.name = %s AND slugs.sequence = %d)"
103
+ conditions = ids.inject(nil) do |clause, id|
104
+ name, seq = id.parse_friendly_id
105
+ string = fragment % [connection.quote(name), seq]
106
+ clause ? clause + " OR #{string}" : string
109
107
  end
108
+ if fc.scope?
109
+ scope = connection.quote(friendly_id_scope)
110
+ conditions = "slugs.scope = %s AND (%s)" % [scope, conditions]
111
+ end
112
+ connection.select_values "SELECT sluggable_id FROM slugs WHERE (%s)" % conditions
113
+ end
110
114
 
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
115
+ def validate_expected_size!(ids, result)
116
+ expected_size =
117
+ if limit_value && ids.size > limit_value
118
+ limit_value
119
+ else
120
+ ids.size
121
+ end
122
+
123
+ # 11 ids with limit 3, offset 9 should give 2 results.
124
+ if offset_value && (ids.size - offset_value < expected_size)
125
+ expected_size = ids.size - offset_value
126
+ end
127
+
128
+ if result.size == expected_size
129
+ result
130
+ else
131
+ conditions = arel.send(:where_clauses).join(', ')
132
+ conditions = " [WHERE #{conditions}]" if conditions.present?
133
+ error = "Couldn't find all #{klass.name.pluralize} with IDs "
134
+ error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
135
+ raise ActiveRecord::RecordNotFound, error
136
+ end
114
137
  end
138
+ end
139
+
140
+ attr :friendly_id_scope
141
+
142
+ # This method overrides Active Record's default in order to allow the :scope option to
143
+ # be passed to finds.
144
+ def apply_finder_options(options)
145
+ @friendly_id_scope = options.delete(:scope)
146
+ @friendly_id_scope = @friendly_id_scope.to_param if @friendly_id_scope.respond_to?(:to_param)
147
+ super
148
+ end
115
149
 
116
- if result.size == expected_size
117
- result
118
- else
119
- conditions = arel.send(:where_clauses).join(', ')
120
- conditions = " [WHERE #{conditions}]" if conditions.present?
150
+ protected
121
151
 
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
152
+ def find_one(id)
153
+ begin
154
+ return super if !klass.uses_friendly_id? or id.unfriendly_id?
155
+ find = Find.new(self, id)
156
+ find.find_one or super
157
+ rescue ActiveRecord::RecordNotFound => error
158
+ find ? find.raise_error(error) : raise(error)
125
159
  end
126
160
  end
161
+
162
+ def find_some(ids)
163
+ return super unless klass.uses_friendly_id?
164
+ Find.new(self, ids).find_some or super
165
+ rescue ActiveRecord::RecordNotFound => error
166
+ find ? find.raise_error(error) : raise(error)
167
+ end
168
+
127
169
  end
128
170
  end
129
171
  end
@@ -101,10 +101,11 @@ module FriendlyId
101
101
  # This method was removed in ActiveRecord 3.0.
102
102
  if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
103
103
  def update_without_callbacks
104
- save :callbacks => false
104
+ attributes_with_values = arel_attributes_values(false, false, attribute_names)
105
+ return false if attributes_with_values.empty?
106
+ self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
105
107
  end
106
108
  end
107
-
108
109
  end
109
110
  end
110
- end
111
+ end
@@ -85,6 +85,14 @@ module FriendlyId
85
85
  yield self if block_given?
86
86
  end
87
87
 
88
+ def cache_column=(value)
89
+ @cache_column = value.to_s.strip.to_sym
90
+ if [:slug, :slugs, :""].include?(@cache_column)
91
+ raise ArgumentError, "FriendlyId cache column can not be named '#{value}'"
92
+ end
93
+ @cache_column
94
+ end
95
+
88
96
  # This should be overridden by adapters that implement caching.
89
97
  def cache_column?
90
98
  false
@@ -107,6 +115,13 @@ module FriendlyId
107
115
  @scope = scope
108
116
  end
109
117
 
118
+ def sequence_separator=(string)
119
+ if string == "-" || string =~ /\s/
120
+ raise ArgumentError, "FriendlyId sequence_separator can not be '#{string}'"
121
+ end
122
+ @sequence_separator = string
123
+ end
124
+
110
125
  # This will be set if FriendlyId's scope feature is used in any model. It is here
111
126
  # to provide a way to avoid invoking costly scope lookup methods when the scoped
112
127
  # slug feature is not being used by any models.
@@ -121,7 +136,7 @@ module FriendlyId
121
136
  end
122
137
 
123
138
  %w[approximate_ascii scope strip_non_ascii use_slug].each do |method|
124
- class_eval(<<-EOM)
139
+ class_eval(<<-EOM, __FILE__, __LINE__ +1)
125
140
  def #{method}?
126
141
  !! #{method}
127
142
  end
@@ -130,6 +145,13 @@ module FriendlyId
130
145
 
131
146
  alias :use_slugs? :use_slug?
132
147
 
148
+ def babosa_options
149
+ {
150
+ :to_ascii => strip_non_ascii?,
151
+ :transliterate => approximate_ascii?,
152
+ :transliterations => ascii_approximation_options,
153
+ :max_length => max_length
154
+ }
155
+ end
133
156
  end
134
-
135
157
  end
@@ -1,14 +1,12 @@
1
1
  # encoding: utf-8
2
2
  module FriendlyId
3
3
 
4
- class SlugString < Babosa::SlugString
4
+ class SlugString < Babosa::Identifier
5
5
  # Normalize the string for a given {FriendlyId::Configuration}.
6
6
  # @param config [FriendlyId::Configuration]
7
7
  # @return String
8
8
  def normalize_for!(config)
9
- approximate_ascii!(config.ascii_approximation_options) if config.approximate_ascii?
10
- to_ascii! if config.strip_non_ascii?
11
- normalize!
9
+ normalize!(config.babosa_options)
12
10
  end
13
11
 
14
12
  # Validate that the slug string is not blank or reserved, and truncate
@@ -94,12 +94,6 @@ module FriendlyId
94
94
  assert_equal instance, klass.send(find_method, "206")
95
95
  end
96
96
 
97
- test "failing finds with unfriendly_id should raise errors normally" do
98
- assert_raise ActiveRecord::RecordNotFound do
99
- klass.send(find_method, 0)
100
- end
101
- end
102
-
103
97
  test "creation should raise an error if the friendly_id text is reserved" do
104
98
  assert_validation_error do
105
99
  klass.send(create_method, :name => "new")
@@ -2,7 +2,7 @@ module FriendlyId
2
2
  module Version
3
3
  MAJOR = 3
4
4
  MINOR = 1
5
- TINY = 3
5
+ TINY = 4
6
6
  BUILD = nil
7
7
  STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
8
  end
@@ -35,6 +35,11 @@ CreateSupportModels.up
35
35
  # A model that uses the automagically configured "cached_slug" column
36
36
  class District < ActiveRecord::Base
37
37
  has_friendly_id :name, :use_slug => true
38
+ before_save :say_hello
39
+
40
+ def say_hello
41
+ @said_hello = true
42
+ end
38
43
  end
39
44
 
40
45
  # A model with optimistic locking enabled
@@ -64,6 +64,11 @@ module FriendlyId
64
64
  instance.friendly_id(true)
65
65
  end
66
66
 
67
+ test "should not fire callbacks when updating slug cache" do
68
+ instance.expects(:say_hello).once
69
+ instance.update_attributes(:name => "new name")
70
+ assert_equal instance.slug.to_friendly_id, cached_slug
71
+ end
67
72
  end
68
73
  end
69
74
  end
@@ -114,6 +114,12 @@ module FriendlyId
114
114
  end
115
115
  end
116
116
 
117
+ test "failing finds with unfriendly_id should raise errors normally" do
118
+ assert_raise ActiveRecord::RecordNotFound do
119
+ klass.send(find_method, 0)
120
+ end
121
+ end
122
+
117
123
  test "instances found by a single id should not be read-only" do
118
124
  i = klass.find(instance.friendly_id)
119
125
  assert !i.readonly?, "expected instance not to be readonly"
@@ -5,20 +5,15 @@ module FriendlyId
5
5
 
6
6
  class ScopedModelTest < ::Test::Unit::TestCase
7
7
 
8
- include FriendlyId::Test::Generic
9
- include FriendlyId::Test::Slugged
10
- include FriendlyId::Test::ActiveRecordAdapter::Slugged
11
- include FriendlyId::Test::ActiveRecordAdapter::Core
12
-
13
8
  def setup
14
- @user = User.create!(:name => "john")
15
- @house = House.create!(:name => "123 Main", :user => @user)
16
- @usa = Country.create!(:name => "USA")
17
- @canada = Country.create!(:name => "Canada")
18
- @resident = Resident.create!(:name => "John Smith", :country => @usa)
9
+ @user = User.create!(:name => "john")
10
+ @house = House.create!(:name => "123 Main", :user => @user)
11
+ @usa = Country.create!(:name => "USA")
12
+ @canada = Country.create!(:name => "Canada")
13
+ @resident = Resident.create!(:name => "John Smith", :country => @usa)
19
14
  @resident2 = Resident.create!(:name => "John Smith", :country => @canada)
20
- @owner = Company.create!(:name => "Acme Events")
21
- @site = Site.create!(:name => "Downtown Venue", :owner => @owner)
15
+ @owner = Company.create!(:name => "Acme Events")
16
+ @site = Site.create!(:name => "Downtown Venue", :owner => @owner)
22
17
  end
23
18
 
24
19
  def teardown
@@ -77,13 +72,21 @@ module FriendlyId
77
72
  end
78
73
 
79
74
  test "should find a single scoped record with a scope as a string" do
80
- assert Resident.find(@resident.friendly_id, :scope => @resident.country)
75
+ assert Resident.find(@resident.friendly_id, :scope => @resident.country.to_param)
81
76
  end
82
77
 
83
78
  test "should find a single scoped record with a scope" do
84
79
  assert Resident.find(@resident.friendly_id, :scope => @resident.country)
85
80
  end
86
81
 
82
+ test "should find a multiple scoped records with a scope" do
83
+ r1 = Resident.create!(:name => "John Smith", :country => @usa)
84
+ r2 = Resident.create!(:name => "Jane Smith", :country => @usa)
85
+ result = Resident.find([r1, r2].map(&:friendly_id), :scope => @resident.country)
86
+ assert_equal 2, result.size
87
+ end
88
+
89
+
87
90
  test "should raise an error when finding a single scoped record with no scope" do
88
91
  assert_raises ActiveRecord::RecordNotFound do
89
92
  Resident.find(@resident.friendly_id)
@@ -123,4 +126,4 @@ module FriendlyId
123
126
  end
124
127
  end
125
128
  end
126
- end
129
+ end
@@ -1,7 +1,52 @@
1
+ # encoding: utf-8
1
2
  require File.expand_path('../test_helper', __FILE__)
2
3
 
3
4
  module FriendlyId
4
5
  module Test
6
+
7
+ class ConfigurationTest < ::Test::Unit::TestCase
8
+ test "should validate sequence separator name" do
9
+ ["-", " ", "\n", "\t"].each do |string|
10
+ assert_raise ArgumentError do
11
+ Configuration.new(NilClass, :hello, :sequence_separator => string)
12
+ end
13
+ end
14
+ end
15
+
16
+ test "should validate cached slug name" do
17
+ ["slug", "slugs", " "].each do |string|
18
+ assert_raise ArgumentError do
19
+ Configuration.new(NilClass, :hello, :cache_column => string)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ class SlugStringTest < ::Test::Unit::TestCase
26
+ test "should not transliterate by default" do
27
+ s = SlugString.new("über")
28
+ assert_equal "über", s.normalize_for!(Configuration.new(nil, :name))
29
+ end
30
+
31
+ test "should transliterate if specified" do
32
+ s = SlugString.new("über")
33
+ options = {:approximate_ascii => true}
34
+ assert_equal "uber", s.normalize_for!(Configuration.new(nil, :name, options))
35
+ end
36
+
37
+ test "should strip non-ascii if specified" do
38
+ s = SlugString.new("über")
39
+ options = {:strip_non_ascii => true}
40
+ assert_equal "ber", s.normalize_for!(Configuration.new(nil, :name, options))
41
+ end
42
+
43
+ test "should use transliterations if given" do
44
+ s = SlugString.new("über")
45
+ options = {:approximate_ascii => true, :ascii_approximation_options => :german}
46
+ assert_equal "ueber", s.normalize_for!(Configuration.new(nil, :name, options))
47
+ end
48
+ end
49
+
5
50
  class FriendlyIdTest < ::Test::Unit::TestCase
6
51
  test "should parse a friendly_id name and sequence" do
7
52
  assert_equal ["test", 2], "test--2".parse_friendly_id
@@ -48,4 +93,4 @@ module FriendlyId
48
93
  end
49
94
  end
50
95
  end
51
- end
96
+ end
data/test/test_helper.rb CHANGED
@@ -5,11 +5,9 @@ $:.uniq!
5
5
  $KCODE = "UTF8" if RUBY_VERSION < "1.9"
6
6
  $VERBOSE = false
7
7
  require "rubygems"
8
- require "bundler"
9
- Bundler.setup
8
+ require "bundler/setup"
10
9
  require "test/unit"
11
10
  require "mocha"
12
11
  require "active_support"
13
- # require "ruby-debug"
14
12
  require "friendly_id"
15
13
  require "friendly_id/test"
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 3
7
7
  - 1
8
- - 3
9
- version: 3.1.3
8
+ - 4
9
+ version: 3.1.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Norman Clarke
@@ -16,23 +16,23 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-08-11 00:00:00 -03:00
19
+ date: 2010-09-01 00:00:00 -03:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
23
23
  name: babosa
24
- prerelease: false
25
24
  requirement: &id001 !ruby/object:Gem::Requirement
26
25
  none: false
27
26
  requirements:
28
- - - ">="
27
+ - - ~>
29
28
  - !ruby/object:Gem::Version
30
29
  segments:
31
30
  - 0
32
- - 1
31
+ - 2
33
32
  - 0
34
- version: 0.1.0
33
+ version: 0.2.0
35
34
  type: :runtime
35
+ prerelease: false
36
36
  version_requirements: *id001
37
37
  description: " FriendlyId is the \"Swiss Army bulldozer\" of slugging and permalink plugins\n for Ruby on Rails. It allows you to create pretty URL\xE2\x80\x99s and work with\n human-friendly strings as if they were numeric ids for ActiveRecord models.\n"
38
38
  email:
@@ -70,7 +70,7 @@ files:
70
70
  - Contributors.md
71
71
  - Guide.md
72
72
  - README.md
73
- - LICENSE
73
+ - MIT-LICENSE
74
74
  - Rakefile
75
75
  - rails/init.rb
76
76
  - generators/friendly_id/friendly_id_generator.rb
@@ -92,7 +92,6 @@ files:
92
92
  - test/active_record_adapter/support/database.mysql.yml
93
93
  - test/active_record_adapter/support/database.postgres.yml
94
94
  - test/active_record_adapter/support/database.sqlite3.yml
95
- - test/active_record_adapter/support/database.yml
96
95
  - test/active_record_adapter/support/models.rb
97
96
  - test/active_record_adapter/tasks_test.rb
98
97
  - test/friendly_id_test.rb
@@ -117,6 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
116
  requirements:
118
117
  - - ">="
119
118
  - !ruby/object:Gem::Version
119
+ hash: 4295902025361844549
120
120
  segments:
121
121
  - 0
122
122
  version: "0"
@@ -125,6 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
125
  requirements:
126
126
  - - ">="
127
127
  - !ruby/object:Gem::Version
128
+ hash: 4295902025361844549
128
129
  segments:
129
130
  - 0
130
131
  version: "0"
@@ -1,6 +0,0 @@
1
- adapter: postgresql
2
- host: localhost
3
- port: 5432
4
- username: postgres
5
- database: friendly_id_test
6
- encoding: utf8