friendly_id 2.2.7 → 2.3.0

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