nwp-friendly_id 2.1.3

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 (40) hide show
  1. data/History.txt +133 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest.txt +39 -0
  4. data/README.rdoc +343 -0
  5. data/Rakefile +49 -0
  6. data/config/website.yml +2 -0
  7. data/friendly_id.gemspec +45 -0
  8. data/generators/friendly_id/friendly_id_generator.rb +12 -0
  9. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  10. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
  11. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
  12. data/init.rb +3 -0
  13. data/lib/friendly_id.rb +101 -0
  14. data/lib/friendly_id/helpers.rb +15 -0
  15. data/lib/friendly_id/non_sluggable_class_methods.rb +42 -0
  16. data/lib/friendly_id/non_sluggable_instance_methods.rb +43 -0
  17. data/lib/friendly_id/slug.rb +102 -0
  18. data/lib/friendly_id/sluggable_class_methods.rb +116 -0
  19. data/lib/friendly_id/sluggable_instance_methods.rb +115 -0
  20. data/lib/friendly_id/version.rb +10 -0
  21. data/lib/tasks/friendly_id.rake +50 -0
  22. data/lib/tasks/friendly_id.rb +1 -0
  23. data/test/contest.rb +94 -0
  24. data/test/custom_slug_normalizer_test.rb +35 -0
  25. data/test/models/book.rb +2 -0
  26. data/test/models/country.rb +4 -0
  27. data/test/models/event.rb +3 -0
  28. data/test/models/novel.rb +3 -0
  29. data/test/models/person.rb +6 -0
  30. data/test/models/post.rb +6 -0
  31. data/test/models/thing.rb +6 -0
  32. data/test/models/user.rb +3 -0
  33. data/test/non_slugged_test.rb +98 -0
  34. data/test/schema.rb +55 -0
  35. data/test/scoped_model_test.rb +53 -0
  36. data/test/slug_test.rb +106 -0
  37. data/test/slugged_model_test.rb +284 -0
  38. data/test/sti_test.rb +48 -0
  39. data/test/test_helper.rb +30 -0
  40. metadata +154 -0
@@ -0,0 +1,3 @@
1
+ class Novel < Book
2
+ has_friendly_id :title, :use_slug => true
3
+ end
@@ -0,0 +1,6 @@
1
+ class Person < ActiveRecord::Base
2
+
3
+ belongs_to :country
4
+ has_friendly_id :name, :use_slug => true, :scope => :country
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ class Post < ActiveRecord::Base
2
+ has_friendly_id :title, :use_slug => true
3
+
4
+ named_scope :published, :conditions => { :published => true }
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'digest/sha1'
2
+ class Thing < ActiveRecord::Base
3
+ has_friendly_id :name, :use_slug => true do |text|
4
+ Digest::SHA1::hexdigest(text)
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_friendly_id :login
3
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class NonSluggedTest < Test::Unit::TestCase
6
+
7
+ context "A non-slugged model with default FriendlyId options" do
8
+
9
+ setup do
10
+ User.delete_all
11
+ @user = User.create!(:login => "joe", :email => "joe@example.org")
12
+ end
13
+
14
+ should "have friendly_id options" do
15
+ assert_not_nil User.friendly_id_options
16
+ end
17
+
18
+ should "not have a slug" do
19
+ assert !@user.respond_to?(:slug)
20
+ end
21
+
22
+ should "be findable by its friendly_id" do
23
+ assert User.find(@user.friendly_id)
24
+ end
25
+
26
+ should "be findable by its regular id" do
27
+ assert User.find(@user.id)
28
+ end
29
+
30
+ should "respect finder conditions" do
31
+ assert_raises ActiveRecord::RecordNotFound do
32
+ User.find(@user.friendly_id, :conditions => "1 = 2")
33
+ end
34
+ end
35
+
36
+ should "indicate if it was found by its friendly id" do
37
+ @user = User.find(@user.friendly_id)
38
+ assert @user.found_using_friendly_id?
39
+ end
40
+
41
+ should "indicate if it was found by its numeric id" do
42
+ @user = User.find(@user.id)
43
+ assert @user.found_using_numeric_id?
44
+ end
45
+
46
+ should "indicate if it has a better id" do
47
+ @user = User.find(@user.id)
48
+ assert @user.has_better_id?
49
+ end
50
+
51
+ should "not validate if the friendly_id text is reserved" do
52
+ @user = User.new(:login => "new", :email => "test@example.org")
53
+ assert !@user.valid?
54
+ end
55
+
56
+ should "have always string for a friendly_id" do
57
+ assert_equal String, @user.to_param.class
58
+ end
59
+
60
+ should "return its id if the friendly_id is null" do
61
+ @user.login = nil
62
+ assert_equal @user.id.to_s, @user.to_param
63
+ end
64
+
65
+
66
+ context "when using an array as the find argument" do
67
+
68
+ setup do
69
+ @user2 = User.create(:login => "jane", :email => "jane@example.org")
70
+ end
71
+
72
+ should "return results" do
73
+ assert_equal 2, User.find([@user.friendly_id, @user2.friendly_id]).size
74
+ end
75
+
76
+ should "not allow mixed friendly and non-friendly ids for the same record" do
77
+ assert_raises ActiveRecord::RecordNotFound do
78
+ User.find([@user.id, @user.friendly_id]).size
79
+ end
80
+ end
81
+
82
+ should "raise an error when all records are not found" do
83
+ assert_raises ActiveRecord::RecordNotFound do
84
+ User.find(['bad', 'bad2'])
85
+ end
86
+ end
87
+
88
+ should "indicate if the results were found using a friendly_id" do
89
+ @users = User.find([@user.id, @user2.friendly_id], :order => "login ASC")
90
+ assert @users[0].found_using_friendly_id?
91
+ assert @users[1].found_using_numeric_id?
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ ActiveRecord::Schema.define(:version => 1) do
4
+
5
+ create_table "books", :force => true do |t|
6
+ t.column "title", "string"
7
+ t.column "type", "text"
8
+ end
9
+
10
+ create_table "things", :force => true do |t|
11
+ t.column "name", "string"
12
+ end
13
+
14
+ create_table "posts", :force => true do |t|
15
+ t.column "title", "string"
16
+ t.column "content", "text"
17
+ t.column "published", "boolean", :default => false
18
+ t.column "created_at", "datetime"
19
+ t.column "updated_at", "datetime"
20
+ end
21
+
22
+ create_table "users", :force => true do |t|
23
+ t.column "login", "string"
24
+ t.column "email", "string"
25
+ t.column "created_at", "datetime"
26
+ t.column "updated_at", "datetime"
27
+ end
28
+
29
+ create_table "people", :force => true do |t|
30
+ t.column "name", "string"
31
+ t.column "country_id", "integer"
32
+ end
33
+
34
+ create_table "countries", :force => true do |t|
35
+ t.column "name", "string"
36
+ end
37
+
38
+ create_table "events", :force => true do |t|
39
+ t.column "name", "string"
40
+ t.column "event_date", "datetime"
41
+ end
42
+
43
+ create_table "slugs", :force => true do |t|
44
+ t.column "name", "string"
45
+ t.column "sluggable_id", "integer"
46
+ t.column "sequence", "integer", :null => false, :default => 1
47
+ t.column "sluggable_type", "string", :limit => 40
48
+ t.column "scope", "string", :limit => 40
49
+ t.column "created_at", "datetime"
50
+ end
51
+
52
+ add_index "slugs", ["sluggable_id"], :name => "index_slugs_on_sluggable_id"
53
+ add_index "slugs", ["name", "sluggable_type", "scope", "sequence"], :name => "index_slugs_on_n_s_s_and_s", :unique => true
54
+
55
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class ScopedModelTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model that uses a scope" do
8
+
9
+ setup do
10
+ Person.delete_all
11
+ Country.delete_all
12
+ Slug.delete_all
13
+ @usa = Country.create!(:name => "USA")
14
+ @canada = Country.create!(:name => "Canada")
15
+ @person = Person.create!(:name => "John Smith", :country => @usa)
16
+ @person2 = Person.create!(:name => "John Smith", :country => @canada)
17
+ end
18
+
19
+ should "find all scoped records without scope" do
20
+ assert_equal 2, Person.find(:all, @person.friendly_id).size
21
+ end
22
+
23
+ should "find a single scoped records with a scope" do
24
+ assert Person.find(@person.friendly_id, :scope => @person.country.to_param)
25
+ end
26
+
27
+ should "raise an error when finding a single scoped record with no scope" do
28
+ assert_raises ActiveRecord::RecordNotFound do
29
+ Person.find(@person.friendly_id)
30
+ end
31
+ end
32
+
33
+ should "append scope error info when missing scope causes a find to fail" do
34
+ begin
35
+ Person.find(@person.friendly_id)
36
+ fail "The find should not have succeeded"
37
+ rescue ActiveRecord::RecordNotFound => e
38
+ assert_match /expected scope/, e.message
39
+ end
40
+ end
41
+
42
+ should "append scope error info when the scope value causes a find to fail" do
43
+ begin
44
+ Person.find(@person.friendly_id, :scope => "badscope")
45
+ fail "The find should not have succeeded"
46
+ rescue ActiveRecord::RecordNotFound => e
47
+ assert_match /scope=badscope/, e.message
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
data/test/slug_test.rb ADDED
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class SlugTest < Test::Unit::TestCase
6
+
7
+ context "a slug" do
8
+
9
+ setup do
10
+ Slug.delete_all
11
+ Post.delete_all
12
+ end
13
+
14
+ should "indicate if it is the most recent slug" do
15
+ @post = Post.create!(:title => "test title", :content => "test content")
16
+ @post.title = "a new title"
17
+ @post.save!
18
+ assert @post.slugs.last.is_most_recent?
19
+ assert !@post.slugs.first.is_most_recent?
20
+ end
21
+
22
+ end
23
+
24
+ context "the Slug class" do
25
+
26
+ should "parse the slug name and sequence" do
27
+ assert_equal ["test", "2"], Slug::parse("test--2")
28
+ end
29
+
30
+ should "parse with a default sequence of 1" do
31
+ assert_equal ["test", "1"], Slug::parse("test")
32
+ end
33
+
34
+ should "should strip diacritics" do
35
+ assert_equal "acai", Slug::strip_diacritics("açaí")
36
+ end
37
+
38
+ should "strip diacritics correctly " do
39
+ input = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
40
+ output = Slug::strip_diacritics(input).split(//)
41
+ expected = "AAAAAAAECEEEIIIIDNOOOOOOUUUUYThssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
42
+ output.each_index do |i|
43
+ assert_equal expected[i], output[i]
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ context "the Slug class's to_friendly_id method" do
50
+
51
+ should "include the sequence if the sequence is greater than 1" do
52
+ slug = Slug.new(:name => "test", :sequence => 2)
53
+ assert_equal "test--2", slug.to_friendly_id
54
+ end
55
+
56
+ should "not include the sequence if the sequence is 1" do
57
+ slug = Slug.new(:name => "test", :sequence => 1)
58
+ assert_equal "test", slug.to_friendly_id
59
+ end
60
+
61
+ end
62
+
63
+ context "the Slug class's normalize method" do
64
+
65
+ should "should lowercase strings" do
66
+ assert_match /abc/, Slug::normalize("ABC")
67
+ end
68
+
69
+ should "should replace whitespace with dashes" do
70
+ assert_match /a-b/, Slug::normalize("a b")
71
+ end
72
+
73
+ should "should replace 2spaces with 1dash" do
74
+ assert_match /a-b/, Slug::normalize("a b")
75
+ end
76
+
77
+ should "should remove punctuation" do
78
+ assert_match /abc/, Slug::normalize('abc!@#$%^&*•¶§∞¢££¡¿()><?"":;][]\.,/')
79
+ end
80
+
81
+ should "should strip trailing space" do
82
+ assert_match /ab/, Slug::normalize("ab ")
83
+ end
84
+
85
+ should "should strip leading space" do
86
+ assert_match /ab/, Slug::normalize(" ab")
87
+ end
88
+
89
+ should "should strip trailing slashes" do
90
+ assert_match /ab/, Slug::normalize("ab-")
91
+ end
92
+
93
+ should "should strip leading slashes" do
94
+ assert_match /ab/, Slug::normalize("-ab")
95
+ end
96
+
97
+ should "should not modify valid name strings" do
98
+ assert_match /a-b-c-d/, Slug::normalize("a-b-c-d")
99
+ end
100
+
101
+ should "work with non roman chars" do
102
+ assert_equal "検-索", Slug::normalize("検 索")
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,284 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class SluggedModelTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model with default FriendlyId options" do
8
+
9
+ setup do
10
+ Post.friendly_id_options = FriendlyId::DEFAULT_FRIENDLY_ID_OPTIONS.merge(:column => :title, :use_slug => true)
11
+ Post.delete_all
12
+ Person.delete_all
13
+ Slug.delete_all
14
+ @post = Post.new :id => 1878415147, :title => "Test post", :content => "Test content", :published => true
15
+ @post.save!
16
+ end
17
+
18
+ should "have friendly_id options" do
19
+ assert_not_nil Post.friendly_id_options
20
+ end
21
+
22
+ should "have a slug" do
23
+ assert_not_nil @post.slug
24
+ end
25
+
26
+ should "be findable by its friendly_id" do
27
+ assert Post.find(@post.friendly_id)
28
+ end
29
+
30
+ should "be findable by its regular id" do
31
+ assert Post.find(@post.id)
32
+ end
33
+
34
+ should "generate slug text" do
35
+ post = Post.new :title => "Test post", :content => "Test content"
36
+ assert_not_nil @post.slug_text
37
+ end
38
+
39
+ should "respect finder conditions" do
40
+ assert_raises ActiveRecord::RecordNotFound do
41
+ Post.find(@post.friendly_id, :conditions => "1 = 2")
42
+ end
43
+ end
44
+
45
+ should "raise an error if the friendly_id text is reserved" do
46
+ assert_raises(FriendlyId::SlugGenerationError) do
47
+ Post.create!(:title => "new")
48
+ end
49
+ end
50
+
51
+ should "raise an error if the friendly_id text is blank" do
52
+ assert_raises(FriendlyId::SlugGenerationError) do
53
+ Post.create(:title => "")
54
+ end
55
+ end
56
+
57
+ should "raise an error if the normalized friendly id becomes blank" do
58
+ assert_raises(FriendlyId::SlugGenerationError) do
59
+ post = Post.create!(:title => "-.-")
60
+ end
61
+ end
62
+
63
+ should "not make a new slug unless the friendly_id method value has changed" do
64
+ @post.content = "Changed content"
65
+ @post.save!
66
+ assert_equal 1, @post.slugs.size
67
+ end
68
+
69
+ should "make a new slug if the friendly_id method value has changed" do
70
+ @post.title = "Changed title"
71
+ @post.save!
72
+ assert_equal 2, @post.slugs.size
73
+ end
74
+
75
+ should "have a slug sequence of 1 by default" do
76
+ assert_equal 1, @post.slug.sequence
77
+ end
78
+
79
+ should "increment sequence for duplicate slug names" do
80
+ @post2 = Post.create! :title => @post.title, :content => "Test content for post2"
81
+ assert_equal 2, @post2.slug.sequence
82
+ end
83
+
84
+ should "have a friendly_id that terminates with -- and the slug sequence if the sequence is greater than 1" do
85
+ @post2 = Post.create! :title => @post.title, :content => "Test content for post2"
86
+ assert_match(/--2\z/, @post2.friendly_id)
87
+ end
88
+
89
+ should "allow datetime columns to be used as slugs" do
90
+ assert Event.create(:name => "Test", :event_date => DateTime.now)
91
+ end
92
+
93
+ should "not strip diacritics" do
94
+ @post = Post.new(:title => "¡Feliz año!")
95
+ assert_match(/#{'ñ'}/, @post.slug_text)
96
+ end
97
+
98
+ should "not convert to ASCII" do
99
+ @post = Post.new(:title => "katakana: ゲコゴサザシジ")
100
+ assert_equal "katakana-ゲコゴサザシジ", @post.slug_text
101
+ end
102
+
103
+ should "allow the same friendly_id across models" do
104
+ @person = Person.create!(:name => @post.title)
105
+ assert_equal @person.friendly_id, @post.friendly_id
106
+ end
107
+
108
+ should "truncate slug text longer than the max length" do
109
+ @post = Post.new(:title => "a" * (Post.friendly_id_options[:max_length] + 1))
110
+ assert_equal @post.slug_text.length, Post.friendly_id_options[:max_length]
111
+ end
112
+
113
+ should "be able to reuse an old friendly_id without incrementing the sequence" do
114
+ old_title = @post.title
115
+ old_friendly_id = @post.friendly_id
116
+ @post.title = "A changed title"
117
+ @post.save!
118
+ @post.title = old_title
119
+ @post.save!
120
+ assert_equal old_friendly_id, @post.friendly_id
121
+ end
122
+
123
+ should "allow eager loading of slugs" do
124
+ assert_nothing_raised do
125
+ Post.find(@post.friendly_id, :include => :slugs)
126
+ end
127
+ end
128
+
129
+ context "and configured to strip diacritics" do
130
+ setup do
131
+ Post.friendly_id_options = Post.friendly_id_options.merge(:strip_diacritics => true)
132
+ end
133
+
134
+ should "strip diacritics from Roman alphabet based characters" do
135
+ @post = Post.new(:title => "¡Feliz año!")
136
+ assert_no_match(/#{'ñ'}/, @post.slug_text)
137
+ end
138
+ end
139
+
140
+ context "and configured to convert to ASCII" do
141
+ setup do
142
+ Post.friendly_id_options = Post.friendly_id_options.merge(:strip_non_ascii => true)
143
+ end
144
+
145
+ should "strip non-ascii characters" do
146
+ @post = Post.new(:title => "katakana: ゲコゴサザシジ")
147
+ assert_equal "katakana", @post.slug_text
148
+ end
149
+ end
150
+
151
+ context "that doesn't have a slug" do
152
+
153
+ setup do
154
+ @post.slug.destroy
155
+ @post = Post.find(@post.id)
156
+ end
157
+
158
+ should "have a to_param method that returns the id cast to a string" do
159
+ assert_equal @post.id.to_s, @post.to_param
160
+ end
161
+
162
+ end
163
+
164
+ context "when found using its friendly_id" do
165
+ setup do
166
+ @post = Post.find(@post.friendly_id)
167
+ end
168
+
169
+ should "indicate that it was found using the friendly_id" do
170
+ assert @post.found_using_friendly_id?
171
+ end
172
+
173
+ should "not indicate that it has a better id" do
174
+ assert !@post.has_better_id?
175
+ end
176
+
177
+ should "not indicate that it was found using its numeric id" do
178
+ assert !@post.found_using_numeric_id?
179
+ end
180
+
181
+ should "have a finder slug" do
182
+ assert_not_nil @post.finder_slug
183
+ end
184
+
185
+ end
186
+
187
+ context "when found using its regular id" do
188
+ setup do
189
+ @post = Post.find(@post.id)
190
+ end
191
+
192
+ should "indicate that it was not found using the friendly id" do
193
+ assert !@post.found_using_friendly_id?
194
+ end
195
+
196
+ should "indicate that it has a better id" do
197
+ assert @post.has_better_id?
198
+ end
199
+
200
+ should "indicate that it was found using its numeric id" do
201
+ assert @post.found_using_numeric_id?
202
+ end
203
+
204
+ should "not have a finder slug" do
205
+ assert_nil @post.finder_slug
206
+ end
207
+
208
+ end
209
+
210
+ context "when found using an outdated friendly id" do
211
+ setup do
212
+ old_id = @post.friendly_id
213
+ @post.title = "Title changed"
214
+ @post.save!
215
+ @post = Post.find(old_id)
216
+ end
217
+
218
+ should "indicate that it was found using a friendly_id" do
219
+ assert @post.found_using_friendly_id?
220
+ end
221
+
222
+ should "indicate that it has a better id" do
223
+ assert @post.has_better_id?
224
+ end
225
+
226
+ should "not indicate that it was found using its numeric id" do
227
+ assert !@post.found_using_numeric_id?
228
+ end
229
+
230
+ should "should have a finder slug different from its default slug" do
231
+ assert_not_equal @post.slug, @post.finder_slug
232
+ end
233
+
234
+ end
235
+
236
+ context "when using an array as the find argument" do
237
+
238
+ setup do
239
+ @post2 = Post.create!(:title => "another post", :content => "more content", :published => true)
240
+ end
241
+
242
+ should "return results when passed an array of non-friendly ids" do
243
+ assert_equal 2, Post.find([@post.id, @post2.id]).size
244
+ end
245
+
246
+ should "return results when passed an array of friendly ids" do
247
+ assert_equal 2, Post.find([@post.friendly_id, @post2.friendly_id]).size
248
+ end
249
+
250
+ should "return results when searching using a named scope" do
251
+ assert_equal 2, Post.published.find([@post.id, @post2.id]).size
252
+ end
253
+
254
+ should "return results when passed a mixed array of friendly and non-friendly ids" do
255
+ assert_equal 2, Post.find([@post.friendly_id, @post2.id]).size
256
+ end
257
+
258
+ should "return results when passed an array of non-friendly ids, of which one represents a record with multiple slugs" do
259
+ @post2.update_attributes(:title => 'another post [updated]')
260
+ assert_equal 2, Post.find([@post.id, @post2.id]).size
261
+ end
262
+
263
+ should "indicate that the results were found using a friendly_id" do
264
+ @posts = Post.find [@post.friendly_id, @post2.friendly_id]
265
+ @posts.each { |p| assert p.found_using_friendly_id? }
266
+ end
267
+
268
+ should "raise an error when all records are not found" do
269
+ assert_raises(ActiveRecord::RecordNotFound) do
270
+ Post.find([@post.friendly_id, 'non-existant-slug-record'])
271
+ end
272
+ end
273
+
274
+ should "allow eager loading of slugs" do
275
+ assert_nothing_raised do
276
+ Post.find([@post.friendly_id, @post2.friendly_id], :include => :slugs)
277
+ end
278
+ end
279
+
280
+ end
281
+
282
+ end
283
+
284
+ end