Création d'une application rubyonrails avec Devise CanCan et ActiveAdmin

Dans ce tutorial, nous partons du principe que vous connaissez déjà rubyonrails, nous utiliserons ici rails 3.1 et ruby 1.9.3 (à adapter pour rails 3 et rb 1.8).

Nous allons créer le début (vraiment la base de la base...) d'une plateforme de blog utilisant Devise, CanCan et ActiveAdmin

1) Création de notre application

rails new myblog

On ajoute dans le gemfile

gem 'devise'
gem 'cancan'
gem 'activeadmin'
## gem 'sass-rails' déjà dans le group assets
gem 'meta_search',    '>= 1.1.0.pre'

Hop un bundle install pour installer les gems manquants

bundle install

Génération du model users et roles

rails g model user username:string
rails g model role name:string

On va maintenant créer un model category pour nos categories d'articles

rails g model category title:string content:text

On va maintenant créer un model post pour nos articles, l'intérêt de references c'est

que lors des migrations rails génére des index en base de données sur la clé

étrangère créée, de plus le belongs_to est aussi ajouté (il faudrait dans l'idéal

ajouter le has_many à la main dans le model category

rails g model post title:string content:text category:references

Création de la table de jointure en (dans la migration du model role)

create_table "roles_users", :id => false do |t|
t.references :user, :role
end

Hop on migrationne... :-p

bundle exec rake db:migrate

Création du controller public avec action index

rails g controller public index

Mettre l'action index de public comme page racine root dans config/routes.rb

root :to => "public#index"

On renomme le fichier /assets/images/index.html

mv public/index.html public/_index.html

On teste notre application

rails s

Dans une autre console

firefox localhost:3000

vous devriez voir ça :

Public#index

Find me in app/views/assets/images/index.html.erb

2) Installation de ActiveAdmin

rails generate active_admin:install
bundle exec rake db:migrate

ReDémarrez, sinon ça marchera pas. (donc ctrl + c puis rails s)

rails s

On lance un navigateur web et on constate que le style n'est plus tout à fait le même (à cause d'active_admin)

firefox localhost:3000

Ensuite on essaye ActiveAdmin, et wouahou le super écran de login, terrible...

login / password : admin@example.com / password

firefox localhost:3000/admin

Note : A savoir que ActiveAdmin utilise Devise

Maintenant on veut voir nos données (posts et catégories) dans ActiveAdmin

rails generate active_admin:resource post

Et

rails generate active_admin:resource category

Deux fichiers sont créés (app/admin/posts.rb et app/admin/categories.rb), ils permettent de configurer l'affichage et les filtres pour ActiveAdmin

Démarrage du serveur, s'il ne l'ai pas, et vous pouvez jouer (localhost:3000/admin)

rails s

On est francais hein, alors en francais c'est mieux hein ! On copie depuis Github le fichier de langue

cd config/locales && wget https://raw.github.com/gregbell/active_admin/master/lib/active_admin/locales/fr.yml

On renomme le fichier de active_admin, sinon la prochaine commande va l'écraser, (en fait, par

convention, le nom du fichier fini par quelquechose.LG.yml mais ça n'a pas d'importance car rails voit

dans le fichier yml "fr:" ou "en:" donc il sait de quelque langue il s'agit.)

mv config/locales/fr.yml config/locales/active_admin.fr.yml

Et on fait la même chose pour rails (format des dates, etc...)

cd config/locales && wget https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/fr.yml

Et on dit que notre application est en francais, dans config/application.rb on mets :

config.i18n.default_locale = :fr

Hop on redémarre le serveur

rails s

Et on teste, wouhaou mais c'est en Francais !

firefox localhost:3000/admin

Bon au login il y a une petite erreur (d'affichage : "translation missing: fr.devise.sessions.user.signed_in"),

en fait le fichier locale qu'on a récupéré est uniquement pour active_admin et non pour devise, donc voici comment faire :

cd config/locales && wget https://raw.github.com/diaspora/diaspora/master/config/locales/devise/devise.fr.yml

Puis redémarrez le serveur :

rails s

3) Installation de devise

Du fait que ActiveAdmin utilise devise, on saute cette étape :

Sinon vous auriez du faire le

1) root :to => "mon_controller#mon_action" dans config/routes si ce n'était pas fait

2) mettre

<%= notice %>

<%= alert %>

dans la vue ou le layout

3) En console : rails generate devise:install # <-- ça c'est donc l'étape qu'on saute

Maintenant on install devise pour notre model user

rails generate devise user

Des routes ont été créées pour gérer le sign_in sign_up, sign_out, mot de passe perdu, etc...

Le model a été marqué comme utilisant devise :

## Include default devise modules. Others available are:
## :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable

Une migration a été créée pour le model user (ajoute les champs nécessaires au bon fonctionnement de devise).

Vous pouvez aller voir la doc de devise pour gérer des composants : https://github.com/plataformatec/devise

On effectue la migrabilitée

bundle exec rake db:migrate

Vous pouvez voir à quoi ressemble notre table users dans db/schema.rb :

create_table "users", :force => true do |t|
t.string   "username"
t.datetime "created_at"
t.datetime "updated_at"
t.string   "email",                                 :default => "", :null => false
t.string   "encrypted_password",     :limit => 128, :default => "", :null => false
t.string   "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer  "sign_in_count",                         :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string   "current_sign_in_ip"
t.string   "last_sign_in_ip"
end

Redémarrer le serveur

rails s

Vous pouvez accéder à quelques vues, offerte par devise :

firefox http://localhost:3000/users/sign_in
firefox http://localhost:3000/users/sign_out
firefox http://localhost:3000/users/password/new

Pour voir toutes les nouvelles vues :

bundle exec rake routes | grep /users

ça donne ça :

          new_user_session GET        /users/sign_in(.:format)             {:action=>"new", :controller=>"devise/sessions"}
user_session POST       /users/sign_in(.:format)             {:action=>"create", :controller=>"devise/sessions"}
destroy_user_session DELETE     /users/sign_out(.:format)            {:action=>"destroy", :controller=>"devise/sessions"}
user_password POST       /users/password(.:format)            {:action=>"create", :controller=>"devise/passwords"}
new_user_password GET        /users/password/new(.:format)        {:action=>"new", :controller=>"devise/passwords"}
edit_user_password GET        /users/password/edit(.:format)       {:action=>"edit", :controller=>"devise/passwords"}
PUT        /users/password(.:format)            {:action=>"update", :controller=>"devise/passwords"}
cancel_user_registration GET        /users/cancel(.:format)              {:action=>"cancel", :controller=>"devise/registrations"}
user_registration POST       /users(.:format)                     {:action=>"create", :controller=>"devise/registrations"}
     new_user_registration GET        /users/sign_up(.:format)             {:action=>"new", :controller=>"devise/registrations"}
    edit_user_registration GET        /users/edit(.:format)                {:action=>"edit", :controller=>"devise/registrations"}
                           PUT        /users(.:format)                     {:action=>"update", :controller=>"devise/registrations"}
                           DELETE     /users(.:format)                     {:action=>"destroy", :controller=>"devise/registrations"}

D'une manière générale, si vous souhaitez voir les routes

bundle exec rake routes

Essayez de vous inscrire sur https://localhost:3000/users/sign_up

firefox http://localhost:3000/users/sign_up

Vous êtes enregistré et logué, une preuve ?

Accéder à https://localhost:3000/users/sign_up et vous serez redirigé sur "/".

Bon là le problème c'est que l'on peut pas se délogué, on va donc ajouter un bouton pour la vue

"public#index", ouvrons donc le fichier "app/views/assets/images/index.html.erb" et ajoutez-y ceci :

<p>
<%= link_to "Déconnexion",  destroy_user_session_path, :method => :delete %>
</p>

Lancer le navigateur et déconnecté vous, vous avez peut-être l'impression

qui ne s'est rien passé et pourtant vous pouvez de nouveau accéder à https://localhost:3000/users/sign_up

Pour que les instructions envoyées par mail fonctionne il faut définir l'url

dans le fichier config/environments/development.rb (il faudra aussi le faire en production.rb)

config.action_mailer.default_url_options = { :host => 'localhost:3000' }

On Redémarre le serveur :

rails s

Bon vous avez oublié votre mot de passe, mais si je vous le dis ! Allez hop on test :

firefox http://localhost:3000/users/password/new

là vous devriez avoir, reçu votre mot de passe, mais en fait non, il faut configurer actionmailer dans rails :

ici il s'agit d'une config qu'il faudra certainement adapter pour vous.

Ajouter le fichier config/initializers/email.rb et mettez dedans (enfin pas bêtement configurez ça comme il faut) :

ActionMailer::Base.smtp_settings = {
:address              => "smtp.gmail.com",
:port                 => 587,
:domain               => "mondomaine.com",
:user_name            => "monidentifiant",
:password             => "monmotdepasse",
:authentication       => "plain",
:enable_starttls_auto => true
}

Redémarrer le serveur

rails s

Bon réessayez, chez moi ça marche en tout cas (ah bon c'est pas un argument ?)

Vérifiez que ça n'a pas atterri dans vos spams. Cliquez sur le lien "Change my password"

http://localhost:3000/users/password/new

Tout ça c'est bien !

Bon mais les vues sont en anglais, et nous avions décidé de tout mettre en francais, alors on va générer les vues :

rails generate devise:views

Maintenant vous traduisez devise à la main (les vues se trouvent dans app/views/devise/... )

Bon devise avec le model admin_user d'ActiveAdmin ne nous intéresse pas vraiment nous avons notre model user pour faire le travail.

Pour ne plus utiliser le devise d'ActiveAdmin (ou en tout cas outrepasser l'authentification, il suffit de commenter les lignes suivantes dans le fichier config/initializers/active_admin.rb :

config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user

A ce stade, si vous redémarrez votre serveur vous constaterez qu'il n'y a plus de système

d'authentification pour accéder à localhost:3000/admin

Bon par contre nous voulons quand même une authentification avec notre installation personnelle de devise et notre model user

Donc les lignes ci-dessus vous pouvez les remplacer par celles-ci (ce sont des méthodes générées par devise : "authenticate_mon_model!" et "current_mon_model") :

config.authentication_method = :authenticate_user!
config.current_user_method = :current_user

Il faut ajouter une ligne dans config/initializers/active_admin.rb à la section # == Logging Out

En dessous de ceci (la ligne est commenté) :

## config.logout_link_path = :destroy_admin_user_session_path

Ajoutez ceci :

config.logout_link_path = :destroy_user_session_path

[EDIT] Et voir ici un autre truc à rajouter : https://twitter.com/petitchevalroux/status/237873578815680513

Bon et du coup vous pouvez par la même occasion commenter les routes devise_for :admin_users dans config/routes.rb et ajouter la config pour user de la sorte :

devise_for :users
## devise_for :admin_users, ActiveAdmin::Devise.config

C'est bien, on avance prochaine étape c'est l'installation de CanCan et cablage avec Devise et ActiveAdmin.

4) Installation de CanCan

Génération du model de gestion des permissions (ability) c'est là dedans que l'on va définir les droits des utilisateurs

rails g cancan:ability

Vous pouvez voir la doc de CanCan : pour savoir quoi mettre dans ability : https://github.com/ryanb/cancan/wiki/Defining-Abilities

Pour résumer : avec CanCan, un model ability permet de voir si l'on a les droits à une ressource. (voir les prochaines lignes de code)

Il faut un before filter ("authorize_resource", "load_and_authorize_resource" ou bien encore directement dans la méthode "authorize!")

dans le controller ou bien dans l'action pour dire à CanCan que l'on veut gérer les droits pour cette action / controller.

Sinon la vérification des droits pour l'utilisateur courant ne se fait pas.

Je vous conseille de TOUT mettre sous le controle de CanCan (et dans ability vous gérez le cas où

user est nil, comme ça pas d'oublie) et justement pour que CanCan vous avertisse d'un oubli vous mettez

dans votre controleur la méthode "check_authorization" qui ne manquera pas de vous signalez qu'une

action n'est pas sous contrôle de CanCan (ou bien dans application_controller si tout est sous control CanCan)

Bon maintenant que tout est expliqué pour CanCan on va ajouter les contrôles pour les posts (billet du blog).

L'idéal c'est que les utilisateurs puissent manager leurs posts (=billets).

Pour tester CanCan dans ActiveAdmin, on va interdire de manager le post qui a pour id 2, pour cela je vous laisse créer 3 posts (du coup id 1,2 et 3)

Ensuite pour vérifier les authorisations éditez le fichier : app/admin/posts.rb, vous noterez qu'il y a un

bug avec load_and_authorize_resource et index, ça viendrait d'un problème entre CanCan load_resource et kaminari donc patience.

ActiveAdmin.register Post do
# Pour CanCan
# controller.load_and_authorize_resource # Ne fonctionne pas pour index
controller.authorize_resource
end

Et dans ability.rb, le corps de la méthode initialize :

user ||= User.new # guest user (not logged in)
if user.nil? then
## Droit à rien, non mais...
else
#can :manage, :all
can :manage, Post,      :id => 2
can :manage, Category,   :id => 2
end

Pas besoin de relancer le serveur, essayez de modifier le post numéro 1 ou 3, ça devrait échoué !

Bon maintenant on a nos utilisateurs et on veut associer les posts à l'utilisateur courant (sans le montrer dans le formulaire ni en hidden (car ça serait modifiable)).

rails g migration add_user_id_to_posts

Donc dans la méthode "change" on mets ceci :

add_column :posts, :user_id, :integer
add_index  :posts, :user_id

On migrabilite...

bundle exec rake db:migrate

Maintenant ça se complique un peu, je veux masquer la relation de user à post dans le

formulaire ActiveAdmin et je veux que le user_id corresponde au current_user de plus je

souhaite récupérer le blog en train d'être administré et quasiment tout scoper sur blog (on veut faire un plateforme multi-blog)

Il existe un méthode scope_to qui permet de scoper par défaut les models dans ActiveAdmin (Attention c'est différent de scope :all)

Donc dans app/admin/posts.rb, ajoutez ceci pour scoper sur blog :

scope_to :current_blog

Et pour associer l'utilisateur et le blog pour chaque post (toujours dans app/admin/posts.rb) :

after_build do |post|
post.user_id = current_user.id
post.blog_id = current_blog.id
end

Bon ensuite il faut définir la méthode current_blog appelée, donc dans ApplicationController (app/controller/application_controller.rb):

def current_blog
return Blog.where(:url => request.env['HTTP_HOST']).first
end

Ce serait bien d'avoir un model blog en fait :

rails g model blog name:string url:string user:references
rails g migration add_blog_id_to_posts

Créez la colonne blog_id dans posts dans la migration précédemment créée :

add_column :posts, :blog_id, :integer
add_index  :posts, :blog_id

On effectue la migratitude :

bundle exec rake db:migrate

Et il faut dire à user qu'il a plusieurs blogs (app/model/user.rb) :

has_many :blogs

Et à post qu'il appartient à un blog (app/model/post.rb) :

belongs_to :blog

Et dans blog.rb :

has_many :posts

Ensuite en console créons le blog :

Blog.new(:user_id => 1, :name => "Mon blog", :url => "localhost:3000").save

Jusque là aucun soucis

Essayons maintenant, créez, filtrez, etc... :

firefox localhost:3000/admin/posts

Bon le problème c'est que dans le formulaire les champs "user" et "blog" apparaissent et nous ne nous

le voulons pas. Nous allons donc trouver un moyen de virer ces champs.

Bon pour les filtres en cherchant un peu, on trouve, voici comment procéder (dans app/admin/posts.rb):

(Post.column_names.map{ |c| c.to_sym } - [:blog_id, :user_id]).each{ |field|
filter field
}

Bon et pour le formulaire voici le code toujours dans (app/admin/posts.rb)

form do |f|
f.inputs "Posts" do
f.input :title
f.input :content
f.input :category
end
f.buttons
end

Dans le show, pour les relations belongs_to, active_admin fait un appel à la méthode name sur le model lié : @post.blog.name @post.user.name

donc vous pouvez faire ceci dans app/model/user.rb :

def name
return email
end

De cette manière l'email s'affichera, sinon une autre solution consiste à redéfinir show (https://activeadmin.info/docs/6-show-screens.html)

Bon l'idéal ici serait de mettre un éditeur W(what)Y(you)S(see)I(is)W(what)Y(you)G(get) pour le contenu du post.

Bon en fait je vais vous faire installer un éditeur mais pas vraiment WYSIWYG : markitup

Je ne suis pas fan du WYSIWYG pourquoi ? Parce que le client pête toute la charte graphique en faisant du site un sapin de noël

(choix des couleurs, surlignement, taille, font).

Pour installer markitup je vous invite à suivre cet article : https://blog.escarworld.com/post/2011/09/26/Pr%C3%A9parer-MarkItup-pour-rails-3.1

De la même manière vous pouvez décider de modifier "show" pour afficher le markdown / textile / bbcode converti en html (les gems à utiliser pour les conversions vers html sont listés sur le lien ci-dessus).

Je pense qu'on a fait le tour vous allez peut-être me dire que ce n'est pas terminé, je ne souhaitai

pas coder une application entière mais essayer de d'anticiper les problèmes potentiels avec ActiveAdmin.

Si vous avez quelques questions je m'efforcerai d'y répondre, si vous remarquez des erreurs/fautes n'hésitez pas !

Maintenant, il vous appartient de choisir entre tout refaire à la main (long mais customizable) ou d'utiliser ActiveAdmin (bien fait et déjà fait, mais pas pleinement personnalisable). Qu'en pensez-vous ?

EDIT : myblog sources