1 2 3 4 5 6 7 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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
|
require "friendly_id/slug_generator"
require "friendly_id/candidates"
module FriendlyId
# @guide begin
#
# ## Slugged Models
#
# FriendlyId can use a separate column to store slugs for models which require
# some text processing.
#
# For example, blog applications typically use a post title to provide the basis
# of a search engine friendly URL. Such identifiers typically lack uppercase
# characters, use ASCII to approximate UTF-8 characters, and strip out other
# characters which may make them aesthetically unappealing or error-prone when
# used in a URL.
#
# class Post < ActiveRecord::Base
# extend FriendlyId
# friendly_id :title, :use => :slugged
# end
#
# @post = Post.create(:title => "This is the first post!")
# @post.friendly_id # returns "this-is-the-first-post"
# redirect_to @post # the URL will be /posts/this-is-the-first-post
#
# In general, use slugs by default unless you know for sure you don't need them.
# To activate the slugging functionality, use the {FriendlyId::Slugged} module.
#
# FriendlyId will generate slugs from a method or column that you specify, and
# store them in a field in your model. By default, this field must be named
# `:slug`, though you may change this using the
# {FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
# option. You should add an index to this column, and in most cases, make it
# unique. You may also wish to constrain it to NOT NULL, but this depends on your
# app's behavior and requirements.
#
# ### Example Setup
#
# # your model
# class Post < ActiveRecord::Base
# extend FriendlyId
# friendly_id :title, :use => :slugged
# validates_presence_of :title, :slug, :body
# end
#
# # a migration
# class CreatePosts < ActiveRecord::Migration
# def self.up
# create_table :posts do |t|
# t.string :title, :null => false
# t.string :slug, :null => false
# t.text :body
# end
#
# add_index :posts, :slug, :unique => true
# end
#
# def self.down
# drop_table :posts
# end
# end
#
# ### Working With Slugs
#
# #### Formatting
#
# By default, FriendlyId uses Active Support's
# [parameterize](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize)
# method to create slugs. This method will intelligently replace spaces with
# dashes, and Unicode Latin characters with ASCII approximations:
#
# movie = Movie.create! :title => "Der Preis fürs Überleben"
# movie.slug #=> "der-preis-furs-uberleben"
#
# #### Column or Method?
#
# FriendlyId always uses a method as the basis of the slug text - not a column. At
# first glance, this may sound confusing, but remember that Active Record provides
# methods for each column in a model's associated table, and that's what
# FriendlyId uses.
#
# Here's an example of a class that uses a custom method to generate the slug:
#
# class Person < ActiveRecord::Base
# extend FriendlyId
# friendly_id :name_and_location, use: :slugged
#
# def name_and_location
# "#{name} from #{location}"
# end
# end
#
# bob = Person.create! :name => "Bob Smith", :location => "New York City"
# bob.friendly_id #=> "bob-smith-from-new-york-city"
#
# FriendlyId refers to this internally as the "base" method.
#
# #### Uniqueness
#
# When you try to insert a record that would generate a duplicate friendly id,
# FriendlyId will append a UUID to the generated slug to ensure uniqueness:
#
# car = Car.create :title => "Peugeot 206"
# car2 = Car.create :title => "Peugeot 206"
#
# car.friendly_id #=> "peugeot-206"
# car2.friendly_id #=> "peugeot-206-f9f3789a-daec-4156-af1d-fab81aa16ee5"
#
# Previous versions of FriendlyId appended a numeric sequence to make slugs
# unique, but this was removed to simplify using FriendlyId in concurrent code.
#
# #### Candidates
#
# Since UUIDs are ugly, FriendlyId provides a "slug candidates" functionality to
# let you specify alternate slugs to use in the event the one you want to use is
# already taken. For example:
#
# class Restaurant < ActiveRecord::Base
# extend FriendlyId
# friendly_id :slug_candidates, use: :slugged
#
# # Try building a slug based on the following fields in
# # increasing order of specificity.
# def slug_candidates
# [
# :name,
# [:name, :city],
# [:name, :street, :city],
# [:name, :street_number, :street, :city]
# ]
# end
# end
#
# r1 = Restaurant.create! name: 'Plaza Diner', city: 'New Paltz'
# r2 = Restaurant.create! name: 'Plaza Diner', city: 'Kingston'
#
# r1.friendly_id #=> 'plaza-diner'
# r2.friendly_id #=> 'plaza-diner-kingston'
#
# To use candidates, make your FriendlyId base method return an array. The
# method need not be named `slug_candidates`; it can be anything you want. The
# array may contain any combination of symbols, strings, procs or lambdas and
# will be evaluated lazily and in order. If you include symbols, FriendlyId will
# invoke a method on your model class with the same name. Strings will be
# interpreted literally. Procs and lambdas will be called and their return values
# used as the basis of the friendly id. If none of the candidates can generate a
# unique slug, then FriendlyId will append a UUID to the first candidate as a
# last resort.
#
# #### Sequence Separator
#
# By default, FriendlyId uses a dash to separate the slug from a sequence.
#
# You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
# sequence_separator} configuration option.
#
# #### Providing Your Own Slug Processing Method
#
# You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
# total control over the slug format. It will be invoked for any generated slug,
# whether for a single slug or for slug candidates.
#
# #### Deciding When to Generate New Slugs
#
# As of FriendlyId 5.0, slugs are only generated when the `slug` field is nil. If
# you want a slug to be regenerated,set the slug field to nil:
#
# restaurant.friendly_id # joes-diner
# restaurant.name = "The Plaza Diner"
# restaurant.save!
# restaurant.friendly_id # joes-diner
# restaurant.slug = nil
# restaurant.save!
# restaurant.friendly_id # the-plaza-diner
#
# You can also override the
# {FriendlyId::Slugged#should_generate_new_friendly_id?} method, which lets you
# control exactly when new friendly ids are set:
#
# class Post < ActiveRecord::Base
# extend FriendlyId
# friendly_id :title, :use => :slugged
#
# def should_generate_new_friendly_id?
# title_changed?
# end
# end
#
# If you want to extend the default behavior but add your own conditions,
# don't forget to invoke `super` from your implementation:
#
# class Category < ActiveRecord::Base
# extend FriendlyId
# friendly_id :name, :use => :slugged
#
# def should_generate_new_friendly_id?
# name_changed? || super
# end
# end
#
# #### Locale-specific Transliterations
#
# Active Support's `parameterize` uses
# [transliterate](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate),
# which in turn can use I18n's transliteration rules to consider the current
# locale when replacing Latin characters:
#
# # config/locales/de.yml
# de:
# i18n:
# transliterate:
# rule:
# ü: "ue"
# ö: "oe"
# etc...
#
# movie = Movie.create! :title => "Der Preis fürs Überleben"
# movie.slug #=> "der-preis-fuers-ueberleben"
#
# This functionality was in fact taken from earlier versions of FriendlyId.
#
# #### Gotchas: Common Problems
#
# FriendlyId uses a before_validation callback to generate and set the slug. This
# means that if you create two model instances before saving them, it's possible
# they will generate the same slug, and the second save will fail.
#
# This can happen in two fairly normal cases: the first, when a model using nested
# attributes creates more than one record for a model that uses friendly_id. The
# second, in concurrent code, either in threads or multiple processes.
#
# To solve the nested attributes issue, I recommend simply avoiding them when
# creating more than one nested record for a model that uses FriendlyId. See [this
# Github issue](https://github.com/norman/friendly_id/issues/185) for discussion.
#
# @guide end
module Slugged
# Sets up behavior and configuration options for FriendlyId's slugging
# feature.
def self.included(model_class)
model_class.friendly_id_config.instance_eval do
self.class.send :include, Configuration
self.slug_generator_class ||= SlugGenerator
defaults[:slug_column] ||= "slug"
defaults[:sequence_separator] ||= "-"
end
model_class.before_validation :set_slug
model_class.before_save :set_slug
model_class.after_validation :unset_slug_if_invalid
end
# Process the given value to make it suitable for use as a slug.
#
# This method is not intended to be invoked directly; FriendlyId uses it
# internally to process strings into slugs.
#
# However, if FriendlyId's default slug generation doesn't suit your needs,
# you can override this method in your model class to control exactly how
# slugs are generated.
#
# ### Example
#
# class Person < ActiveRecord::Base
# extend FriendlyId
# friendly_id :name_and_location
#
# def name_and_location
# "#{name} from #{location}"
# end
#
# # Use default slug, but upper case and with underscores
# def normalize_friendly_id(string)
# super.upcase.gsub("-", "_")
# end
# end
#
# bob = Person.create! :name => "Bob Smith", :location => "New York City"
# bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
#
# ### More Resources
#
# You might want to look into Babosa[https://github.com/norman/babosa],
# which is the slugging library used by FriendlyId prior to version 4, which
# offers some specialized functionality missing from Active Support.
#
# @param [#to_s] value The value used as the basis of the slug.
# @return The candidate slug text, without a sequence.
def normalize_friendly_id(value)
value = value.to_s.parameterize
value = value[0...friendly_id_config.slug_limit] if friendly_id_config.slug_limit
value
end
# Whether to generate a new slug.
#
# You can override this method in your model if, for example, you only want
# slugs to be generated once, and then never updated.
def should_generate_new_friendly_id?
send(friendly_id_config.slug_column).nil? && !send(friendly_id_config.base).nil?
end
# Public: Resolve conflicts.
#
# This method adds UUID to first candidate and truncates (if `slug_limit` is set).
#
# Examples:
#
# resolve_friendly_id_conflict(['12345'])
# # => '12345-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
#
# FriendlyId.defaults { |config| config.slug_limit = 40 }
# resolve_friendly_id_conflict(['12345'])
# # => '123-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
#
# candidates - the Array with candidates.
#
# Returns the String with new slug.
def resolve_friendly_id_conflict(candidates)
uuid = SecureRandom.uuid
[
apply_slug_limit(candidates.first, uuid),
uuid
].compact.join(friendly_id_config.sequence_separator)
end
# Private: Apply slug limit to candidate.
#
# candidate - the String with candidate.
# uuid - the String with UUID.
#
# Return the String with truncated candidate.
def apply_slug_limit(candidate, uuid)
return candidate unless candidate && friendly_id_config.slug_limit
candidate[0...candidate_limit(uuid)]
end
private :apply_slug_limit
# Private: Get max length of candidate.
#
# uuid - the String with UUID.
#
# Returns the Integer with max length.
def candidate_limit(uuid)
[
friendly_id_config.slug_limit - uuid.size - friendly_id_config.sequence_separator.size,
0
].max
end
private :candidate_limit
# Sets the slug.
def set_slug(normalized_slug = nil)
if should_generate_new_friendly_id?
candidates = FriendlyId::Candidates.new(self, normalized_slug || send(friendly_id_config.base))
slug = slug_generator.generate(candidates) || resolve_friendly_id_conflict(candidates)
send "#{friendly_id_config.slug_column}=", slug
end
end
private :set_slug
def scope_for_slug_generator
scope = self.class.base_class.unscoped
scope = scope.friendly unless scope.respond_to?(:exists_by_friendly_id?)
primary_key_name = self.class.primary_key
scope.where(self.class.base_class.arel_table[primary_key_name].not_eq(send(primary_key_name)))
end
private :scope_for_slug_generator
def slug_generator
friendly_id_config.slug_generator_class.new(scope_for_slug_generator, friendly_id_config)
end
private :slug_generator
def unset_slug_if_invalid
if errors.key?(friendly_id_config.query_field) && attribute_changed?(friendly_id_config.query_field.to_s)
diff = changes[friendly_id_config.query_field]
send "#{friendly_id_config.slug_column}=", diff.first
end
end
private :unset_slug_if_invalid
# This module adds the `:slug_column`, and `:slug_limit`, and `:sequence_separator`,
# and `:slug_generator_class` configuration options to
# {FriendlyId::Configuration FriendlyId::Configuration}.
module Configuration
attr_writer :slug_column, :slug_limit, :sequence_separator
attr_accessor :slug_generator_class
# Makes FriendlyId use the slug column for querying.
# @return String The slug column.
def query_field
slug_column
end
# The string used to separate a slug base from a numeric sequence.
#
# You can change the default separator by setting the
# {FriendlyId::Slugged::Configuration#sequence_separator
# sequence_separator} configuration option.
# @return String The sequence separator string. Defaults to "`-`".
def sequence_separator
@sequence_separator ||= defaults[:sequence_separator]
end
# The column that will be used to store the generated slug.
def slug_column
@slug_column ||= defaults[:slug_column]
end
# The limit that will be used for slug.
def slug_limit
@slug_limit ||= defaults[:slug_limit]
end
end
end
end
|