norman / friendly_id
- Source
- Commits
- Network (45)
- Issues (3)
- Downloads (21)
- Wiki (1)
- Graphs
-
Tree:
b6f6bf6
miloops (author)
Mon Jun 23 14:37:34 -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. | ||||
| 32bc65dd » | norman | 2008-01-18 | 3 | module Randomba | |
| 4 | module FriendlyId | ||||
| d1ac2955 » | norman | 2008-01-08 | 5 | ||
| 32bc65dd » | norman | 2008-01-18 | 6 | def self.included(base) # :nodoc: | |
| 7 | base.extend(ClassMethods) | ||||
| d1ac2955 » | norman | 2008-01-08 | 8 | end | |
| 1aa31a8d » | norman | 2008-01-09 | 9 | ||
| 32bc65dd » | norman | 2008-01-18 | 10 | module ClassMethods | |
| 9e4c85c7 » | norman | 2008-01-09 | 11 | ||
| 32bc65dd » | norman | 2008-01-18 | 12 | # Set up an ActiveRecord model to use a friendly_id. | |
| 13 | # | ||||
| eebc4f30 » | adrian | 2008-01-23 | 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 | 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 | 22 | options.assert_valid_keys(:use_slug, :max_length, :strip_diacritics) | |
| 32bc65dd » | norman | 2008-01-18 | 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 | 26 | ||
| 32bc65dd » | norman | 2008-01-18 | 27 | if options[:use_slug] | |
| f13e09ef » | norman | 2008-03-13 | 28 | has_many :slugs, :order => "id DESC", :as => :sluggable, | |
| 29 | :dependent => :destroy | ||||
| 32bc65dd » | norman | 2008-01-18 | 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 | 38 | ||
| 32bc65dd » | norman | 2008-01-18 | 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 | 48 | ||
| d1ac2955 » | norman | 2008-01-08 | 49 | end | |
| 50 | |||||
| 32bc65dd » | norman | 2008-01-18 | 51 | module SingletonMethods | |
| eebc4f30 » | adrian | 2008-01-23 | 52 | # Extends ActiveRecord::Base.find to allow simple finds by friendly id. | |
| 53 | # | ||||
| 32bc65dd » | norman | 2008-01-18 | 54 | # @record = Record.find("record name") | |
| 55 | def find(*args) | ||||
| 40b8b4e0 » | norman | 2008-02-07 | 56 | find_using_friendly_id(*args) or super(*args) | |
| 32bc65dd » | norman | 2008-01-18 | 57 | end | |
| d1ac2955 » | norman | 2008-01-08 | 58 | end | |
| 59 | |||||
| 32bc65dd » | norman | 2008-01-18 | 60 | module NonSluggableClassMethods | |
| eebc4f30 » | adrian | 2008-01-23 | 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 | 63 | # argument other than an instance of String or Array, then it also | |
| 64 | # returns false. | ||||
| 40b8b4e0 » | norman | 2008-02-07 | 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 | 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 | 85 | end | |
| 40b8b4e0 » | norman | 2008-02-07 | 86 | ||
| d1ac2955 » | norman | 2008-01-08 | 87 | end | |
| 32bc65dd » | norman | 2008-01-18 | 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 | 127 | end | |
| 32bc65dd » | norman | 2008-01-18 | 128 | ||
| 129 | module SluggableClassMethods | ||||
| 130 | |||||
| eebc4f30 » | adrian | 2008-01-23 | 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 | 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 | 136 | def find_using_friendly_id(*args) | |
| 48ac24f9 » | Emilio Tagua | 2008-06-05 | 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 | 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 | 177 | !slug.nil? && (found_using_numeric_id? || found_using_outdated_friendly_id?) | |
| 32bc65dd » | norman | 2008-01-18 | 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 | 187 | # Returns the most recent slug, which is used to determine the friendly | |
| 188 | # id. | ||||
| b6f6bf65 » | miloops | 2008-06-23 | 189 | def slug(reload = false) | |
| 190 | @most_recent_slug = nil if reload | ||||
| 191 | @most_recent_slug ||= slugs.first | ||||
| 32bc65dd » | norman | 2008-01-18 | 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 | 201 | slug_text = truncated_friendly_id_base | |
| 32bc65dd » | norman | 2008-01-18 | 202 | count = Slug.count_matches(slug_text, self.class.to_s, :all, | |
| d1ac2955 » | norman | 2008-01-08 | 203 | :conditions => "sluggable_id <> #{self.id or 0}") | |
| 32bc65dd » | norman | 2008-01-18 | 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 | 209 | end | |
| 32bc65dd » | norman | 2008-01-18 | 210 | ||
| 211 | # Set the slug using the generated friendly id. | ||||
| 212 | def set_slug | ||||
| 4b8e2998 » | norman | 2008-01-21 | 213 | return unless self.class.friendly_id_options[:use_slug] | |
| b6f6bf65 » | miloops | 2008-06-23 | 214 | @most_recent_slug = nil | |
| 32bc65dd » | norman | 2008-01-18 | 215 | slug_text = generate_friendly_id | |
| 216 | if slugs.empty? || slugs.first.name != slug_text | ||||
| 8d9986bd » | norman | 2008-03-13 | 217 | previous_slug = slugs.find_by_name(slug_text) | |
| 218 | previous_slug.destroy if previous_slug | ||||
| 32bc65dd » | norman | 2008-01-18 | 219 | slugs.build(:name => slug_text) | |
| 220 | end | ||||
| d1ac2955 » | norman | 2008-01-08 | 221 | end | |
| 32bc65dd » | norman | 2008-01-18 | 222 | ||
| 8d9986bd » | norman | 2008-03-13 | 223 | # Remove diacritics from the string. | |
| 32bc65dd » | norman | 2008-01-18 | 224 | def strip_diacritics(string) | |
| 225 | require 'iconv' | ||||
| 226 | require 'unicode' | ||||
| 384fc335 » | Norman Clarke | 2008-04-19 | 227 | Iconv.new("ascii//ignore//translit", "utf-8").iconv(Unicode.normalize_KD(string)) | |
| 1aa31a8d » | norman | 2008-01-09 | 228 | end | |
| 229 | |||||
| eebc4f30 » | adrian | 2008-01-23 | 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 | 233 | def friendly_id_base | |
| 6ad9710e » | Norman Clarke | 2008-05-15 | 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 | 238 | if self.friendly_id_options[:strip_diacritics] | |
| 6ad9710e » | Norman Clarke | 2008-05-15 | 239 | Slug::normalize(strip_diacritics(base)) | |
| 32bc65dd » | norman | 2008-01-18 | 240 | else | |
| 6ad9710e » | Norman Clarke | 2008-05-15 | 241 | Slug::normalize(base) | |
| 32bc65dd » | norman | 2008-01-18 | 242 | end | |
| 243 | end | ||||
| 1aa31a8d » | norman | 2008-01-09 | 244 | ||
| 32bc65dd » | norman | 2008-01-18 | 245 | protected | |
| 246 | |||||
| 247 | # Sets the slug that was used to find the record. This can be used to | ||||
| eebc4f30 » | adrian | 2008-01-23 | 248 | # determine whether the record was found using the most recent friendly | |
| 249 | # id. | ||||
| 32bc65dd » | norman | 2008-01-18 | 250 | def finder_slug=(val) | |
| 251 | @finder_slug = val | ||||
| 252 | end | ||||
| 253 | |||||
| 254 | private | ||||
| 92ad2ca7 » | norman | 2008-04-18 | 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 | 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 | 272 | slug_text = truncated_friendly_id_base + extension | |
| 32bc65dd » | norman | 2008-01-18 | 273 | count = Slug.count_matches(slug_text, self.class.to_s, :all, | |
| 40b8b4e0 » | norman | 2008-02-07 | 274 | :conditions => "sluggable_id <> #{self.id or 0}") | |
| 32bc65dd » | norman | 2008-01-18 | 275 | if count != 0 | |
| 92ad2ca7 » | norman | 2008-04-18 | 276 | slug_text = truncated_friendly_id_base + "-" + (count + 1).to_s | |
| 32bc65dd » | norman | 2008-01-18 | 277 | else | |
| 278 | return slug_text | ||||
| 279 | end | ||||
| d1ac2955 » | norman | 2008-01-08 | 280 | end | |
| 281 | end | ||||
| 32bc65dd » | norman | 2008-01-18 | 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 | 286 | end | |
| 287 | end | ||||
