norman / friendly_id

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.

This URL has Read+Write access

friendly_id / lib / friendly_id.rb
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 1 # FriendlyId is a Rails plugin which lets you use text-based ids in addition
2 # to numeric ones.
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 3 module Randomba
4 module FriendlyId
d1ac2955 » norman 2008-01-08 imported sources 5
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 6 def self.included(base) # :nodoc:
7 base.extend(ClassMethods)
d1ac2955 » norman 2008-01-08 imported sources 8 end
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 9
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 10 module ClassMethods
9e4c85c7 » norman 2008-01-09 Improved syntax of has_frie... 11
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 12 # Set up an ActiveRecord model to use a friendly_id.
13 #
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 14 # The method argument can be one of your model's columns, or a method
15 # you use to generate the slug.
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 16 #
17 # Options:
18 # * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
19 # * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
20 # * <tt>:strip_diacritics</tt> - Defaults to false. If true, it will remove accents, umlauts, etc. from western characters. You must have the unicode gem installed for this to work.
21 def has_friendly_id(method, options = {})
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 22 options.assert_valid_keys(:use_slug, :max_length, :strip_diacritics)
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 23 options = default_friendly_id_options.merge(options).merge(:method => method)
24 write_inheritable_attribute(:friendly_id_options, options)
25 class_inheritable_reader :friendly_id_options
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 26
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 27 if options[:use_slug]
f13e09ef » norman 2008-03-13 Added ":dependent => :destr... 28 has_many :slugs, :order => "id DESC", :as => :sluggable,
29 :dependent => :destroy
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 30 before_save :set_slug
31 include SluggableInstanceMethods
32 extend SluggableClassMethods
33 else
34 include NonSluggableInstanceMethods
35 extend NonSluggableClassMethods
36 end
37 end
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 38
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 39 # Gets the default options for friendly_id.
40 def default_friendly_id_options
41 {
42 :method => nil,
43 :use_slug => false,
44 :max_length => 255,
45 :strip_diacritics => false
46 }
47 end
9e4c85c7 » norman 2008-01-09 Improved syntax of has_frie... 48
d1ac2955 » norman 2008-01-08 imported sources 49 end
50
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 51 module SingletonMethods
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 52 # Extends ActiveRecord::Base.find to allow simple finds by friendly id.
53 #
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 54 # @record = Record.find("record name")
55 def find(*args)
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 56 find_using_friendly_id(*args) or super(*args)
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 57 end
d1ac2955 » norman 2008-01-08 imported sources 58 end
59
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 60 module NonSluggableClassMethods
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 61 # Finds the record using only the friendly id. If it can't be found
62 # using the friendly id, then it returns false. If you pass in any
48ac24f9 » Emilio Tagua 2008-06-05 Added the option to work wi... 63 # argument other than an instance of String or Array, then it also
64 # returns false.
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 65 # def find_using_friendly_id()
66 # return false unless slug_text.kind_of?(String)
67 # finder = "find_by_#{self.friendly_id_options[:method].to_s}".to_sym
68 # record = send(finder, slug_text)
69 # record.send(:found_using_friendly_id=, true) if record
70 # return record
71 # end
72
73 def find_using_friendly_id(slug_text, options = {})
48ac24f9 » Emilio Tagua 2008-06-05 Added the option to work wi... 74 case slug_text
75 when String
76 finder = "find_by_#{self.friendly_id_options[:method].to_s}".to_sym
77 when Array
78 finder = "find_all_by_#{self.friendly_id_options[:method].to_s}".to_sym
79 else
80 return false
81 end
82 records = send(finder, slug_text, options)
83 [*records].each { |record| record.send(:found_using_friendly_id=, true) } unless records.blank?
84 return records
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 85 end
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 86
d1ac2955 » norman 2008-01-08 imported sources 87 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 88
89 module NonSluggableInstanceMethods
90
91 def self.included(base)
92 base.extend SingletonMethods
93 end
94
95 attr :found_using_friendly_id
96
97 # Was the record found using one of its friendly ids?
98 def found_using_friendly_id?
99 @found_using_friendly_id
100 end
101
102 # Was the record found using its numeric id?
103 def found_using_numeric_id?
104 ! @found_using_friendly_id
105 end
106
107 alias has_better_id? found_using_numeric_id?
108
109 # Returns the friendly_id.
110 def friendly_id
111 send(friendly_id_options[:method].to_sym)
112 end
113
114 alias best_id friendly_id
115
116 # Returns the friendly id, or if none is available, the numeric id.
117 def to_param
118 friendly_id ? friendly_id : id
119 end
120
121 private
122
123 def found_using_friendly_id=(value)
124 @found_using_friendly_id = value
125 end
126
d1ac2955 » norman 2008-01-08 imported sources 127 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 128
129 module SluggableClassMethods
130
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 131 # Finds the record using only the friendly id. If it can't be found
132 # using the friendly id, then it returns false. If you pass in any
48ac24f9 » Emilio Tagua 2008-06-05 Added the option to work wi... 133 # argument other than an instance of String or Array, then it also
134 # returns false. When given as an array will try to find any of the
135 # records and return those that can be found.
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 136 def find_using_friendly_id(*args)
48ac24f9 » Emilio Tagua 2008-06-05 Added the option to work wi... 137 case args.first
138 when String
139 slugs = Slug.find_by_name_and_sluggable_type(args.first, self.to_s)
140 when Array
141 slugs = Slug.find_all_by_name_and_sluggable_type(args.first, self.to_s)
142 else
143 return false
144 end
145
146 return false if slugs.blank? || ![*slugs].all?(&:sluggable)
147 [*slugs].each { |slug| slug.sluggable.send(:finder_slug=, slug) }
148 (slugs.kind_of?(Array)) ? slugs.collect(&:sluggable) : slugs.sluggable
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 149 end
150 end
151
152 module SluggableInstanceMethods
153
154 def self.included(base)
155 base.extend SingletonMethods
156 end
157
158 attr :finder_slug
159
160 # Was the record found using one of its friendly ids?
161 def found_using_friendly_id?
162 !!@finder_slug
163 end
164
165 # Was the record found using its numeric id?
166 def found_using_numeric_id?
167 !found_using_friendly_id?
168 end
169
170 # Was the record found using an old friendly id?
171 def found_using_outdated_friendly_id?
172 @finder_slug.id != slug.id
173 end
174
175 # Was the record found using an old friendly id, or its numeric id?
176 def has_better_id?
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 177 !slug.nil? && (found_using_numeric_id? || found_using_outdated_friendly_id?)
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 178 end
179
180 # Returns the friendly id.
181 def friendly_id
182 slug.name
183 end
184
185 alias best_id friendly_id
186
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 187 # Returns the most recent slug, which is used to determine the friendly
188 # id.
b6f6bf65 » miloops 2008-06-23 Most recently used slug sho... 189 def slug(reload = false)
190 @most_recent_slug = nil if reload
191 @most_recent_slug ||= slugs.first
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 192 end
193
194 # Returns the friendly id, or if none is available, the numeric id.
195 def to_param
196 slug ? slug.name : id
197 end
198
199 # Generate the text for the friendly id, ensuring no duplication.
200 def generate_friendly_id
92ad2ca7 » norman 2008-04-18 Improved slug name collisio... 201 slug_text = truncated_friendly_id_base
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 202 count = Slug.count_matches(slug_text, self.class.to_s, :all,
d1ac2955 » norman 2008-01-08 imported sources 203 :conditions => "sluggable_id <> #{self.id or 0}")
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 204 if count == 0
205 return slug_text
206 else
207 generate_friendly_id_with_extension(slug_text, count)
208 end
d1ac2955 » norman 2008-01-08 imported sources 209 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 210
211 # Set the slug using the generated friendly id.
212 def set_slug
4b8e2998 » norman 2008-01-21 Made set_slug return if the... 213 return unless self.class.friendly_id_options[:use_slug]
b6f6bf65 » miloops 2008-06-23 Most recently used slug sho... 214 @most_recent_slug = nil
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 215 slug_text = generate_friendly_id
216 if slugs.empty? || slugs.first.name != slug_text
8d9986bd » norman 2008-03-13 Updated documentation. Fixe... 217 previous_slug = slugs.find_by_name(slug_text)
218 previous_slug.destroy if previous_slug
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 219 slugs.build(:name => slug_text)
220 end
d1ac2955 » norman 2008-01-08 imported sources 221 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 222
8d9986bd » norman 2008-03-13 Updated documentation. Fixe... 223 # Remove diacritics from the string.
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 224 def strip_diacritics(string)
225 require 'iconv'
226 require 'unicode'
384fc335 » Norman Clarke 2008-04-19 Reversed order of "translit... 227 Iconv.new("ascii//ignore//translit", "utf-8").iconv(Unicode.normalize_KD(string))
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 228 end
229
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 230 # Get the string used as the basis of the friendly id. If you set the
231 # option to remove diacritics from the friendly id's then they will be
232 # removed.
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 233 def friendly_id_base
6ad9710e » Norman Clarke 2008-05-15 Made friendly_id raise an e... 234 base = self.send(friendly_id_options[:method].to_sym)
235 if base.blank?
236 raise SlugGenerationError.new("The method or column used as the base of friendly_id's slug text returned a blank value")
237 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 238 if self.friendly_id_options[:strip_diacritics]
6ad9710e » Norman Clarke 2008-05-15 Made friendly_id raise an e... 239 Slug::normalize(strip_diacritics(base))
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 240 else
6ad9710e » Norman Clarke 2008-05-15 Made friendly_id raise an e... 241 Slug::normalize(base)
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 242 end
243 end
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 244
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 245 protected
246
247 # Sets the slug that was used to find the record. This can be used to
eebc4f30 » adrian 2008-01-23 Changed/fixed documentation... 248 # determine whether the record was found using the most recent friendly
249 # id.
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 250 def finder_slug=(val)
251 @finder_slug = val
252 end
253
254 private
92ad2ca7 » norman 2008-04-18 Improved slug name collisio... 255
256 def truncated_friendly_id_base
257 max_length = friendly_id_options[:max_length]
258 slug_text = friendly_id_base[0, max_length - NUM_CHARS_RESERVED_FOR_EXTENSION]
259 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 260
261 # Reserve a few spaces at the end of the slug for the counter extension.
262 # This is to avoid generating slugs longer than the maxlength when an
263 # extension is added.
264 NUM_CHARS_RESERVED_FOR_EXTENSION = 2
265
266 def generate_friendly_id_with_extension(slug_text, count)
267 extension = "-" + (count + 1).to_s
268 if extension.length > NUM_CHARS_RESERVED_FOR_EXTENSION
269 raise FriendlyId::SlugGenerationError.new("slug text #{slug_text} " +
270 "goes over limit for similarly named slugs")
271 end
92ad2ca7 » norman 2008-04-18 Improved slug name collisio... 272 slug_text = truncated_friendly_id_base + extension
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 273 count = Slug.count_matches(slug_text, self.class.to_s, :all,
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 274 :conditions => "sluggable_id <> #{self.id or 0}")
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 275 if count != 0
92ad2ca7 » norman 2008-04-18 Improved slug name collisio... 276 slug_text = truncated_friendly_id_base + "-" + (count + 1).to_s
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 277 else
278 return slug_text
279 end
d1ac2955 » norman 2008-01-08 imported sources 280 end
281 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 282
283 # This error is raised when it's not possible to generate a unique slug.
284 class SlugGenerationError < StandardError ; end
285
d1ac2955 » norman 2008-01-08 imported sources 286 end
287 end