dougcole-friendly_id 2.0.5 → 2.0.6

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 (43) hide show
  1. data/History.txt +20 -0
  2. data/Manifest.txt +10 -18
  3. data/README.rdoc +34 -5
  4. data/Rakefile +8 -0
  5. data/VERSION.yml +1 -1
  6. data/lib/friendly_id.rb +33 -13
  7. data/lib/friendly_id/non_sluggable_class_methods.rb +3 -3
  8. data/lib/friendly_id/non_sluggable_instance_methods.rb +9 -1
  9. data/lib/friendly_id/slug.rb +14 -12
  10. data/lib/friendly_id/sluggable_class_methods.rb +14 -3
  11. data/lib/friendly_id/sluggable_instance_methods.rb +11 -4
  12. data/lib/friendly_id/version.rb +1 -1
  13. data/test/custom_slug_normalizer_test.rb +35 -0
  14. data/test/models/book.rb +2 -0
  15. data/test/{fixtures → models}/country.rb +0 -0
  16. data/test/models/novel.rb +3 -0
  17. data/test/{fixtures → models}/person.rb +0 -0
  18. data/test/models/post.rb +3 -0
  19. data/test/models/thing.rb +6 -0
  20. data/test/{fixtures → models}/user.rb +0 -0
  21. data/test/non_slugged_test.rb +71 -60
  22. data/test/schema.rb +29 -20
  23. data/test/scoped_model_test.rb +43 -13
  24. data/test/slug_test.rb +93 -74
  25. data/test/slugged_model_test.rb +263 -0
  26. data/test/sti_test.rb +48 -0
  27. data/test/test_helper.rb +30 -29
  28. metadata +15 -20
  29. data/lib/friendly_id/shoulda_macros.rb +0 -36
  30. data/test/database.yml +0 -3
  31. data/test/fixtures/countries.yml +0 -4
  32. data/test/fixtures/people.yml +0 -7
  33. data/test/fixtures/post.rb +0 -3
  34. data/test/fixtures/posts.yml +0 -23
  35. data/test/fixtures/slugs.yml +0 -53
  36. data/test/fixtures/users.yml +0 -7
  37. data/test/rails/2.x/app/controllers/application.rb +0 -0
  38. data/test/rails/2.x/config/boot.rb +0 -109
  39. data/test/rails/2.x/config/database.yml +0 -3
  40. data/test/rails/2.x/config/environment.rb +0 -7
  41. data/test/rails/2.x/config/environments/test.rb +0 -6
  42. data/test/rails/2.x/config/routes.rb +0 -0
  43. data/test/sluggable_test.rb +0 -185
data/History.txt CHANGED
@@ -1,3 +1,23 @@
1
+ == 2.0.4 2009-02-12
2
+
3
+ * 1 major enhancment:
4
+ * You can now pass in your own custom slug generation blocks while setting up friendly_id.
5
+
6
+ == 2.0.3 2009-02-11
7
+
8
+ * 1 minor enhancment:
9
+ * Fixed to_param returning an empty string for non-slugged models with a null friendly_id.
10
+
11
+ == 2.0.2 2009-02-09
12
+
13
+ * 2 major enhancements:
14
+ * Made FriendlyId depend only on ActiveRecord. It should now be possible to
15
+ use FriendlyId with Camping or any other codebase that uses AR.
16
+ * Overhauled creaky testing setup and switched to Shoulda.
17
+
18
+ * 1 minor enhancment:
19
+ * Made reserved words work for non-slugged models.
20
+
1
21
  == 2.0.1 2009-01-19
2
22
 
3
23
  * 1 minor enhancements:
data/Manifest.txt CHANGED
@@ -14,32 +14,24 @@ lib/friendly_id.rb
14
14
  lib/friendly_id/helpers.rb
15
15
  lib/friendly_id/non_sluggable_class_methods.rb
16
16
  lib/friendly_id/non_sluggable_instance_methods.rb
17
- lib/friendly_id/shoulda_macros.rb
18
17
  lib/friendly_id/slug.rb
19
18
  lib/friendly_id/sluggable_class_methods.rb
20
19
  lib/friendly_id/sluggable_instance_methods.rb
21
20
  lib/friendly_id/version.rb
22
21
  lib/tasks/friendly_id.rake
23
22
  lib/tasks/friendly_id.rb
24
- test/database.yml
25
- test/fixtures/countries.yml
26
- test/fixtures/country.rb
27
- test/fixtures/people.yml
28
- test/fixtures/person.rb
29
- test/fixtures/post.rb
30
- test/fixtures/posts.yml
31
- test/fixtures/slugs.yml
32
- test/fixtures/user.rb
33
- test/fixtures/users.yml
23
+ test/custom_slug_normalizer_test.rb
24
+ test/models/book.rb
25
+ test/models/country.rb
26
+ test/models/novel.rb
27
+ test/models/person.rb
28
+ test/models/post.rb
29
+ test/models/thing.rb
30
+ test/models/user.rb
34
31
  test/non_slugged_test.rb
35
- test/rails/2.x/app/controllers/application.rb
36
- test/rails/2.x/config/boot.rb
37
- test/rails/2.x/config/database.yml
38
- test/rails/2.x/config/environment.rb
39
- test/rails/2.x/config/environments/test.rb
40
- test/rails/2.x/config/routes.rb
41
32
  test/schema.rb
42
33
  test/scoped_model_test.rb
43
34
  test/slug_test.rb
44
- test/sluggable_test.rb
35
+ test/slugged_model_test.rb
36
+ test/sti_test.rb
45
37
  test/test_helper.rb
data/README.rdoc CHANGED
@@ -136,9 +136,13 @@ Here's how to do it:
136
136
 
137
137
  class Restaurant < ActiveRecord::Base
138
138
  belongs_to :city
139
- has_friendly_id :name, :use_slug => true, :reserved => ["new", "index"]
139
+ has_friendly_id :name, :use_slug => true, :reserved => ["my", "values"]
140
140
  end
141
141
 
142
+ As of FriendlyId version 2.0.2, "new" and "index" are reseved by default. When
143
+ you attempt to store a reserved value, FriendlyId raises a
144
+ FriendlyId::SlugGenerationError.
145
+
142
146
 
143
147
  === Scoped Slugs
144
148
 
@@ -208,6 +212,33 @@ that uses a non-Roman writing system, your feedback would be most welcome.
208
212
  @post.friendly_id # "友好编号在中国"
209
213
  @post2 = Post.create(:title => "友好编号在中国")
210
214
  @post2.friendly_id # "友好编号在中国--2"
215
+
216
+ === Custom Slug Generation
217
+
218
+ While FriendlyId's slug generation options work for most people, you may need
219
+ something else. As of version 2.0.4 you can pass in your own custom slug
220
+ generation block:
221
+
222
+ require 'stringex'
223
+ class Post < ActiveRecord::Base
224
+ has_friendly_id :title, :use_slug => true do |text|
225
+ # User stringex to generate the friendly_id rather than the baked-in methods
226
+ text.to_url
227
+ end
228
+ end
229
+
230
+ ...
231
+
232
+ @post = Post.create(:title => "tell your readers 你好")
233
+ @post.friendly_id # "tell-your-readers-ni-hao"
234
+
235
+ FriendlyId will still respect your settings for max length and reserved words,
236
+ but will use your block rather than the baked-in methods to normalize the
237
+ friendly_id text.
238
+
239
+ (As an aside, the stringex[http://github.com/rsl/stringex/tree/master] library
240
+ provides some very cool slugging functionality and is a great option for
241
+ apps using FriendlyId in either English or Chinese. Definitely check it out.)
211
242
 
212
243
  == Getting it
213
244
 
@@ -228,7 +259,7 @@ which FriendlyId depends on:
228
259
 
229
260
  == Setting it up
230
261
 
231
- FriendlyId currently works with Rails 2.0.0 and higher. Here's how to set it up.
262
+ FriendlyId currently works with Rails 2.0.0 - 2.3.0. Here's how to set it up.
232
263
 
233
264
  1) Install the Gem:
234
265
 
@@ -267,7 +298,7 @@ rake:friendly_id:remove_old_slugs MODEL=MyModelName DAYS=60
267
298
 
268
299
  == Upgrading from an older version
269
300
 
270
- If you installed an older version of FriendlyId and want to upgrade to 2.0,
301
+ If you installed an older version of FriendlyId and want to upgrade to 2.0.x,
271
302
  follow these steps:
272
303
 
273
304
  ==== Install the friendly_id Gem:
@@ -297,8 +328,6 @@ Add this to the bottom of environment.rb:
297
328
  ./script generate friendly_id_20_upgrade
298
329
  rake db:migrate
299
330
 
300
- That's it!
301
-
302
331
  == Hacking FriendlyId:
303
332
 
304
333
  FriendlyId is {hosted on Github}[git://github.com/norman/friendly_id.git], and
data/Rakefile CHANGED
@@ -59,3 +59,11 @@ def run_coverage(files)
59
59
  puts cmd
60
60
  sh cmd
61
61
  end
62
+
63
+ desc 'Publish RDoc to RubyForge.'
64
+ task :publish_docs => [:clean, :docs] do
65
+ host = "compay@rubyforge.org"
66
+ remote_dir = "/var/www/gforge-projects/friendly-id"
67
+ local_dir = 'doc'
68
+ sh %{rsync -av --delete #{local_dir}/ #{host}:#{remote_dir}}
69
+ end
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 5
2
+ :patch: 6
3
3
  :major: 2
4
4
  :minor: 0
data/lib/friendly_id.rb CHANGED
@@ -1,21 +1,35 @@
1
- require 'unicode'
2
1
  require 'friendly_id/helpers'
3
2
  require 'friendly_id/slug'
4
- require 'friendly_id/shoulda_macros'
5
-
6
3
 
7
4
  # FriendlyId is a comprehensize Rails plugin/gem for slugging and permalinks.
8
5
  module FriendlyId
9
6
 
7
+ # Default options for has_friendly_id.
8
+ DEFAULT_FRIENDLY_ID_OPTIONS = {
9
+ :max_length => 255,
10
+ :method => nil,
11
+ :reserved => ["new", "index"],
12
+ :reserved_message => 'can not be "%s"',
13
+ :scope => nil,
14
+ :strip_diacritics => false,
15
+ :strip_non_ascii => false,
16
+ :use_slug => false }.freeze
17
+
18
+ # Valid keys for has_friendly_id options.
19
+ VALID_FRIENDLY_ID_KEYS = [
20
+ :max_length,
21
+ :reserved,
22
+ :reserved_message,
23
+ :scope,
24
+ :strip_diacritics,
25
+ :strip_non_ascii,
26
+ :use_slug ].freeze
27
+
10
28
  # This error is raised when it's not possible to generate a unique slug.
11
29
  class SlugGenerationError < StandardError ; end
12
30
 
13
31
  module ClassMethods
14
32
 
15
- # Default options for friendly_id.
16
- DEFAULT_FRIENDLY_ID_OPTIONS = {:method => nil, :use_slug => false, :max_length => 255, :reserved => [], :strip_diacritics => false, :scope => nil}.freeze
17
- VALID_FRIENDLY_ID_KEYS = [:use_slug, :max_length, :reserved, :strip_diacritics, :scope].freeze
18
-
19
33
  # Set up an ActiveRecord model to use a friendly_id.
20
34
  #
21
35
  # The column argument can be one of your model's columns, or a method
@@ -25,25 +39,32 @@ module FriendlyId
25
39
  # * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
26
40
  # * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
27
41
  # * <tt>:strip_diacritics</tt> - Defaults to false. If true, it will remove accents, umlauts, etc. from western characters.
28
- # * <tt>:reseved</tt> - Array of words that are reserved and can't be used as slugs. If such a word is used, it will be treated the same as if that slug was already taken (numeric extension will be appended). Defaults to [].
29
- def has_friendly_id(column, options = {})
42
+ # * <tt>:strip_non_ascii</tt> - Defaults to false. If true, it will all non-ascii ([^a-z0-9]) characters.
43
+ # * <tt>:reserved</tt> - Array of words that are reserved and can't be used as friendly_id's. For sluggable models, if such a word is used, it will be treated the same as if that slug was already taken (numeric extension will be appended). Defaults to ["new", "index"].
44
+ # * <tt>:reserved_message</tt> - The validation message that will be shown when a reserved word is used as a frindly_id. Defaults to '"%s" is reserved'.
45
+ def has_friendly_id(column, options = {}, &block)
30
46
  options.assert_valid_keys VALID_FRIENDLY_ID_KEYS
31
47
  options = DEFAULT_FRIENDLY_ID_OPTIONS.merge(options).merge(:column => column)
32
48
  write_inheritable_attribute :friendly_id_options, options
33
- class_inheritable_reader :friendly_id_options
49
+ class_inheritable_accessor :friendly_id_options
50
+ class_inheritable_reader :slug_normalizer_block
34
51
 
35
52
  if options[:use_slug]
36
- has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy, :readonly => true
53
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
37
54
  require 'friendly_id/sluggable_class_methods'
38
55
  require 'friendly_id/sluggable_instance_methods'
39
56
  extend SluggableClassMethods
40
57
  include SluggableInstanceMethods
41
58
  before_save :set_slug
59
+ if block_given?
60
+ write_inheritable_attribute :slug_normalizer_block, block
61
+ end
42
62
  else
43
63
  require 'friendly_id/non_sluggable_class_methods'
44
64
  require 'friendly_id/non_sluggable_instance_methods'
45
65
  extend NonSluggableClassMethods
46
66
  include NonSluggableInstanceMethods
67
+ validate_on_create :validate_friendly_id
47
68
  end
48
69
  end
49
70
 
@@ -55,7 +76,6 @@ module FriendlyId
55
76
  def enable
56
77
  return if ActiveRecord::Base.methods.include? 'has_friendly_id'
57
78
  ActiveRecord::Base.class_eval { extend FriendlyId::ClassMethods }
58
- Test::Unit::TestCase.class_eval { include FriendlyId::ShouldaMacros }
59
79
  end
60
80
 
61
81
  end
@@ -64,4 +84,4 @@ end
64
84
 
65
85
  if defined?(ActiveRecord)
66
86
  FriendlyId::enable
67
- end
87
+ end
@@ -21,17 +21,17 @@ module FriendlyId::NonSluggableClassMethods
21
21
  end
22
22
 
23
23
  def find_some_with_friendly(ids_and_names, options) #:nodoc:#
24
-
24
+
25
25
  results = with_scope :find => options do
26
26
  find :all, :conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?) OR #{friendly_id_options[:column].to_s} IN (?)",
27
27
  ids_and_names.map {|id| id.to_i}, ids_and_names.map {|id| id.to_s}]
28
28
  end
29
-
29
+
30
30
  expected = expected_size(ids_and_names, options)
31
31
  if results.size != expected
32
32
  raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })"
33
33
  end
34
-
34
+
35
35
  results.each {|r| r.send(:found_using_friendly_id=, true) if ids_and_names.include?(r.friendly_id)}
36
36
 
37
37
  results
@@ -24,10 +24,18 @@ module FriendlyId::NonSluggableInstanceMethods
24
24
 
25
25
  # Returns the friendly id, or if none is available, the numeric id.
26
26
  def to_param
27
- friendly_id.to_s || id.to_s
27
+ (friendly_id || id).to_s
28
28
  end
29
29
 
30
30
  private
31
+
32
+ def validate_friendly_id
33
+ if self.class.friendly_id_options[:reserved].include? friendly_id
34
+ self.errors.add(self.class.friendly_id_options[:column],
35
+ self.class.friendly_id_options[:reserved_message] % friendly_id)
36
+ return false
37
+ end
38
+ end
31
39
 
32
40
  def found_using_friendly_id=(value) #:nodoc#
33
41
  @found_using_friendly_id = value
@@ -1,3 +1,4 @@
1
+ require 'unicode'
1
2
  # A Slug is a unique, human-friendly identifier for an ActiveRecord.
2
3
  class Slug < ActiveRecord::Base
3
4
 
@@ -23,12 +24,12 @@ class Slug < ActiveRecord::Base
23
24
  # terror in Europe unlike anything ever seen before or after. I'm not
24
25
  # taking any chances.
25
26
  def normalize(slug_text)
26
- return "" if slug_text.blank?
27
- slug_text.
27
+ return "" if slug_text.nil? || slug_text == ""
28
+ Unicode::normalize_KC(slug_text).
28
29
  send(chars_func).
29
30
  # For some reason Spanish ¡ and ¿ are not detected as non-word
30
31
  # characters. Bug in Ruby?
31
- normalize.gsub(/[\W|¡|¿]/u, ' ').
32
+ gsub(/[\W|¡|¿]/u, ' ').
32
33
  strip.
33
34
  gsub(/\s+/u, '-').
34
35
  gsub(/-\z/u, '').
@@ -42,20 +43,21 @@ class Slug < ActiveRecord::Base
42
43
  return name, sequence
43
44
  end
44
45
 
45
- # Remove diacritics (accents, umlauts, etc.) from the string.
46
+ # Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
47
+ # from "The Ruby Way."
46
48
  def strip_diacritics(string)
47
- require 'unicode'
48
- Unicode::normalize_KD(string).unpack('U*').select { |cp|
49
- cp < 0x300 || cp > 0x036F
50
- }.pack('U*')
49
+ Unicode::normalize_KD(string).unpack('U*').select { |u| u < 0x300 || u > 0x036F }.pack('U*')
50
+ end
51
+
52
+ # Remove non-ascii characters from the string.
53
+ def strip_non_ascii(string)
54
+ strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
51
55
  end
52
56
 
53
57
  private
54
58
 
55
59
  def chars_func
56
- Rails.version =~ /2.2.[\d]*/ ? :mb_chars : :chars
57
- rescue NoMethodError
58
- :chars
60
+ "".respond_to?(:mb_chars) ? :mb_chars : :chars
59
61
  end
60
62
 
61
63
  end
@@ -73,7 +75,7 @@ class Slug < ActiveRecord::Base
73
75
 
74
76
  # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
75
77
  def check_for_blank_name #:nodoc:#
76
- if name.blank?
78
+ if name == "" || name.nil?
77
79
  raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
78
80
  end
79
81
  end
@@ -1,7 +1,7 @@
1
1
  module FriendlyId::SluggableClassMethods
2
-
3
- include FriendlyId::Helpers
4
2
 
3
+ include FriendlyId::Helpers
4
+
5
5
  def self.extended(base) #:nodoc:#
6
6
 
7
7
  class << base
@@ -38,6 +38,17 @@ module FriendlyId::SluggableClassMethods
38
38
  end
39
39
 
40
40
  result
41
+ rescue ActiveRecord::RecordNotFound => e
42
+
43
+ if friendly_id_options[:scope]
44
+ if !scope
45
+ e.message << "; expected scope but got none"
46
+ else
47
+ e.message << " and scope=#{scope}"
48
+ end
49
+ end
50
+
51
+ raise e
41
52
 
42
53
  end
43
54
 
@@ -87,7 +98,7 @@ module FriendlyId::SluggableClassMethods
87
98
  ids = []
88
99
  ids_and_names.each do |id_or_name|
89
100
  name, sequence = Slug.parse id_or_name
90
- slug = Slug.find(:first, :readonly => true, :conditions => {
101
+ slug = Slug.find(:first, :conditions => {
91
102
  :name => name,
92
103
  :scope => scope,
93
104
  :sequence => sequence,
@@ -6,7 +6,7 @@ module FriendlyId::SluggableInstanceMethods
6
6
  attr_accessor :finder_slug_name
7
7
 
8
8
  def finder_slug
9
- @finder_slug ||= init_finder_slug
9
+ @finder_slug ||= init_finder_slug or nil
10
10
  end
11
11
 
12
12
  # Was the record found using one of its friendly ids?
@@ -56,11 +56,18 @@ module FriendlyId::SluggableInstanceMethods
56
56
  # Get the processed string used as the basis of the friendly id.
57
57
  def slug_text
58
58
  base = send friendly_id_options[:column]
59
- if self.friendly_id_options[:strip_diacritics]
60
- base = Slug::normalize(Slug::strip_diacritics(base))
59
+ if self.slug_normalizer_block
60
+ base = self.slug_normalizer_block.call(base)
61
61
  else
62
+ if self.friendly_id_options[:strip_diacritics]
63
+ base = Slug::strip_diacritics(base)
64
+ end
65
+ if self.friendly_id_options[:strip_non_ascii]
66
+ base = Slug::strip_non_ascii(base)
67
+ end
62
68
  base = Slug::normalize(base)
63
69
  end
70
+
64
71
  if base.length > friendly_id_options[:max_length]
65
72
  base = base[0...friendly_id_options[:max_length]]
66
73
  end
@@ -82,7 +89,7 @@ module FriendlyId::SluggableInstanceMethods
82
89
  def init_finder_slug
83
90
  return false if !@finder_slug_name
84
91
  name, sequence = Slug.parse(@finder_slug_name)
85
- slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.name })
92
+ slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
86
93
  finder_slug = slug
87
94
  end
88
95
 
@@ -2,7 +2,7 @@ module FriendlyId #:nodoc:
2
2
  module Version #:nodoc:
3
3
  MAJOR = 2
4
4
  MINOR = 0
5
- TINY = 1
5
+ TINY = 4
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class CustomSlugNormalizerTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model using a custom slug generator" do
8
+
9
+ setup do
10
+ Thing.friendly_id_options = FriendlyId::DEFAULT_FRIENDLY_ID_OPTIONS.merge(:column => :name, :use_slug => true)
11
+ Thing.delete_all
12
+ Slug.delete_all
13
+ end
14
+
15
+ should "invoke the block code" do
16
+ @thing = Thing.create!(:name => "test")
17
+ assert_equal "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", @thing.friendly_id
18
+ end
19
+
20
+ should "respect the max_length option" do
21
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:max_length => 10)
22
+ @thing = Thing.create!(:name => "test")
23
+ assert_equal "a94a8fe5cc", @thing.friendly_id
24
+ end
25
+
26
+ should "respect the reserved option" do
27
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:reserved => ["a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"])
28
+ assert_raises FriendlyId::SlugGenerationError do
29
+ Thing.create!(:name => "test")
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end