norman / friendly_id
- Source
- Commits
- Network (46)
- Issues (3)
- Downloads (21)
- Wiki (1)
- Graphs
-
Tree:
64f7bf2
Norman Clarke (author)
Thu Oct 30 14:49:14 -0700 2008
friendly_id / lib / friendly_id.rb
| 1aa31a8d » | norman | 2008-01-09 | 1 | # FriendlyId is a Rails plugin which lets you use text-based ids in addition | |
| 2 | # to numeric ones. | ||||
| ab8d07dd » | boof | 2008-10-09 | 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 | 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 | 13 | ||
| 14 | # Set up an ActiveRecord model to use a friendly_id. | ||||
| 15 | # | ||||
| 48ab818c » | Norman Clarke | 2008-10-09 | 16 | # The column argument can be one of your model's columns, or a method | |
| ab8d07dd » | boof | 2008-10-09 | 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 | 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 | 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 | 38 | end | |
| d1ac2955 » | norman | 2008-01-08 | 39 | end | |
| 40 | |||||
| ab8d07dd » | boof | 2008-10-09 | 41 | end | |
| e4543948 » | boof | 2008-10-06 | 42 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 49 | end | |
| ab8d07dd » | boof | 2008-10-09 | 50 | end | |
| d1ac2955 » | norman | 2008-01-08 | 51 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 64 | if id.is_a?(String) and result = send("find_by_#{ friendly_id_options[:column] }", id, options) | |
| ab8d07dd » | boof | 2008-10-09 | 65 | result.found_using_friendly_id = true | |
| 66 | else | ||||
| 67 | result = find_one_without_friendly id, options | ||||
| e4543948 » | boof | 2008-10-06 | 68 | end | |
| 40b8b4e0 » | norman | 2008-02-07 | 69 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 76 | ||
| ab8d07dd » | boof | 2008-10-09 | 77 | names = results_by_name.map { |r| r[ friendly_id_options[:column] ] } | |
| 78 | ids = ids_and_names - names | ||||
| e4543948 » | boof | 2008-10-06 | 79 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 83 | ||
| ab8d07dd » | boof | 2008-10-09 | 84 | results = results_by_name + ( results_by_id || [] ) | |
| e4543948 » | boof | 2008-10-06 | 85 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 88 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 90 | ||
| ab8d07dd » | boof | 2008-10-09 | 91 | results_by_name.each { |r| r.found_using_friendly_id = true } | |
| 92 | results | ||||
| e4543948 » | boof | 2008-10-06 | 93 | end | |
| 94 | |||||
| ab8d07dd » | boof | 2008-10-09 | 95 | end | |
| e4543948 » | boof | 2008-10-06 | 96 | ||
| ab8d07dd » | boof | 2008-10-09 | 97 | module NonSluggableInstanceMethods | |
| 32bc65dd » | norman | 2008-01-18 | 98 | ||
| ab8d07dd » | boof | 2008-10-09 | 99 | attr :found_using_friendly_id | |
| 32bc65dd » | norman | 2008-01-18 | 100 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 105 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 111 | ||
| ab8d07dd » | boof | 2008-10-09 | 112 | # Returns the friendly_id. | |
| 113 | def friendly_id | ||||
| 114 | send friendly_id_options[:column] | ||||
| 115 | end | ||||
| 32bc65dd » | norman | 2008-01-18 | 116 | ||
| ab8d07dd » | boof | 2008-10-09 | 117 | alias best_id friendly_id | |
| 32bc65dd » | norman | 2008-01-18 | 118 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 123 | ||
| ab8d07dd » | boof | 2008-10-09 | 124 | def found_using_friendly_id=(value) | |
| 125 | @found_using_friendly_id = value | ||||
| d1ac2955 » | norman | 2008-01-08 | 126 | end | |
| 32bc65dd » | norman | 2008-01-18 | 127 | ||
| ab8d07dd » | boof | 2008-10-09 | 128 | end | |
| 32bc65dd » | norman | 2008-01-18 | 129 | ||
| ab8d07dd » | boof | 2008-10-09 | 130 | module SluggableClassMethods | |
| e4543948 » | boof | 2008-10-06 | 131 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 138 | ||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 139 | # Finds a single record using the friendly_id, or the record's id. | |
| ab8d07dd » | boof | 2008-10-09 | 140 | def find_one_with_friendly(id_or_name, options) | |
| 141 | conditions = Slug.with_name id_or_name | ||||
| e4543948 » | boof | 2008-10-06 | 142 | ||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 143 | result = with_scope :find => {:select => "#{self.table_name}.*", :joins => :slugs, :conditions => conditions} do | |
| 144 | find_initial(options) | ||||
| ab8d07dd » | boof | 2008-10-09 | 145 | end | |
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 146 | ||
| ab8d07dd » | boof | 2008-10-09 | 147 | if result | |
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 148 | result.finder_slug_name = id_or_name | |
| ab8d07dd » | boof | 2008-10-09 | 149 | else | |
| 150 | result = find_one_without_friendly id_or_name, options | ||||
| 32bc65dd » | norman | 2008-01-18 | 151 | end | |
| ab8d07dd » | boof | 2008-10-09 | 152 | result | |
| 153 | end | ||||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 154 | ||
| 155 | # Finds multiple records using the friendly_ids, or the records' ids. | ||||
| ab8d07dd » | boof | 2008-10-09 | 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 | 162 | ||
| ab8d07dd » | boof | 2008-10-09 | 163 | # search in slugs and own table | |
| 164 | results = [] | ||||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 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 | 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 | 177 | result.finder_slug_name = slug.name | |
| e4543948 » | boof | 2008-10-06 | 178 | end | |
| ab8d07dd » | boof | 2008-10-09 | 179 | ||
| 180 | results | ||||
| e4543948 » | boof | 2008-10-06 | 181 | end | |
| ab8d07dd » | boof | 2008-10-09 | 182 | end | |
| 32bc65dd » | norman | 2008-01-18 | 183 | ||
| ab8d07dd » | boof | 2008-10-09 | 184 | module SluggableInstanceMethods | |
| 32bc65dd » | norman | 2008-01-18 | 185 | ||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 186 | attr :finder_slug | |
| 187 | attr_accessor :finder_slug_name | ||||
| 32bc65dd » | norman | 2008-01-18 | 188 | ||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 189 | def finder_slug | |
| 190 | @finder_slug ||= init_finder_slug | ||||
| 191 | end | ||||
| 192 | |||||
| ab8d07dd » | boof | 2008-10-09 | 193 | # Was the record found using one of its friendly ids? | |
| 194 | def found_using_friendly_id? | ||||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 195 | @finder_slug_name | |
| ab8d07dd » | boof | 2008-10-09 | 196 | end | |
| 32bc65dd » | norman | 2008-01-18 | 197 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 202 | ||
| ab8d07dd » | boof | 2008-10-09 | 203 | # Was the record found using an old friendly id? | |
| 204 | def found_using_outdated_friendly_id? | ||||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 205 | finder_slug.id != slug.id | |
| ab8d07dd » | boof | 2008-10-09 | 206 | end | |
| 32bc65dd » | norman | 2008-01-18 | 207 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 212 | ||
| ab8d07dd » | boof | 2008-10-09 | 213 | # Returns the friendly id. | |
| 214 | def friendly_id | ||||
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 215 | finder_slug_name or slug.name | |
| ab8d07dd » | boof | 2008-10-09 | 216 | end | |
| 217 | alias best_id friendly_id | ||||
| 32bc65dd » | norman | 2008-01-18 | 218 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 225 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 230 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 235 | count += 1 if self.class.friendly_id_options[:reserved].include?(slug_text) | |
| ab8d07dd » | boof | 2008-10-09 | 236 | count == 0 ? slug_text : generate_friendly_id_with_extension(slug_text, count) | |
| 237 | end | ||||
| 32bc65dd » | norman | 2008-01-18 | 238 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 244 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 248 | ||
| ab8d07dd » | boof | 2008-10-09 | 249 | slugs.build :name => slug_text | |
| 32bc65dd » | norman | 2008-01-18 | 250 | end | |
| d1ac2955 » | norman | 2008-01-08 | 251 | end | |
| ab8d07dd » | boof | 2008-10-09 | 252 | end | |
| 32bc65dd » | norman | 2008-01-18 | 253 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 262 | Slug::normalize(Slug::strip_diacritics(base)) | |
| ab8d07dd » | boof | 2008-10-09 | 263 | else | |
| 48ab818c » | Norman Clarke | 2008-10-09 | 264 | Slug::normalize(base) | |
| 32bc65dd » | norman | 2008-01-18 | 265 | end | |
| ab8d07dd » | boof | 2008-10-09 | 266 | end | |
| 1aa31a8d » | norman | 2008-01-09 | 267 | ||
| ab8d07dd » | boof | 2008-10-09 | 268 | private | |
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 269 | ||
| ab8d07dd » | boof | 2008-10-09 | 270 | NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2 | |
| 64f7bf2d » | Norman Clarke | 2008-10-30 | 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 | 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 | 283 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 291 | ||
| ab8d07dd » | boof | 2008-10-09 | 292 | slug_text = "#{ truncated_friendly_id_base }-#{ count + 1 }" | |
| e4543948 » | boof | 2008-10-06 | 293 | ||
| ab8d07dd » | boof | 2008-10-09 | 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 | 296 | end | |
| 297 | end | ||||
| ab8d07dd » | boof | 2008-10-09 | 298 | ||
| e4543948 » | boof | 2008-10-06 | 299 | end | |

