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.
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 3 module FriendlyId
4
5 # This error is raised when it's not possible to generate a unique slug.
6 SlugGenerationError = Class.new StandardError
7
8 module ClassMethods
9
10 # Default options for friendly_id.
4c9617e3 » metatribe 2008-10-22 Added support for reseved s... 11 DEFAULT_FRIENDLY_ID_OPTIONS = {:method => nil, :use_slug => false, :max_length => 255, :reserved => [], :strip_diacritics => false}.freeze
12 VALID_FRIENDLY_ID_KEYS = [:use_slug, :max_length, :reserved, :strip_diacritics].freeze
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 13
14 # Set up an ActiveRecord model to use a friendly_id.
15 #
48ab818c » Norman Clarke 2008-10-09 Added support for Edge Rail... 16 # The column argument can be one of your model's columns, or a method
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 17 # you use to generate the slug.
18 #
19 # Options:
20 # * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
21 # * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
22 # * <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.
4c9617e3 » metatribe 2008-10-22 Added support for reseved s... 23 # * <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 [].
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 24 def has_friendly_id(column, options = {})
25 options.assert_valid_keys VALID_FRIENDLY_ID_KEYS
26 options = DEFAULT_FRIENDLY_ID_OPTIONS.merge(options).merge(:column => column)
27 write_inheritable_attribute :friendly_id_options, options
28 class_inheritable_reader :friendly_id_options
29
30 if options[:use_slug]
31 has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
32 extend SluggableClassMethods
33 include SluggableInstanceMethods
34 before_save :set_slug
35 else
36 extend NonSluggableClassMethods
37 include NonSluggableInstanceMethods
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 38 end
d1ac2955 » norman 2008-01-08 imported sources 39 end
40
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 41 end
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 42
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 43 module NonSluggableClassMethods
44
45 def self.extended(base)
46 class << base
47 alias_method_chain :find_one, :friendly
48 alias_method_chain :find_some, :friendly
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 49 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 50 end
d1ac2955 » norman 2008-01-08 imported sources 51
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 52 # Finds the record using only the friendly id. If it can't be found
53 # using the friendly id, then it returns false. If you pass in any
54 # argument other than an instance of String or Array, then it also
55 # returns false.
56 # def find_using_friendly_id()
57 # return false unless slug_text.kind_of?(String)
58 # finder = "find_by_#{self.friendly_id_options[:column].to_s}".to_sym
59 # record = send(finder, slug_text)
60 # record.send(:found_using_friendly_id=, true) if record
61 # return record
62 # end
63 def find_one_with_friendly(id, options)
7bc48684 » nagybence 2008-10-21 Corrected find in case if a... 64 if id.is_a?(String) and result = send("find_by_#{ friendly_id_options[:column] }", id, options)
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 65 result.found_using_friendly_id = true
66 else
67 result = find_one_without_friendly id, options
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 68 end
40b8b4e0 » norman 2008-02-07 Feb 7, 2008 69
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 70 result
71 end
72 def find_some_with_friendly(ids_and_names, options)
73 results_by_name = with_scope :find => options do
74 find :all, :conditions => ["#{ quoted_table_name }.#{ friendly_id_options[:column] } IN (?)", ids_and_names]
75 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 76
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 77 names = results_by_name.map { |r| r[ friendly_id_options[:column] ] }
78 ids = ids_and_names - names
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 79
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 80 results_by_id = with_scope :find => options do
81 find :all, :conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?)", ids]
82 end unless ids.empty?
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 83
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 84 results = results_by_name + ( results_by_id || [] )
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 85
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 86 expected_size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
87 expected_size = options[:limit] if options[:limit] && expected_size > options[:limit]
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 88
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 89 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_size })" if results.size != expected_size
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 90
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 91 results_by_name.each { |r| r.found_using_friendly_id = true }
92 results
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 93 end
94
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 95 end
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 96
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 97 module NonSluggableInstanceMethods
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 98
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 99 attr :found_using_friendly_id
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 100
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 101 # Was the record found using one of its friendly ids?
102 def found_using_friendly_id?
103 @found_using_friendly_id
104 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 105
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 106 # Was the record found using its numeric id?
107 def found_using_numeric_id?
108 !@found_using_friendly_id
109 end
110 alias has_better_id? found_using_numeric_id?
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 111
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 112 # Returns the friendly_id.
113 def friendly_id
114 send friendly_id_options[:column]
115 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 116
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 117 alias best_id friendly_id
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 118
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 119 # Returns the friendly id, or if none is available, the numeric id.
120 def to_param
121 friendly_id || id
122 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 123
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 124 def found_using_friendly_id=(value)
125 @found_using_friendly_id = value
d1ac2955 » norman 2008-01-08 imported sources 126 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 127
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 128 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 129
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 130 module SluggableClassMethods
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 131
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 132 def self.extended(base)
133 class << base
134 alias_method_chain :find_one, :friendly
135 alias_method_chain :find_some, :friendly
136 end
137 end
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 138
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 139 # Finds a single record using the friendly_id, or the record's id.
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 140 def find_one_with_friendly(id_or_name, options)
141 conditions = Slug.with_name id_or_name
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 142
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 143 result = with_scope :find => {:select => "#{self.table_name}.*", :joins => :slugs, :conditions => conditions} do
144 find_initial(options)
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 145 end
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 146
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 147 if result
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 148 result.finder_slug_name = id_or_name
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 149 else
150 result = find_one_without_friendly id_or_name, options
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 151 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 152 result
153 end
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 154
155 # Finds multiple records using the friendly_ids, or the records' ids.
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 156 def find_some_with_friendly(ids_and_names, options)
157 slugs = Slug.find_all_by_names_and_sluggable_type ids_and_names, base_class.name
158
159 # seperate ids and slug names
160 names = slugs.map { |s| s[:name] }
161 ids = ids_and_names - names
dd367e69 » boof 2008-10-09 Fixed finder behaviour. 162
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 163 # search in slugs and own table
164 results = []
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 165 results += with_scope(:find => {:select => "#{self.table_name}.*", :joins => :slugs, :conditions => Slug.with_names(names)}) { find_every options } unless names.empty?
166 results += with_scope(:find => {:select => "#{self.table_name}.*", :conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?)", ids]}) { find_every options } unless ids.empty?
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 167
168 # calculate expected size, taken from active_record/base.rb
169 expected_size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
170 expected_size = options[:limit] if options[:limit] && expected_size > options[:limit]
171
172 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_size })" if results.size != expected_size
173
174 # assign finder slugs
175 slugs.each do |slug|
176 result = results.find { |r| r.id == slug.sluggable_id } and
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 177 result.finder_slug_name = slug.name
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 178 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 179
180 results
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 181 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 182 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 183
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 184 module SluggableInstanceMethods
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 185
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 186 attr :finder_slug
187 attr_accessor :finder_slug_name
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 188
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 189 def finder_slug
190 @finder_slug ||= init_finder_slug
191 end
192
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 193 # Was the record found using one of its friendly ids?
194 def found_using_friendly_id?
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 195 @finder_slug_name
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 196 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 197
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 198 # Was the record found using its numeric id?
199 def found_using_numeric_id?
200 !found_using_friendly_id?
201 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 202
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 203 # Was the record found using an old friendly id?
204 def found_using_outdated_friendly_id?
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 205 finder_slug.id != slug.id
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 206 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 207
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 208 # Was the record found using an old friendly id, or its numeric id?
209 def has_better_id?
210 slug and found_using_numeric_id? || found_using_outdated_friendly_id?
211 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 212
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 213 # Returns the friendly id.
214 def friendly_id
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 215 finder_slug_name or slug.name
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 216 end
217 alias best_id friendly_id
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 218
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 219 # Returns the most recent slug, which is used to determine the friendly
220 # id.
221 def slug(reload = false)
222 @most_recent_slug = nil if reload
223 @most_recent_slug ||= slugs.first
224 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 225
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 226 # Returns the friendly id, or if none is available, the numeric id.
227 def to_param
228 slug ? slug.name : id
229 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 230
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 231 # Generate the text for the friendly id, ensuring no duplication.
232 def generate_friendly_id
233 slug_text = truncated_friendly_id_base
234 count = Slug.count_matches slug_text, self.class.name, :all, :conditions => "sluggable_id <> #{ id.to_i }"
4c9617e3 » metatribe 2008-10-22 Added support for reseved s... 235 count += 1 if self.class.friendly_id_options[:reserved].include?(slug_text)
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 236 count == 0 ? slug_text : generate_friendly_id_with_extension(slug_text, count)
237 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 238
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 239 # Set the slug using the generated friendly id.
240 def set_slug
241 if self.class.friendly_id_options[:use_slug]
242 @most_recent_slug = nil
243 slug_text = generate_friendly_id
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 244
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 245 if slugs.empty? || slugs.first.name != slug_text
246 previous_slug = slugs.find_by_name slug_text
247 previous_slug.destroy if previous_slug
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 248
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 249 slugs.build :name => slug_text
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 250 end
d1ac2955 » norman 2008-01-08 imported sources 251 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 252 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 253
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 254 # Get the string used as the basis of the friendly id. If you set the
255 # option to remove diacritics from the friendly id's then they will be
256 # removed.
257 def friendly_id_base
258 base = send friendly_id_options[:column]
259 if base.blank?
260 raise SlugGenerationError.new('The method or column used as the base of friendly_id\'s slug text returned a blank value')
261 elsif self.friendly_id_options[:strip_diacritics]
48ab818c » Norman Clarke 2008-10-09 Added support for Edge Rail... 262 Slug::normalize(Slug::strip_diacritics(base))
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 263 else
48ab818c » Norman Clarke 2008-10-09 Added support for Edge Rail... 264 Slug::normalize(base)
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 265 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 266 end
1aa31a8d » norman 2008-01-09 Added support for non-slugg... 267
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 268 private
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 269
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 270 NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
64f7bf2d » Norman Clarke 2008-10-30 Fixed some compatibility is... Comment 271
272 def init_finder_slug
273 raise RuntimeError, 'No slug name is set' if !@finder_slug_name
274 slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => @finder_slug_name})
275 slug.sluggable = self
276 return slug
277 end
278
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 279 def truncated_friendly_id_base
280 max_length = friendly_id_options[:max_length]
281 slug_text = friendly_id_base[0, max_length - NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION]
282 end
32bc65dd » norman 2008-01-18 Moved plugin code into its ... 283
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 284 # Reserve a few spaces at the end of the slug for the counter extension.
285 # This is to avoid generating slugs longer than the maxlength when an
286 # extension is added.
287 POSSIBILITIES = 10 ** NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION - 1
288 def generate_friendly_id_with_extension(slug_text, count)
289 count <= POSSIBILITIES or
290 raise FriendlyId::SlugGenerationError.new("slug text #{slug_text} goes over limit for similarly named slugs")
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 291
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 292 slug_text = "#{ truncated_friendly_id_base }-#{ count + 1 }"
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 293
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 294 count = Slug.count_matches slug_text, self.class.name, :all, :conditions => "sluggable_id <> #{ id.to_i }"
295 count > 0 ? "#{ truncated_friendly_id_base }-#{ count + 1 }" : slug_text
d1ac2955 » norman 2008-01-08 imported sources 296 end
297 end
ab8d07dd » boof 2008-10-09 Finder got a facelift again... 298
e4543948 » boof 2008-10-06 Refactored friendly_id plugin. 299 end