public
Rubygem
Description: FriendlyId is the “Swiss Army bulldozer” of slugging and permalink plugins for ActiveRecord. It allows you to create pretty URL’s and work with human-friendly strings as if they were numeric ids for ActiveRecord models.
Homepage: http://friendly-id.rubyforge.org
Clone URL: git://github.com/norman/friendly_id.git
Fixed some compatibility issues with Rails 2.0.x. Fixed generated join syntax
(closes ticket #3). Made some access to slugs load more lazily to improve
performance. Removed usage of Edge Rails' parameterize method since it doesn't
properly support the full western character set yet. Removed some extra
whitespace.
norman (author)
Thu Oct 30 14:49:14 -0700 2008
commit  64f7bf2d7d0601c876c6798f03b1c4ffef9fbb7a
tree    fde4b9bdc7225fcb81221b167f29e9f25ca1e1cb
parent  acdad26a87096b8724871f3df366f2525b40f8fe
...
136
137
138
139
140
141
142
143
 
144
145
146
147
148
 
 
149
150
 
151
152
 
153
154
155
156
157
158
 
 
159
160
161
...
165
166
167
168
169
 
 
170
171
172
...
177
178
179
180
 
181
182
183
...
186
187
188
189
 
 
190
 
 
 
 
191
192
193
 
194
195
196
...
200
201
202
203
 
204
205
206
...
210
211
212
213
 
214
215
216
...
263
264
265
266
267
268
269
270
271
272
273
274
275
 
276
 
 
 
 
 
 
 
 
277
278
279
...
136
137
138
 
 
 
 
 
139
140
141
142
 
 
143
144
145
 
146
147
 
148
149
150
151
 
152
153
154
155
156
157
158
...
162
163
164
 
 
165
166
167
168
169
...
174
175
176
 
177
178
179
180
...
183
184
185
 
186
187
188
189
190
191
192
193
194
 
195
196
197
198
...
202
203
204
 
205
206
207
208
...
212
213
214
 
215
216
217
218
...
265
266
267
 
 
 
 
 
 
 
 
 
268
269
270
271
272
273
274
275
276
277
278
279
280
281
0
@@ -136,26 +136,23 @@ module FriendlyId
0
       end
0
     end
0
 
0
-    # Finds the record using only the friendly id. If it can't be found
0
-    # using the friendly id, then it returns false. If you pass in any
0
-    # argument other than an instance of String or Array, then it also
0
-    # returns false. When given as an array will try to find any of the
0
-    # records and return those that can be found.
0
+    # Finds a single record using the friendly_id, or the record's id.
0
     def find_one_with_friendly(id_or_name, options)
0
       conditions = Slug.with_name id_or_name
0
 
0
-      result = with_scope :find => {:joins => :slugs, :conditions => conditions} do
0
-        find_every(options).first
0
+      result = with_scope :find => {:select => "#{self.table_name}.*", :joins => :slugs, :conditions => conditions} do
0
+        find_initial(options)
0
       end
0
-
0
+      
0
       if result
0
-        result.init_finder_slug result.slugs.find_by_name(id_or_name)
0
+        result.finder_slug_name = id_or_name
0
       else
0
         result = find_one_without_friendly id_or_name, options
0
       end
0
-
0
       result
0
     end
0
+
0
+    # Finds multiple records using the friendly_ids, or the records' ids.
0
     def find_some_with_friendly(ids_and_names, options)
0
       slugs = Slug.find_all_by_names_and_sluggable_type ids_and_names, base_class.name
0
 
0
@@ -165,8 +162,8 @@ module FriendlyId
0
 
0
       # search in slugs and own table
0
       results = []
0
-      results += with_scope(:find => {:joins => :slugs, :conditions => Slug.with_names(names)}) { find_every options } unless names.empty?
0
-      results += with_scope(:find => {:conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?)", ids]}) { find_every options } unless ids.empty?
0
+      results += with_scope(:find => {:select => "#{self.table_name}.*", :joins => :slugs, :conditions => Slug.with_names(names)}) { find_every options } unless names.empty?
0
+      results += with_scope(:find => {:select => "#{self.table_name}.*", :conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?)", ids]}) { find_every options } unless ids.empty?
0
 
0
       # calculate expected size, taken from active_record/base.rb
0
       expected_size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
0
@@ -177,7 +174,7 @@ module FriendlyId
0
       # assign finder slugs
0
       slugs.each do |slug|
0
         result = results.find { |r| r.id == slug.sluggable_id } and
0
-        result.init_finder_slug slug
0
+        result.finder_slug_name = slug.name
0
       end
0
 
0
       results
0
@@ -186,11 +183,16 @@ module FriendlyId
0
 
0
   module SluggableInstanceMethods
0
 
0
-    attr :finder_slug
0
+    attr :finder_slug 
0
+    attr_accessor :finder_slug_name
0
 
0
+    def finder_slug
0
+      @finder_slug ||= init_finder_slug
0
+    end
0
+    
0
     # Was the record found using one of its friendly ids?
0
     def found_using_friendly_id?
0
-      !!@finder_slug
0
+      @finder_slug_name
0
     end
0
 
0
     # Was the record found using its numeric id?
0
@@ -200,7 +202,7 @@ module FriendlyId
0
 
0
     # Was the record found using an old friendly id?
0
     def found_using_outdated_friendly_id?
0
-      @finder_slug.id != slug.id
0
+      finder_slug.id != slug.id
0
     end
0
 
0
     # Was the record found using an old friendly id, or its numeric id?
0
@@ -210,7 +212,7 @@ module FriendlyId
0
 
0
     # Returns the friendly id.
0
     def friendly_id
0
-      slug.name
0
+      finder_slug_name or slug.name
0
     end
0
     alias best_id friendly_id
0
 
0
@@ -263,17 +265,17 @@ module FriendlyId
0
       end
0
     end
0
 
0
-    # Sets the slug that was used to find the record. This can be used to
0
-    # determine whether the record was found using the most recent friendly
0
-    # id.
0
-    def init_finder_slug(finder_slug)
0
-      raise RuntimeError, 'Slug already introduced' if @finder_slug
0
-      @finder_slug = finder_slug
0
-      @finder_slug.sluggable = self
0
-    end
0
-
0
     private
0
+
0
     NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
0
+
0
+    def init_finder_slug
0
+      raise RuntimeError, 'No slug name is set' if !@finder_slug_name
0
+      slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => @finder_slug_name})
0
+      slug.sluggable = self
0
+      return slug
0
+    end
0
+
0
     def truncated_friendly_id_base
0
       max_length = friendly_id_options[:max_length]
0
       slug_text = friendly_id_base[0, max_length - NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION]
...
17
18
19
20
21
 
22
23
24
...
42
43
44
 
 
 
 
 
 
 
 
45
46
47
 
 
48
49
50
...
64
65
66
 
67
68
69
...
17
18
19
 
 
20
21
22
23
...
41
42
43
44
45
46
47
48
49
50
51
52
 
 
53
54
55
56
57
...
71
72
73
74
75
76
77
0
@@ -17,8 +17,7 @@ class Slug < ActiveRecord::Base
0
     def find_all_by_names_and_sluggable_type(names, type)
0
       names = with_names names
0
       type  = "#{ quoted_table_name }.sluggable_type = #{ quote_value type, columns_hash['sluggable_type'] }"
0
-
0
-      all :conditions => "#{ names } AND #{ type }"
0
+      find :all, :conditions => "#{ names } AND #{ type }"
0
     end
0
 
0
     # Count exact matches for a slug. Matches include slugs with the same name
0
@@ -42,9 +41,17 @@ class Slug < ActiveRecord::Base
0
     #
0
     # Example:
0
     #   slug.normalize('This... is an example!') # => "this-is-an-example"
0
+    #
0
+    # Note that Rails 2.2.x offers a parameterize method for stripping
0
+    # diacritics. This is not used here because at the time of writing, it
0
+    # handles several characters incorrectly, for instance replacing
0
+    # Icelandic's "thorn" character with "y" rather than "d." This might be
0
+    # pedantic, but I don't want to piss off the Vikings. The last time anyone
0
+    # pissed them off, they uleashed a wave of terror in Europe unlike
0
+    # anything ever seen before or after. I'm not taking any chances.
0
     def normalize(slug_text)
0
-      # As of Oct 9 2008, this is in Edge Rails (http://github.com/rails/rails/commit/90366a1521659d07a3b75936b3231adeb376f1a4)
0
-      return slug_text.parameterize if slug_text.respond_to?(:parameterize)
0
+      # Use this onces it starts working reliably
0
+      # return slug_text.parameterize.to_s if slug_text.respond_to?(:parameterize)
0
       s = slug_text.clone
0
       s.gsub!(/[\?`^~‘’'“”",.;:]/, '')
0
       s.gsub!(/&/, 'and')
0
@@ -64,6 +71,7 @@ class Slug < ActiveRecord::Base
0
 
0
   # Whether or not this slug is the most recent of its owner's slugs.
0
   def is_most_recent?
2
+    debugger
0
     sluggable.slug == self
0
   end
0
 
...
8
9
10
11
12
13
14
15
16
17
18
19
20
 
 
 
 
 
 
21
22
23
24
25
26
 
27
28
29
30
 
31
32
33
34
35
36
 
37
38
39
40
41
 
42
43
44
45
46
 
47
48
49
50
51
52
 
53
54
55
56
57
 
58
59
60
61
62
 
63
64
65
66
67
 
68
69
70
71
72
 
73
74
75
76
77
 
78
79
80
81
82
 
83
84
85
86
87
88
 
89
90
91
92
93
94
 
95
96
97
98
99
100
 
101
102
103
104
105
106
 
107
108
109
110
111
112
 
113
114
115
116
117
118
 
119
120
121
122
123
 
124
125
126
127
128
 
129
130
131
132
133
134
 
135
136
137
...
141
142
143
144
 
145
146
147
...
154
155
156
157
 
158
159
160
...
164
165
166
167
 
168
169
170
171
 
172
173
174
175
176
177
 
178
179
180
...
8
9
10
 
 
 
 
 
 
 
 
 
 
11
12
13
14
15
16
17
18
19
20
21
 
22
23
24
25
 
26
27
28
29
30
31
 
32
33
34
35
36
 
37
38
39
40
41
 
42
43
44
45
46
47
 
48
49
50
51
52
 
53
54
55
56
57
 
58
59
60
61
62
 
63
64
65
66
67
 
68
69
70
71
72
 
73
74
75
76
77
 
78
79
80
81
82
83
 
84
85
86
87
88
89
 
90
91
92
93
94
95
 
96
97
98
99
100
101
 
102
103
104
105
106
107
 
108
109
110
111
112
113
 
114
115
116
117
118
 
119
120
121
122
123
 
124
125
126
127
128
129
 
130
131
132
133
...
137
138
139
 
140
141
142
143
...
150
151
152
 
153
154
155
156
...
160
161
162
 
163
164
165
166
 
167
168
169
170
171
172
 
173
174
175
176
0
@@ -8,130 +8,126 @@ class SluggableTest < Test::Unit::TestCase
0
     Post.friendly_id_options[:max_length] = FriendlyId::ClassMethods::DEFAULT_FRIENDLY_ID_OPTIONS[:max_length]
0
   end
0
 
0
-  # This test fails right now because this fix has not been implemented
0
-  # for sluggable models, only non-sluggable models. The failing test is here
0
-  # as a reminder to FIX THE CODE, but I don't have time to do it right now.
0
-  # The test is temporarily commented out so as not to break anybody's build.
0
-  # def test_finder_options_are_not_ignored
0
-  #   assert_raises ActiveRecord::RecordNotFound do
0
-  #     Post.find_using_friendly_id(slugs(:one).name, :conditions => "1 = 2")
0
-  #   end
0
-  # end
0
-
0
+  def test_finder_options_are_not_ignored
0
+    assert_raises ActiveRecord::RecordNotFound do
0
+      Post.find(slugs(:one).name, :conditions => "1 = 2")
0
+    end
0
+  end
0
+  
0
   def test_post_should_generate_friendly_id
0
    @post = Post.new(:name => "Test post", :content => "Test content")
0
    assert_equal "test-post", @post.generate_friendly_id
0
    assert_equal "Test post", @post.name
0
   end
0
-
0
+  
0
   def test_post_should_have_friendly_id_options
0
     assert_not_nil Post.friendly_id_options
0
   end
0
-
0
+  
0
   def test_slug_should_not_have_friendly_id_options
0
     assert_raises NoMethodError do
0
       Slug.friendly_id_options
0
     end
0
   end
0
-
0
+  
0
   def test_post_should_not_be_found_using_friendly_id_unless_it_really_was
0
     @post = Post.new
0
     assert !@post.found_using_friendly_id?
0
   end
0
-
0
+  
0
   def test_posts_should_be_using_friendly_id_when_given_as_array
0
     @posts = Post.find([posts(:with_one_slug).slug.name, posts(:with_two_slugs).slug.name])
0
     assert @posts.all? { |post| post.found_using_friendly_id? }
0
   end
0
-
0
+  
0
   def test_posts_raises_active_record_not_found_when_not_all_records_found
0
     assert_raises(ActiveRecord::RecordNotFound) do
0
       Post.find([posts(:with_one_slug).slug.name, 'non-existant-slug-record'])
0
     end
0
   end
0
-
0
+  
0
   def test_post_should_be_considered_found_by_numeric_id_as_default
0
     @post = Post.new
0
     assert @post.found_using_numeric_id?
0
   end
0
-
0
+  
0
   def test_post_should_indicate_if_it_was_found_using_numeric_id
0
     @post = Post.find(posts(:with_two_slugs).id)
0
     assert @post.found_using_numeric_id?
0
   end
0
-
0
+  
0
   def test_post_should_indicate_if_it_was_found_using_friendly_id
0
     @post = Post.find(posts(:with_two_slugs).slug.name)
0
     assert @post.found_using_friendly_id?
0
   end
0
-
0
+  
0
   def test_post_should_indicate_if_it_was_found_using_outdated_friendly_id
0
     @post = Post.find(posts(:with_two_slugs).slugs.last.name)
0
     assert @post.found_using_outdated_friendly_id?
0
   end
0
-
0
+  
0
   def test_should_indicate_there_is_a_better_id_if_found_by_numeric_id
0
     @post = Post.find(posts(:with_one_slug).id)
0
     assert @post.has_better_id?
0
   end
0
-
0
+  
0
   def test_should_indicate_there_is_a_better_id_if_found_by_outdated_friendly_id
0
     @post = Post.find(posts(:with_two_slugs).slugs.last.name)
0
     assert @post.has_better_id?
0
   end
0
-
0
+  
0
   def test_should_indicate_correct_best_id
0
     @post = Post.find(posts(:with_two_slugs).slug.name)
0
     assert !@post.has_better_id?
0
     assert slugs(:two_new).name, @post.slug.name
0
   end
0
-
0
+  
0
   def test_should_strip_diactics_from_slug
0
     Post.friendly_id_options[:strip_diacritics] = true
0
     @post = Post.new(:name => "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ", :content => "Test content")
0
     assert_equal "aaaaaaaeceeeiiiidnoooooouuuuythssaaaaaaaeceeeeiiiidnoooooouuuuythy", @post.generate_friendly_id
0
   end
0
-
0
+  
0
   def test_should_not_strip_diactics_from_slug
0
     Post.friendly_id_options[:strip_diacritics] = false
0
     @post = Post.new(:name => "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ", :content => "Test content")
0
     assert_equal "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ", @post.generate_friendly_id
0
   end
0
-
0
+  
0
   def test_post_should_not_make_new_slug_if_name_is_unchanged
0
     posts(:with_one_slug).content = "Edited content"
0
     posts(:with_one_slug).save!
0
     assert_equal 1, posts(:with_one_slug).slugs.size
0
   end
0
-
0
+  
0
   def test_post_should_make_new_slug_if_name_is_changed
0
     posts(:with_one_slug).name = "Edited name"
0
     posts(:with_one_slug).save!
0
     assert_equal 2, posts(:with_one_slug).slugs.size
0
   end
0
-
0
+  
0
   def test_should_not_consider_substrings_as_duplicate_slugs
0
     @substring = slugs(:one).name[0, slugs(:one).name.length - 1]
0
     @post = Post.new(:name => @substring, :content => "stuff")
0
     assert_equal @substring, @post.generate_friendly_id
0
   end
0
-
0
+  
0
   def test_should_append_extension_to_duplicate_slugs
0
     @post = Post.new(:name => slugs(:one).name, :content => "stuff")
0
     assert_equal slugs(:one).name + "-2", @post.generate_friendly_id
0
   end
0
-
0
+  
0
   def test_should_create_post_with_slug
0
     @post = Post.create(:name => "Test post", :content => "Test content")
0
     assert_not_nil @post.slug
0
   end
0
-
0
+  
0
   def test_should_truncate_slugs_longer_than_maxlength
0
     Post.friendly_id_options[:max_length] = 10
0
     @post = Post.new(:name => "x" * 11, :content => "Test content")
0
     assert @post.generate_friendly_id.length <= Post.friendly_id_options[:max_length]
0
   end
0
-
0
+  
0
   def test_should_ensure_truncated_slugs_are_unique
0
     max_length = posts(:with_one_slug).friendly_id.length
0
     Post.friendly_id_options[:max_length] = max_length
0
@@ -141,7 +137,7 @@ class SluggableTest < Test::Unit::TestCase
0
     assert_not_equal posts(:with_one_slug).friendly_id, q.friendly_id
0
     assert_not_equal p.friendly_id, q.friendly_id
0
   end
0
-
0
+  
0
   def test_should_be_able_to_rename_back_to_old_friendly_id
0
     p = Post.create!(:name => "value")
0
     assert_equal "value", p.friendly_id
0
@@ -154,7 +150,7 @@ class SluggableTest < Test::Unit::TestCase
0
     p.reload
0
     assert_equal "value", p.friendly_id
0
   end
0
-
0
+  
0
   def test_should_avoid_extention_collisions
0
     Post.create!(:name => "Post 2/4")
0
     assert Post.create!(:name => "Post")
0
@@ -164,17 +160,17 @@ class SluggableTest < Test::Unit::TestCase
0
     assert Post.create!(:name => "Post-2-2")
0
     assert Post.create!(:name => "Post 2/4")
0
   end
0
-
0
+  
0
   def test_slug_should_indicate_if_it_is_the_most_recent
0
     assert slugs(:two_new).is_most_recent?
0
   end
0
-
0
+  
0
   def test_should_raise_error_if_friendly_is_base_is_blank
0
     assert_raises(FriendlyId::SlugGenerationError) do
0
       Post.create(:name => nil)
0
     end
0
   end
0
-
0
+  
0
   def test_should_not_use_reserved_slugs
0
     post = Post.create!(:name => 'new')
0
     assert_not_equal 'new', post.friendly_id

Comments

ChrisNolan Sun Nov 02 12:33:10 -0800 2008 at lib/slug.rb L50

whoops.

norman Mon Nov 03 07:15:42 -0800 2008 at lib/slug.rb L50

Yes indeed. Fixed in the latest commit. :-)