friendly_id_globalize3 3.2.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 (58) hide show
  1. data/Changelog.md +354 -0
  2. data/Contributors.md +43 -0
  3. data/Guide.md +686 -0
  4. data/MIT-LICENSE +19 -0
  5. data/README.md +99 -0
  6. data/Rakefile +75 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +40 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +19 -0
  11. data/extras/template-gem.rb +26 -0
  12. data/extras/template-plugin.rb +28 -0
  13. data/generators/friendly_id/friendly_id_generator.rb +30 -0
  14. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  15. data/lib/friendly_id.rb +93 -0
  16. data/lib/friendly_id/active_record.rb +74 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +68 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +148 -0
  19. data/lib/friendly_id/active_record_adapter/relation.rb +165 -0
  20. data/lib/friendly_id/active_record_adapter/simple_model.rb +63 -0
  21. data/lib/friendly_id/active_record_adapter/slug.rb +77 -0
  22. data/lib/friendly_id/active_record_adapter/slugged_model.rb +122 -0
  23. data/lib/friendly_id/active_record_adapter/tasks.rb +72 -0
  24. data/lib/friendly_id/configuration.rb +178 -0
  25. data/lib/friendly_id/datamapper.rb +5 -0
  26. data/lib/friendly_id/railtie.rb +22 -0
  27. data/lib/friendly_id/sequel.rb +5 -0
  28. data/lib/friendly_id/slug_string.rb +25 -0
  29. data/lib/friendly_id/slugged.rb +105 -0
  30. data/lib/friendly_id/status.rb +35 -0
  31. data/lib/friendly_id/test.rb +350 -0
  32. data/lib/friendly_id/version.rb +9 -0
  33. data/lib/generators/friendly_id_generator.rb +25 -0
  34. data/lib/tasks/friendly_id.rake +19 -0
  35. data/rails/init.rb +2 -0
  36. data/test/active_record_adapter/ar_test_helper.rb +150 -0
  37. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  38. data/test/active_record_adapter/cached_slug_test.rb +76 -0
  39. data/test/active_record_adapter/core.rb +138 -0
  40. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  41. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  42. data/test/active_record_adapter/default_scope_test.rb +30 -0
  43. data/test/active_record_adapter/optimistic_locking_test.rb +18 -0
  44. data/test/active_record_adapter/scoped_model_test.rb +119 -0
  45. data/test/active_record_adapter/simple_test.rb +76 -0
  46. data/test/active_record_adapter/slug_test.rb +34 -0
  47. data/test/active_record_adapter/slugged.rb +33 -0
  48. data/test/active_record_adapter/slugged_status_test.rb +28 -0
  49. data/test/active_record_adapter/sti_test.rb +22 -0
  50. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  52. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  53. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  54. data/test/active_record_adapter/support/models.rb +104 -0
  55. data/test/active_record_adapter/tasks_test.rb +82 -0
  56. data/test/friendly_id_test.rb +96 -0
  57. data/test/test_helper.rb +13 -0
  58. metadata +193 -0
@@ -0,0 +1,5 @@
1
+ begin
2
+ require 'friendly_id_datamapper'
3
+ rescue LoadError
4
+ raise "To use FriendlyId's DataMapper adapter, please `gem install friendly_id_datamapper`"
5
+ end
@@ -0,0 +1,22 @@
1
+ module FriendlyId
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer "friendly_id.configure_rails_initialization" do |app|
5
+ # Experimental Sequel support. See: http://github.com/norman/friendly_id_sequel
6
+ if app.config.generators.rails[:orm] == :sequel
7
+ require "friendly_id/sequel"
8
+ # Experimental DataMapper support. See: http://github.com/myabc/friendly_id_datamapper
9
+ elsif app.config.generators.rails[:orm] == :data_mapper
10
+ require 'friendly_id/datamapper'
11
+ else
12
+ # AR is the default.
13
+ require "friendly_id/active_record"
14
+ end
15
+ end
16
+
17
+ rake_tasks do
18
+ load "tasks/friendly_id.rake"
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ begin
2
+ require "friendly_id_sequel"
3
+ rescue LoadError
4
+ raise "To use FriendlyId's Sequel adapter, please `gem install friendly_id_sequel`"
5
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ module FriendlyId
3
+
4
+ class SlugString < Babosa::Identifier
5
+ # Normalize the string for a given {FriendlyId::Configuration}.
6
+ # @param config [FriendlyId::Configuration]
7
+ # @return String
8
+ def normalize_for!(config)
9
+ normalize!(config.babosa_options)
10
+ end
11
+
12
+ # Validate that the slug string is not blank or reserved, and truncate
13
+ # it to the max length if necessary.
14
+ # @param config [FriendlyId::Configuration]
15
+ # @return String
16
+ # @raise FriendlyId::BlankError
17
+ # @raise FriendlyId::ReservedError
18
+ def validate_for!(config)
19
+ truncate_bytes!(config.max_length)
20
+ raise FriendlyId::BlankError if empty?
21
+ raise FriendlyId::ReservedError if config.reserved?(self)
22
+ self
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,105 @@
1
+ module FriendlyId
2
+ module Slugged
3
+
4
+ class Status < FriendlyId::Status
5
+
6
+ attr_accessor :sequence, :slug
7
+
8
+ # Did the find operation use the best possible id? True if +id+ is
9
+ # numeric, but the model has no slug, or +id+ is friendly and current
10
+ def best?
11
+ current? || (numeric? && !record.slug)
12
+ end
13
+
14
+ # Did the find operation use the current slug?
15
+ def current?
16
+ !! slug && slug.current?
17
+ end
18
+
19
+ # Did the find operation use a friendly id?
20
+ def friendly?
21
+ !! (name or slug)
22
+ end
23
+
24
+ def friendly_id=(friendly_id)
25
+ @name, @sequence = friendly_id.parse_friendly_id(record.friendly_id_config.sequence_separator)
26
+ end
27
+
28
+ # Did the find operation use an outdated slug?
29
+ def outdated?
30
+ !current?
31
+ end
32
+
33
+ # The slug that was used to find the model.
34
+ def slug
35
+ @slug ||= record.find_slug(name, sequence)
36
+ end
37
+
38
+ end
39
+
40
+ module Model
41
+ attr_accessor :slug
42
+
43
+ def find_slug
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def friendly_id_config
48
+ self.class.friendly_id_config
49
+ end
50
+
51
+ # Get the {FriendlyId::Status} after the find has been performed.
52
+ def friendly_id_status
53
+ @friendly_id_status ||= Status.new(:record => self)
54
+ end
55
+
56
+ # The friendly id.
57
+ # @param
58
+ def friendly_id(skip_cache = false)
59
+ if friendly_id_config.cache_column? && !skip_cache
60
+ friendly_id = send(friendly_id_config.cache_column)
61
+ end
62
+ friendly_id || (slug.to_friendly_id if slug?)
63
+ end
64
+
65
+ # Clean up the string before setting it as the friendly_id. You can override
66
+ # this method to add your own custom normalization routines.
67
+ # @param string An instance of {FriendlyId::SlugString}.
68
+ # @return [String]
69
+ def normalize_friendly_id(string)
70
+ string.normalize_for!(friendly_id_config).to_s
71
+ end
72
+
73
+ # Does the instance have a slug?
74
+ def slug?
75
+ !! slug
76
+ end
77
+
78
+ private
79
+
80
+ # Get the processed string used as the basis of the friendly id.
81
+ def slug_text
82
+ base = send(friendly_id_config.method)
83
+ unless base.nil? && friendly_id_config.allow_nil?
84
+ text = normalize_friendly_id(SlugString.new(base))
85
+ SlugString.new(text.to_s).validate_for!(friendly_id_config).to_s
86
+ end
87
+ end
88
+
89
+ # Has the slug text changed?
90
+ def slug_text_changed?
91
+ slug_text != slug.name
92
+ end
93
+
94
+ # Has the basis of our friendly id changed, requiring the generation of a
95
+ # new slug?
96
+ def new_slug_needed?
97
+ if friendly_id_config.allow_nil?
98
+ (!slug? && !slug_text.blank?) || (slug? && slug_text_changed?)
99
+ else
100
+ !slug? || slug_text_changed?
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -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,350 @@
1
+ # encoding: utf-8
2
+ Module.send :include, Module.new {
3
+ def test(name, &block)
4
+ define_method("test_#{name.gsub(/[^a-z0-9]/i, "_")}".to_sym, &block)
5
+ end
6
+ alias :should :test
7
+ }
8
+
9
+ module FriendlyId
10
+ module Test
11
+
12
+ # Tests for any model that implements FriendlyId. Any test that tests model
13
+ # features should include this module.
14
+ module Generic
15
+
16
+ def setup
17
+ klass.send delete_all_method
18
+ end
19
+
20
+ def teardown
21
+ klass.send delete_all_method
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 update_method
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def validation_exceptions
49
+ return RuntimeError
50
+ end
51
+
52
+ def assert_validation_error
53
+ if validation_exceptions
54
+ assert_raise(*[validation_exceptions].flatten) do
55
+ yield
56
+ end
57
+ else # DataMapper does not raise Validation Errors
58
+ i = yield
59
+ if i.kind_of?(TrueClass) || i.kind_of?(FalseClass)
60
+ assert !i
61
+ else
62
+ instance = i
63
+ assert !instance.errors.empty?
64
+ end
65
+ end
66
+ end
67
+
68
+ test "models should have a friendly id config" do
69
+ assert_not_nil klass.friendly_id_config
70
+ end
71
+
72
+ test "instances should have a friendly id by default" do
73
+ assert_not_nil instance.friendly_id
74
+ end
75
+
76
+ test "instances should have a friendly id status" do
77
+ assert_not_nil instance.friendly_id_status
78
+ end
79
+
80
+ test "instances should be findable by their friendly id" do
81
+ assert_equal instance, klass.send(find_method, instance.friendly_id)
82
+ end
83
+
84
+ test "instances should be findable by their numeric id as an integer" do
85
+ assert_equal instance, klass.send(find_method, instance.id.to_i)
86
+ end
87
+
88
+ test "instances should be findable by their numeric id as a string" do
89
+ assert_equal instance, klass.send(find_method, instance.id.to_s)
90
+ end
91
+
92
+ test "instances should be findable by a numeric friendly_id" do
93
+ instance = klass.send(create_method, :name => "206")
94
+ assert_equal instance, klass.send(find_method, "206")
95
+ end
96
+
97
+ test "creation should raise an error if the friendly_id text is reserved" do
98
+ assert_validation_error do
99
+ klass.send(create_method, :name => "new")
100
+ end
101
+ end
102
+
103
+ test "creation should raise an error if the friendly_id text is an empty string" do
104
+ assert_validation_error do
105
+ klass.send(create_method, :name => "")
106
+ end
107
+ end
108
+
109
+ test "creation should raise an error if the friendly_id text is a blank string" do
110
+ assert_validation_error do
111
+ klass.send(create_method, :name => " ")
112
+ end
113
+ end
114
+
115
+ test "creation should raise an error if the friendly_id text is nil and allow_nil is false" do
116
+ assert_validation_error do
117
+ klass.send(create_method, :name => nil)
118
+ end
119
+ end
120
+
121
+ test "creation should succeed if the friendly_id text is nil and allow_nil is true" do
122
+ klass.friendly_id_config.stubs(:allow_nil?).returns(true)
123
+ assert klass.send(create_method, :name => nil)
124
+ end
125
+
126
+ test "should allow the same friendly_id across models" do
127
+ other_instance = other_class.send(create_method, :name => instance.name)
128
+ assert_equal other_instance.friendly_id, instance.friendly_id
129
+ end
130
+
131
+ test "reserved words can be specified as a regular expression" do
132
+ klass.friendly_id_config.stubs(:reserved_words).returns(/jo/)
133
+ assert_validation_error do
134
+ klass.send(create_method, :name => "joe")
135
+ end
136
+ end
137
+
138
+ test "should not raise reserved error unless regexp matches" do
139
+ klass.friendly_id_config.stubs(:reserved_words).returns(/ddsadad/)
140
+ assert_nothing_raised do
141
+ klass.send(create_method, :name => "joe")
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ module Simple
148
+
149
+ test "should allow friendly_id to be nillable if allow_nil is true" do
150
+ klass.friendly_id_config.stubs(:allow_nil?).returns(true)
151
+ instance = klass.send(create_method, :name => "hello")
152
+ assert instance.friendly_id
153
+ instance.name = nil
154
+ assert instance.send(save_method)
155
+ end
156
+
157
+ end
158
+
159
+ # Tests for any model that implements slugs.
160
+ module Slugged
161
+
162
+ test "should have a slug" do
163
+ assert_not_nil instance.slug
164
+ end
165
+
166
+ test "should not make a new slug unless the friendly_id method value has changed" do
167
+ instance.note = instance.note.to_s << " updated"
168
+ instance.send save_method
169
+ assert_equal 1, instance.slugs.size
170
+ end
171
+
172
+ test "should make a new slug if the friendly_id method value has changed" do
173
+ instance.name = "Changed title"
174
+ instance.send save_method
175
+ slugs = if instance.slugs.respond_to?(:reload)
176
+ instance.slugs.reload
177
+ else
178
+ instance.slugs(true)
179
+ end
180
+ assert_equal 2, slugs.size
181
+ end
182
+
183
+ test "should be able to reuse an old friendly_id without incrementing the sequence" do
184
+ old_title = instance.name
185
+ old_friendly_id = instance.friendly_id
186
+ instance.name = "A changed title"
187
+ instance.send save_method
188
+ instance.name = old_title
189
+ instance.send save_method
190
+ assert_equal old_friendly_id, instance.friendly_id
191
+ end
192
+
193
+ test "should increment the slug sequence for duplicate friendly ids" do
194
+ instance2 = klass.send(create_method, :name => instance.name)
195
+ assert_match(/2\z/, instance2.friendly_id)
196
+ end
197
+
198
+ test "should find instance with a sequenced friendly_id" do
199
+ instance2 = klass.send(create_method, :name => instance.name)
200
+ assert_equal instance2, klass.send(find_method, instance2.friendly_id)
201
+ end
202
+
203
+ test "should indicate correct status when found with a sequence" do
204
+ instance2 = klass.send(create_method, :name => instance.name)
205
+ instance2 = klass.send(find_method, instance2.friendly_id)
206
+ assert instance2.friendly_id_status.best?
207
+ end
208
+
209
+ test "should indicate correct status when found by a numeric friendly_id" do
210
+ instance = klass.send(create_method, :name => "100")
211
+ instance2 = klass.send(find_method, "100")
212
+ assert instance2.friendly_id_status.best?, "status expected to be best but isn't."
213
+ assert instance2.friendly_id_status.current?, "status expected to be current but isn't."
214
+ end
215
+
216
+ test "should remain findable by previous slugs" do
217
+ old_friendly_id = instance.friendly_id
218
+ instance.name = "#{old_friendly_id} updated"
219
+ instance.send(save_method)
220
+ assert_not_equal old_friendly_id, instance.friendly_id
221
+ assert_equal instance, klass.send(find_method, old_friendly_id)
222
+ end
223
+
224
+ test "should not create a slug when allow_nil is true and friendy_id text is blank" do
225
+ klass.friendly_id_config.stubs(:allow_nil?).returns(true)
226
+ instance = klass.send(create_method, :name => nil)
227
+ assert_nil instance.slug
228
+ end
229
+
230
+ test "should not allow friendly_id to be nillable even if allow_nil is true" do
231
+ klass.friendly_id_config.stubs(:allow_nil?).returns(true)
232
+ instance = klass.send(create_method, :name => "hello")
233
+ assert instance.friendly_id
234
+ instance.name = nil
235
+ assert_validation_error do
236
+ instance.send(save_method)
237
+ end
238
+ end
239
+
240
+ test "should approximate ascii if configured" do
241
+ klass.friendly_id_config.stubs(:approximate_ascii?).returns(true)
242
+ instance = klass.send(create_method, :name => "Cañón")
243
+ assert_equal "canon", instance.friendly_id
244
+ end
245
+
246
+ test "should approximate ascii with options if configured" do
247
+ klass.friendly_id_config.stubs(:approximate_ascii?).returns(true)
248
+ klass.friendly_id_config.stubs(:ascii_approximation_options).returns(:spanish)
249
+ instance = klass.send(create_method, :name => "Cañón")
250
+ assert_equal "canion", instance.friendly_id
251
+ end
252
+ end
253
+
254
+ # Tests for FriendlyId::Status.
255
+ module Status
256
+
257
+ test "should default to not friendly" do
258
+ assert !status.friendly?
259
+ end
260
+
261
+ test "should default to numeric" do
262
+ assert status.numeric?
263
+ end
264
+
265
+ end
266
+
267
+ # Tests for FriendlyId::Status for a model that uses slugs.
268
+ module SluggedStatus
269
+
270
+ test "should be friendly if slug is set" do
271
+ status.slug = Slug.new
272
+ assert status.friendly?
273
+ end
274
+
275
+ test "should be friendly if name is set" do
276
+ status.name = "name"
277
+ assert status.friendly?
278
+ end
279
+
280
+ test "should be current if current slug is set" do
281
+ status.slug = instance.slug
282
+ assert status.current?
283
+ end
284
+
285
+ test "should not be current if non-current slug is set" do
286
+ status.slug = Slug.new(:sluggable => instance)
287
+ assert !status.current?
288
+ end
289
+
290
+ test "should be best if it is current" do
291
+ status.slug = instance.slug
292
+ assert status.best?
293
+ end
294
+
295
+ test "should be best if it is numeric, but record has no slug" do
296
+ instance.slugs = []
297
+ instance.slug = nil
298
+ assert status.best?
299
+ end
300
+
301
+ [:record, :name].each do |symbol|
302
+ test "should have #{symbol} after find using friendly_id" do
303
+ instance2 = klass.send(find_method, instance.friendly_id)
304
+ assert_not_nil instance2.friendly_id_status.send(symbol)
305
+ end
306
+ end
307
+
308
+ def status
309
+ @status ||= instance.friendly_id_status
310
+ end
311
+
312
+ def klass
313
+ raise NotImplementedError
314
+ end
315
+
316
+ def instance
317
+ raise NotImplementedError
318
+ end
319
+
320
+ end
321
+
322
+ # Tests for models to ensure that they properly implement using the
323
+ # +normalize_friendly_id+ method to allow developers to hook into the
324
+ # slug string generation.
325
+ module CustomNormalizer
326
+
327
+ test "should invoke the custom normalizer" do
328
+ assert_equal "JOE SCHMOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
329
+ end
330
+
331
+ test "should respect the max_length option" do
332
+ klass.friendly_id_config.stubs(:max_length).returns(3)
333
+ assert_equal "JOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
334
+ end
335
+
336
+ test "should raise an error if the friendly_id text is reserved" do
337
+ klass.friendly_id_config.stubs(:reserved_words).returns(["JOE"])
338
+ if validation_exceptions
339
+ assert_raise(*[validation_exceptions].flatten) do
340
+ klass.send(create_method, :name => "Joe")
341
+ end
342
+ else # DataMapper does not raise Validation Errors
343
+ instance = klass.send(create_method, :name => "Joe")
344
+ assert !instance.errors.empty?
345
+ end
346
+ end
347
+
348
+ end
349
+ end
350
+ end