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