friendly_id_globalize3 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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