friendly_id 2.2.7 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/Changelog.md +225 -0
  2. data/Contributors.md +28 -0
  3. data/Guide.md +509 -0
  4. data/LICENSE +1 -1
  5. data/README.md +76 -0
  6. data/Rakefile +48 -15
  7. data/extras/bench.rb +59 -0
  8. data/extras/extras.rb +31 -0
  9. data/extras/prof.rb +14 -0
  10. data/extras/template-gem.rb +1 -1
  11. data/extras/template-plugin.rb +1 -1
  12. data/generators/friendly_id/friendly_id_generator.rb +1 -1
  13. data/generators/friendly_id/templates/create_slugs.rb +2 -2
  14. data/lib/friendly_id.rb +54 -63
  15. data/lib/friendly_id/active_record2.rb +47 -0
  16. data/lib/friendly_id/active_record2/configuration.rb +66 -0
  17. data/lib/friendly_id/active_record2/finders.rb +140 -0
  18. data/lib/friendly_id/active_record2/simple_model.rb +162 -0
  19. data/lib/friendly_id/active_record2/slug.rb +111 -0
  20. data/lib/friendly_id/active_record2/slugged_model.rb +317 -0
  21. data/lib/friendly_id/active_record2/tasks.rb +66 -0
  22. data/lib/friendly_id/active_record2/tasks/friendly_id.rake +19 -0
  23. data/lib/friendly_id/configuration.rb +132 -0
  24. data/lib/friendly_id/finders.rb +106 -0
  25. data/lib/friendly_id/slug_string.rb +292 -0
  26. data/lib/friendly_id/slugged.rb +91 -0
  27. data/lib/friendly_id/status.rb +35 -0
  28. data/lib/friendly_id/test.rb +168 -0
  29. data/lib/friendly_id/version.rb +5 -5
  30. data/rails/init.rb +2 -0
  31. data/test/active_record2/basic_slugged_model_test.rb +14 -0
  32. data/test/active_record2/cached_slug_test.rb +61 -0
  33. data/test/active_record2/core.rb +93 -0
  34. data/test/active_record2/custom_normalizer_test.rb +20 -0
  35. data/test/active_record2/custom_table_name_test.rb +22 -0
  36. data/test/active_record2/scoped_model_test.rb +111 -0
  37. data/test/active_record2/simple_test.rb +59 -0
  38. data/test/active_record2/slug_test.rb +34 -0
  39. data/test/active_record2/slugged.rb +30 -0
  40. data/test/active_record2/slugged_status_test.rb +61 -0
  41. data/test/active_record2/sti_test.rb +22 -0
  42. data/test/active_record2/support/database.mysql.yml +4 -0
  43. data/test/{support/database.yml.postgres → active_record2/support/database.postgres.yml} +0 -0
  44. data/test/{support/database.yml.sqlite3 → active_record2/support/database.sqlite3.yml} +0 -0
  45. data/test/{support → active_record2/support}/models.rb +28 -0
  46. data/test/active_record2/tasks_test.rb +82 -0
  47. data/test/active_record2/test_helper.rb +107 -0
  48. data/test/friendly_id_test.rb +23 -0
  49. data/test/slug_string_test.rb +74 -0
  50. data/test/test_helper.rb +7 -102
  51. metadata +64 -56
  52. data/History.txt +0 -194
  53. data/README.rdoc +0 -385
  54. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +0 -12
  55. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +0 -19
  56. data/init.rb +0 -1
  57. data/lib/friendly_id/helpers.rb +0 -12
  58. data/lib/friendly_id/non_sluggable_class_methods.rb +0 -34
  59. data/lib/friendly_id/non_sluggable_instance_methods.rb +0 -45
  60. data/lib/friendly_id/slug.rb +0 -98
  61. data/lib/friendly_id/sluggable_class_methods.rb +0 -110
  62. data/lib/friendly_id/sluggable_instance_methods.rb +0 -161
  63. data/lib/friendly_id/tasks.rb +0 -56
  64. data/lib/tasks/friendly_id.rake +0 -25
  65. data/lib/tasks/friendly_id.rb +0 -1
  66. data/test/cached_slug_test.rb +0 -109
  67. data/test/custom_slug_normalizer_test.rb +0 -36
  68. data/test/non_slugged_test.rb +0 -99
  69. data/test/scoped_model_test.rb +0 -64
  70. data/test/slug_test.rb +0 -105
  71. data/test/slugged_model_test.rb +0 -348
  72. data/test/sti_test.rb +0 -49
  73. data/test/tasks_test.rb +0 -105
@@ -0,0 +1,35 @@
1
+ module FriendlyId
2
+
3
+ # FriendlyId::Status presents information about the status of the
4
+ # id that was used to find the model. This class can be useful for figuring
5
+ # out when to redirect to a new URL.
6
+ class Status
7
+
8
+ # The id or name used as the finder argument
9
+ attr_accessor :name
10
+
11
+ # The found result, if any
12
+ attr_accessor :record
13
+
14
+ def initialize(options={})
15
+ options.each {|key, value| self.send("#{key}=".to_sym, value)}
16
+ end
17
+
18
+ # Did the find operation use a friendly id?
19
+ def friendly?
20
+ !! name
21
+ end
22
+
23
+ # Did the find operation use a numeric id?
24
+ def numeric?
25
+ !friendly?
26
+ end
27
+
28
+ # Did the find operation use the best available id?
29
+ def best?
30
+ record.friendly_id ? friendly? : true
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,168 @@
1
+ Module.send :include, Module.new {
2
+ def test(name, &block)
3
+ define_method("test_#{name.gsub(/[^a-z0-9]/i, "_")}".to_sym, &block)
4
+ end
5
+ alias :should :test
6
+ }
7
+
8
+ module FriendlyId
9
+ module Test
10
+
11
+ # Tests for any model that implements FriendlyId. Any test that tests model
12
+ # features should include this module.
13
+ module Generic
14
+
15
+ def setup
16
+ klass.send delete_all_method
17
+ end
18
+
19
+ def teardown
20
+ klass.send delete_all_method
21
+ # other_class.delete_all
22
+ end
23
+
24
+ def instance
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def klass
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def other_class
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def find_method
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def create_method
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def validation_exceptions
45
+ return RuntimeError
46
+ end
47
+
48
+ test "models should have a friendly id config" do
49
+ assert_not_nil klass.friendly_id_config
50
+ end
51
+
52
+ test "instances should have a friendly id" do
53
+ assert_not_nil instance.friendly_id
54
+ end
55
+
56
+ test "instances should have a friendly id status" do
57
+ assert_not_nil instance.friendly_id_status
58
+ end
59
+
60
+ test "instances should be findable by their friendly id" do
61
+ assert_equal instance, klass.send(find_method, instance.friendly_id)
62
+ end
63
+
64
+ test "instances should be findable by their numeric id as an integer" do
65
+ assert_equal instance, klass.send(find_method, instance.id.to_i)
66
+ end
67
+
68
+ test "instances should be findable by their numeric id as a string" do
69
+ assert_equal instance, klass.send(find_method, instance.id.to_s)
70
+ end
71
+
72
+ test "creation should raise an error if the friendly_id text is reserved" do
73
+ assert_raise(*[validation_exceptions].flatten) do
74
+ klass.send(create_method, :name => "new")
75
+ end
76
+ end
77
+
78
+ test "creation should raise an error if the friendly_id text is an empty string" do
79
+ assert_raise(*[validation_exceptions].flatten) do
80
+ klass.send(create_method, :name => "")
81
+ end
82
+ end
83
+
84
+ test "creation should raise an error if the friendly_id text is a blank string" do
85
+ assert_raise(*[validation_exceptions].flatten) do
86
+ klass.send(create_method, :name => " ")
87
+ end
88
+ end
89
+
90
+ test "creation should raise an error if the friendly_id text is nil" do
91
+ assert_raise(*[validation_exceptions].flatten) do
92
+ klass.send(create_method, :name => nil)
93
+ end
94
+ end
95
+
96
+ test "should allow the same friendly_id across models" do
97
+ other_instance = other_class.send(create_method, :name => instance.name)
98
+ assert_equal other_instance.friendly_id, instance.friendly_id
99
+ end
100
+
101
+ end
102
+
103
+ # Tests for any model that implements slugs.
104
+ module Slugged
105
+
106
+ test "should have a slug" do
107
+ assert_not_nil instance.slug
108
+ end
109
+
110
+ test "should not make a new slug unless the friendly_id method value has changed" do
111
+ instance.note = instance.note.to_s << " updated"
112
+ instance.send save_method
113
+ assert_equal 1, instance.slugs.size
114
+ end
115
+
116
+ test "should make a new slug if the friendly_id method value has changed" do
117
+ instance.name = "Changed title"
118
+ instance.send save_method
119
+ assert_equal 2, instance.slugs.size
120
+ end
121
+
122
+ test "should be able to reuse an old friendly_id without incrementing the sequence" do
123
+ old_title = instance.name
124
+ old_friendly_id = instance.friendly_id
125
+ instance.name = "A changed title"
126
+ instance.send save_method
127
+ instance.name = old_title
128
+ instance.send save_method
129
+ assert_equal old_friendly_id, instance.friendly_id
130
+ end
131
+
132
+ test "should increment the slug sequence for duplicate friendly ids" do
133
+ instance2 = klass.send(create_method, :name => instance.name)
134
+ assert_match(/2\z/, instance2.friendly_id)
135
+ end
136
+
137
+ test "should find instance with a sequenced friendly_id" do
138
+ instance2 = klass.send(create_method, :name => instance.name)
139
+ assert_equal instance2, klass.send(find_method, instance2.friendly_id)
140
+ end
141
+
142
+ end
143
+
144
+ # Tests for models to ensure that they properly implement using the
145
+ # +normalize_friendly_id+ method to allow developers to hook into the
146
+ # slug string generation.
147
+ module CustomNormalizer
148
+
149
+ test "should invoke the custom normalizer" do
150
+ assert_equal "JOE SCHMOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
151
+ end
152
+
153
+ test "should respect the max_length option" do
154
+ klass.friendly_id_config.stubs(:max_length).returns(3)
155
+ assert_equal "JOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
156
+ end
157
+
158
+ test "should raise an error if the friendly_id text is reserved" do
159
+ klass.friendly_id_config.stubs(:reserved_words).returns(["JOE"])
160
+ assert_raise(*[validation_exceptions].flatten) do
161
+ klass.send(create_method, :name => "Joe")
162
+ end
163
+
164
+ end
165
+
166
+ end
167
+ end
168
+ end
@@ -1,8 +1,8 @@
1
- module FriendlyId #:nodoc:
2
- module Version #:nodoc:
1
+ module FriendlyId
2
+ module Version
3
3
  MAJOR = 2
4
- MINOR = 2
5
- TINY = 7
4
+ MINOR = 3
5
+ TINY = 0
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
- end
8
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "friendly_id"
2
+ require "friendly_id/active_record2"
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+ module Test
5
+ module ActiveRecord2
6
+ class BasicSluggedModelTest < ::Test::Unit::TestCase
7
+ include FriendlyId::Test::Generic
8
+ include FriendlyId::Test::Slugged
9
+ include FriendlyId::Test::ActiveRecord2::Slugged
10
+ include FriendlyId::Test::ActiveRecord2::Core
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+ module Test
5
+ module ActiveRecord2
6
+
7
+ class CachedSlugTest < ::Test::Unit::TestCase
8
+
9
+ include FriendlyId::Test::Generic
10
+ include FriendlyId::Test::Slugged
11
+ include FriendlyId::Test::ActiveRecord2::Slugged
12
+ include FriendlyId::Test::ActiveRecord2::Core
13
+
14
+ def klass
15
+ District
16
+ end
17
+
18
+ def other_class
19
+ Post
20
+ end
21
+
22
+ def cached_slug
23
+ instance.send(cache_column)
24
+ end
25
+
26
+ def cache_column
27
+ klass.friendly_id_config.cache_column
28
+ end
29
+
30
+ test "should have a cached_slug" do
31
+ assert_equal cached_slug, instance.slug.to_friendly_id
32
+ end
33
+
34
+ test "should protect the cached slug value" do
35
+ old_value = cached_slug
36
+ instance.update_attributes(cache_column => "Madrid")
37
+ instance.reload
38
+ assert_equal old_value, cached_slug
39
+ end
40
+
41
+ test "should update the cached slug when updating the slug" do
42
+ instance.update_attributes(:name => "new name")
43
+ assert_equal instance.slug.to_friendly_id, cached_slug
44
+ end
45
+
46
+ test "should not update the cached slug column if it has not changed" do
47
+ instance.note = "a note"
48
+ instance.expects("#{cache_column}=".to_sym).never
49
+ instance.save!
50
+ end
51
+
52
+ test "should cache the incremented sequence for duplicate slug names" do
53
+ instance_2 = klass.create!(:name => instance.name)
54
+ assert_match(/2\z/, instance_2.send(cache_column))
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,93 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+
5
+ module Test
6
+
7
+ module ActiveRecord2
8
+
9
+ module Core
10
+
11
+ def teardown
12
+ klass.delete_all
13
+ other_class.delete_all
14
+ Slug.delete_all
15
+ end
16
+
17
+ def find_method
18
+ :find
19
+ end
20
+
21
+ def create_method
22
+ :create!
23
+ end
24
+
25
+ def delete_all_method
26
+ :delete_all
27
+ end
28
+
29
+ def save_method
30
+ :save!
31
+ end
32
+
33
+ def validation_exceptions
34
+ [ActiveRecord::RecordInvalid, FriendlyId::ReservedError, FriendlyId::BlankError]
35
+ end
36
+
37
+ test "should return their friendly_id for #to_param" do
38
+ assert_match(instance.friendly_id, instance.to_param)
39
+ end
40
+
41
+ test "instances should be findable by their own instance" do
42
+ assert_equal instance, klass.find(instance)
43
+ end
44
+
45
+ test "instances should be findable by an array of friendly_ids" do
46
+ second = klass.create!(:name => "second_instance")
47
+ assert_equal 2, klass.find([instance.friendly_id, second.friendly_id]).size
48
+ end
49
+
50
+ test "instances should be findable by an array of numeric ids" do
51
+ second = klass.create!(:name => "second_instance")
52
+ assert_equal 2, klass.find([instance.id.to_i, second.id.to_i]).size
53
+ end
54
+
55
+ test "instances should be findable by an array of numeric ids as strings" do
56
+ second = klass.create!(:name => "second_instance")
57
+ assert_equal 2, klass.find([instance.id.to_s, second.id.to_s]).size
58
+ end
59
+
60
+ test "instances should be findable by an array of instances" do
61
+ second = klass.create!(:name => "second_instance")
62
+ assert_equal 2, klass.find([instance, second]).size
63
+ end
64
+
65
+ test "instances should be findable by an array of mixed types" do
66
+ second = klass.create!(:name => "second_instance")
67
+ assert_equal 2, klass.find([instance.friendly_id, second]).size
68
+ end
69
+
70
+ test "models should raise an error when not all records are found" do
71
+ assert_raises(ActiveRecord::RecordNotFound) do
72
+ klass.find([instance.friendly_id, 'bad-friendly-id'])
73
+ end
74
+ end
75
+
76
+ test "models should respect finder conditions" do
77
+ assert_raise ActiveRecord::RecordNotFound do
78
+ klass.find(instance.friendly_id, :conditions => "1 = 2")
79
+ end
80
+ end
81
+
82
+ # This emulates a fairly common issue where id's generated by fixtures are very high.
83
+ test "should continue to admit very large ids" do
84
+ klass.connection.execute("INSERT INTO #{klass.table_name} (id, name) VALUES (2047483647, 'An instance')")
85
+ assert_nothing_raised do
86
+ klass.base_class.find(2047483647)
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+ module Test
5
+ module ActiveRecord2
6
+
7
+ class CustomNormalizerTest < ::Test::Unit::TestCase
8
+
9
+ include FriendlyId::Test::ActiveRecord2::Core
10
+ include FriendlyId::Test::ActiveRecord2::Slugged
11
+ include FriendlyId::Test::CustomNormalizer
12
+
13
+ def klass
14
+ Person
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+ module Test
5
+ module ActiveRecord2
6
+
7
+ class CustomTableNameTest < ::Test::Unit::TestCase
8
+
9
+ include FriendlyId::Test::Generic
10
+ include FriendlyId::Test::Slugged
11
+ include FriendlyId::Test::ActiveRecord2::Slugged
12
+ include FriendlyId::Test::ActiveRecord2::Core
13
+
14
+ def klass
15
+ Place
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,111 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module FriendlyId
4
+ module Test
5
+
6
+ class ScopedModelTest < ::Test::Unit::TestCase
7
+
8
+ include FriendlyId::Test::Generic
9
+ include FriendlyId::Test::Slugged
10
+ include FriendlyId::Test::ActiveRecord2::Slugged
11
+ include FriendlyId::Test::ActiveRecord2::Core
12
+
13
+ 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)
19
+ @resident2 = Resident.create!(:name => "John Smith", :country => @canada)
20
+ end
21
+
22
+ def teardown
23
+ Resident.delete_all
24
+ Country.delete_all
25
+ User.delete_all
26
+ House.delete_all
27
+ Slug.delete_all
28
+ end
29
+
30
+ test "a slugged model should auto-detect that it is being used as a parent scope" do
31
+ assert_equal [Resident], Country.friendly_id_config.child_scopes
32
+ end
33
+
34
+ test "a slugged model should update its child model's scopes when its friendly_id changes" do
35
+ @usa.update_attributes(:name => "United States")
36
+ assert_equal "united-states", @usa.to_param
37
+ assert_equal "united-states", @resident.slugs(true).first.scope
38
+ end
39
+
40
+ test "a non-slugged model should auto-detect that it is being used as a parent scope" do
41
+ assert_equal [House], User.friendly_id_config.child_scopes
42
+ end
43
+
44
+ test "should update the slug when the scope changes" do
45
+ @resident.update_attributes! :country => Country.create!(:name => "Argentina")
46
+ assert_equal "argentina", @resident.slugs(true).first.scope
47
+ end
48
+
49
+ test "updating only the scope should not append sequence to friendly_id" do
50
+ old_friendly_id = @resident.friendly_id
51
+ @resident.update_attributes! :country => Country.create!(:name => "Argentina")
52
+ assert_equal old_friendly_id, @resident.friendly_id
53
+ end
54
+
55
+ test "updating the scope should increment sequence to avoid conflicts" do
56
+ old_friendly_id = @resident.friendly_id
57
+ @resident.update_attributes! :country => @canada
58
+ assert_equal "#{old_friendly_id}--2", @resident.friendly_id
59
+ assert_equal "canada", @resident.slugs(true).first.scope
60
+ end
61
+
62
+ test "a non-slugged model should update its child model's scopes when its friendly_id changes" do
63
+ @user.update_attributes(:name => "jack")
64
+ assert_equal "jack", @user.to_param
65
+ assert_equal "jack", @house.slugs(true).first.scope
66
+ end
67
+
68
+ test "should should not show the scope in the friendly_id" do
69
+ assert_equal "john-smith", @resident.friendly_id
70
+ assert_equal "john-smith", @resident2.friendly_id
71
+ end
72
+
73
+ test "should find all scoped records without scope" do
74
+ assert_equal 2, Resident.find(:all, @resident.friendly_id).size
75
+ end
76
+
77
+ test "should find a single scoped record with a scope as a string" do
78
+ assert Resident.find(@resident.friendly_id, :scope => @resident.country)
79
+ end
80
+
81
+ test "should find a single scoped record with a scope" do
82
+ assert Resident.find(@resident.friendly_id, :scope => @resident.country)
83
+ end
84
+
85
+ test "should raise an error when finding a single scoped record with no scope" do
86
+ assert_raises ActiveRecord::RecordNotFound do
87
+ Resident.find(@resident.friendly_id)
88
+ end
89
+ end
90
+
91
+ test "should append scope error info when missing scope causes a find to fail" do
92
+ begin
93
+ Resident.find(@resident.friendly_id)
94
+ fail "The find should not have succeeded"
95
+ rescue ActiveRecord::RecordNotFound => e
96
+ assert_match(/scope: expected/, e.message)
97
+ end
98
+ end
99
+
100
+ test "should append scope error info when the scope value causes a find to fail" do
101
+ begin
102
+ Resident.find(@resident.friendly_id, :scope => "badscope")
103
+ fail "The find should not have succeeded"
104
+ rescue ActiveRecord::RecordNotFound => e
105
+ assert_match(/scope: badscope/, e.message)
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+ end