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.
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 parslug_candidates
, - générer des slugs uniques,
- archiver les versions précédentes des slugs, pour permettre de les rediriger.
- générer une url sémantique (un
# 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