Générer des URLs sémantiques avec Rails 6, FriendlyId et CanCanCan

Pour une application utilisant Ruby on Rails 6, je devais générer des URLs sémantiques basées pour un modèle. Je voulais également que l’URL soit mise à jour en cas de modification, et rediriger l’ancienne version de l’URL si besoin. Contrainte supplémentaire, ce projet utilise la gemme CanCanCan pour la gestion des droits.

Voici le code que j’ai mis en place pour générer et mettre à jour des URLs sémantiques compatibles avec le chargement automatique de CanCanCan.

Tags : Ruby on Rails FriendlyId CanCanCan

Publié le 18 juillet 2020

Ajouter les gemmes au projet

bundle add friendly_id cancancan

Configurer les gemmes

Lancer la procédure d’installation des gemmes, et ajouter les nouvelles tables à la base.

bundle exec rails generate friendly_id
bundle exec rails generate cancan:ability
bundle exec rails db:migrate

Configurer le modèle

  • extend FriendlyId enrichit le modèle des méthodes et automatismes apportées par la gemme.
  • friendly_id :slug_candidates, use: [:slugged, :history] configure FriendlyId pour :
    • générer une url sémantique (un slug) en essayant les méthodes listées par slug_candidates,
    • générer des slugs uniques,
    • archiver les versions précédentes des slugs, pour permettre de les rediriger.
# app/models/my_model.rb
class MyModel < ApplicationRecord
  extend FriendlyId
  friendly_id :slug_candidates, use: [:slugged, :history]

  validates :name, presence: true

  private

  def slug_candidates
    [:name, [:name, :id]]
  end

  def should_generate_new_friendly_id?
    name_changed? || super
  end
end

À noter

La méthode slug_candidates doit renvoyer une liste de symboles correspondant à des méthodes ou des attributs permettant de générer un slug unique. Ici, le slug sera généré à partir du nom (:name), mais au cas où celui-ci serait déjà pris on ajoute l’identifiant de l’objet.

The should_generate_new_friendly_id? method is overridden to force slug generation when the underlying attribute changes. La méthode should_generate_new_friendly_id? est surchargée pour regénérer un slug quand le nom de l’enregistrement change. Par défaut, le slug n’est généré qu’à la création de l’objet.

Configurer CanCanCan dans les contrôleurs

CanCanCan encourage de vérifier systématiquement que l’autorisation d’accès a bien été contrôlée, et facilite cette vérification avec la commande check_authorization, qu’il est recommandé d’inclure dans le contrôleur de base (ApplicationController).

Pour charger automatiquement la ou les ressources identifiées par l’URL, inclure la commande load_and_authorize_resource. Si le visiteur a les droits, les ressources qui lui sont accessibles seront chargées, sinon une erreur CanCan::AccessDenied sera levée.

Les actions ne nécessitant pas de contrôle doivent le préciser explicitement (avec skip_authorization_check). Par exemple : page d’accueil, pages d’erreur customisées…

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  check_authorization

  rescue_from CanCan::AccessDenied do |exception|
    respond_to do |format|
      format.json { head :forbidden, content_type: 'text/html' }
      format.html { redirect_to main_app.root_url, notice: exception.message }
      format.js   { head :forbidden, content_type: 'text/html' }
    end
  end
end

Configurer FriendlyId dans les contrôleurs

Quand le module :slugged de FriendlyId est activé, Rails génèrera les URLs et chargera automatiquement les modèles à partir de leur slug, ce qui est très bien.

Nous devons cependant intervenir pour rediriger les anciens slugs et éviter les erreurs 404s. Pour cela, nous devons ajouter une méthode nommée load_or_redirect et l’appeler avant load_and_authorize_resource. Cette méthode n’est pas un point d’entrée correspondant à une URL, elle doit donc être privée.

Cette méthode chargera l’objet à partir de son slug, et redirigera si le slug a été modifié entretemps. Si une ressource a été chargée, la méthode load_and_authorize_resource ne s’occupe que de contrôler l’accès.

# app/controllers/my_models_controller.rb
class MyModelsController < ApplicationController
  # Redirect if a previous slug is used because FriendlyId's history module
  before_action :load_or_redirect, only: :show
  # Tell CanCanCan to populate @need and @needs
  load_and_authorize_resource

  # [Usual actions: index, new, show, edit, update, etc]

  private

  def load_or_redirect
    slug = params.fetch(:id)
    @my_model = MyModel.find_by(slug: slug)
    return unless @my_model.nil?

    @my_model = MyModel.find_by_friendly_id(slug)
    raise ActiveRecord::RecordNotFound if @my_model.nil?

    redirect_to @my_model, status: :moved_permanently, notice: "You have been redirected, please use the current URL and update your bookmarks."
  end
Page précédente Page précédente Page suivante Page suivante