vendredi 19 juin 2009

Haml, Formtastic et Authlogic


Deuxième partie couvrant l'installation de deux gems usuelles (haml et formtastic) et la mise en place du traitement des autorisations d'accès au site (avec la librairie authlogic).

Prochaine partie : will_paginate, tinyMCE, acts_as_versionned pour un début de wiki home made.




1. Préparation
Deux librairies Haml et Formtastic sont installées au départ car elles sont utilisées en parmanence dans la suite du projet.

Haml permet de simplifier et clarifier les vues html, il s'installe simplement par :
>gem install haml
>haml --rails path/to/rails/app

Formtastic est là aussi pour simplifier la génération des formulaires de saisie (la seconde commande génére 2 fichiers css pour la mise en forme des formulaires) :
>gem install justinfrench-formtastic
>ruby script/generate formtastic_stylesheets

Deux choix pour l'authentification : restfull_authenticate et authlogic. J'ai choisi le second car il me semblait plus simple et plus évolutif. NB : ne pas confondre authentification (identifier un utilisateur) et authorisation (identifier les droits d'un utilisateur).
Donc :
>gem install authlogic

Puis modification du fichier de configuration pour inclure les gems (fichier /config/environment.rb) :
Rails::Initializer.run do |config|
config.gem "authlogic"
config.gem "justinfrench-formtastic", :lib =>'formtastic', :source =>'http://gems.github.com'
config.active_record.timestamped_migrations = false

La dernière ligne permet de raccourcir le nom des fichiers de migrations (comme avec Rails 1.x).


2. Création du model User
Une façon simple de commencer avec authlogic est de partir de son exemple de base mais pour comprendre un peu plus le fonctionnement j'ai refait le boulot (DRY oui, mais une fois qu'on a compris :) ... ).

Création d'un model utilisateur avec les champs nécessaires à authlogic : comme on se servira des vues de bases au démarrage, on lance un scaffold avec les champs principaux :
>ruby script/generate scaffolf user username:string
lastname:string login:string password:string email:string

et modifier ensuite le fichier de migration généré (dans /db/migrate) comme suite :

class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :username, :email, :ident, :null => false
t.string :lastname, :last_login_ip, :current_login_ip
t.string :crypted_password, :password_salt, :persistence_token
t.datetime :last_request_at, :last_login_at, :current_login_at
t.integer :login_count, :failed_login_count
t.string :perishable_token, :default => "", :null => false
t.boolean :remember_me
t.timestamps
end

add_index :users, :ident
add_index :users, :persistence_token
add_index :users, :last_request_at
add_index :users, :email

u=User.new
u.password='admin'
u.password_confirmation = 'admin'
u.password_salt
u.id = 1
u.username = 'Admin'
u.ident = 'red'
u.email = "admin@red.net"
u.save
end

def self.down
drop_table :users
end

end

Deux points sur ce fichier : j'ai ajouté un premier utilisateur pour éviter d'avoir à le faire après chaque migration et pour pouvoir tester tout de suite avec un login correct (à ne pas faire en production). Attention, la migration ne pourra se faire qu'une fois complétée le modèle user. J'ai rajouté un champ "ident" qui servira pour la connexion. Les autres champs sont remplis automatiquement par authlogic. Les autres champs sont automatiquement interprétés et utilisés par le plugin (email, password, etc : voir les MagicColumns dans la doc.).

Définition du model app/models/user.rb pour prendre en compte authlogic (le champs utilisé pour le login est spécifié ici, voir la doc pour les options supplémentaires) :
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.login_field = :ident
end
end

Et petite modification du controller app/controller/users_controller.rb afin de restreindre l’accès aux utilisateurs connectés :
class UsersController < ApplicationController
before_filter :require_user
...

Les vues standards (index, edit, new, show : CRUD) ont été crées aussi : dans /app/views/users avec une structure de base fonctionnelle.
La route à été ajoutée au fichier config/routes.rb
map.ressources users


3. Création de la session

Les sessions permettent à un utilisateur de naviguer sur le site en étant authentifié. On peut les stocker en tant que fichiers ou bien dans la base de données. Ayant choisit la 2ème possibilité il faut donc modifier la configuration config/environment.rb et rajouter une ligne :
Rails::Initializer.run do |config|
...
config.action_controller.session_store = :active_record_store

Il faut ensuite définir le le modèle, le controleur et la migration:
>ruby script/generate session user_session
>ruby script/generate controller user_sessions
>ruby script/generate session_migration


Le modèle a besoin d’être créé simplement : app/views/model/user_session.rb
class UserSession < Authlogic::Session::Base
End

Il faut implémenter une méthode pour se connecter et pour se déconnecter (new et destroy) dans user ( attention au pluriel de UserSessions ! regardez dans la doc les méthodes pluralize et singularize).
Implémentation des fonctions login et logout dans user_sessions_controller.rb :
class UserSessionsController < ApplicationController
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => :destroy
def new
@user_session = UserSession.new
end

def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Login successful!"
redirect_to root_url
else
render :action => :new
end
end

def destroy
current_user_session.destroy
flash[:notice] = "Logout successful!"
redirect_to root_url
end

end

Les before_filter sont explicites et restreignent donc l’accès (comme pour le controller User).
A noter aussi l'appel d'une fonction encore non définie :current_user_session qui fait partie d'un ensemble de quelques fonctions très utiles pour la persistence d’une session. Elles sont définies directement dans application_controler.rb qui devient :
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details

# Scrub sensitive parameters from your log
helper_method :current_user_session, :current_user
filter_parameter_logging :password, :password_confirmation

private
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end

def current_user
# return @current_user if defined?(@current_user)  equivalent au ||= en-dessous
@current_user ||= current_user_session && current_user_session.record
end

def require_user
unless current_user
store_location
flash[:notice] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end

def require_no_user
if current_user
store_location
flash[:notice] = "You must be logged out to access this page"
redirect_to login_url
return false
end
end

def store_location
session[:return_to] = request.request_uri
end

def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
end
(copier/coller du tutorial authlogic).
La méthode current_user est appelée très souvent par la suite : pour retrouver un utilisateur déjà connecté la fonction principale est UserSession.find.

Pour le fichier routes.rb on aura finalement :
#   map.login  'login',   :controller => 'user_sessions', :action => 'new'
map.logout 'logout', :controller => 'user_sessions', :action => 'destroy'
map.signup 'signup', :controller => 'users', :action => 'new'
map.resource :user_session
map.resources :users
map.root :controller =>"users", :action => "index"


La difference entre map.resources et map.resource est bien expliquée dans le guide Rails ; on fixe map.root sur un page simple, en attendant mieux.
Il reste à intégrer le mécanisme d’authentification dans le controller (et les prochains controller devront aussi avoir cette modification) :
class UsersController < ApplicationController
before_filter :require_user



4. Création des vues

C’est maintenant que haml et formtastique vont être utiles … rendez-vous sur leur doc respective pour plus de détail ou dans les commentaires :)
Création de la page de login app/view/user_sessions/new.html.haml:
#bloc_login
%h1 Red
- semantic_form_for @user_session, :url => user_session_path do |f|
- f.inputs do
= f.input :ident, :label => 'Login'
= f.input :password
= f.input :remember_me, :required => false, :as => :boolean
- f.buttons do
= f.commit_button " Login "


Création d’un bloc header qui sera present sur toutes nos pages (fichier app/view/shared/_head.html.haml à créer) :
#user_nav
= "( "+pluralize(User.logged_in.count, "user") + " currently logged in )"
- if current_user
= link_to current_user.ident, current_user
= link_to "Logout", logout_path
- else
= link_to "Register", signup_path
= link_to "Login", login_path
= link_to "Lost Password", "password_reset_path"


Et integration du partiel dans un fichier maquette unique pour l’ensemble du site : app/view/layout/application.html.haml (attention, les script/generate génèrent un layout associé au controller, donc il faut les supprimer sinon ils seront employés : le layout application n’est utilisé que si le layout du controleur est absent.)
!!!
%html
%head
%title Red
= stylesheet_link_tag 'scaffold'
= stylesheet_link_tag 'formtastic'
= stylesheet_link_tag 'red'
= stylesheet_link_tag 'login' if not current_user
= javascript_include_tag :defaults
%body
= render :partial => 'shared/head' if current_user
%p.flash_notice= flash[:notice]
= yield


Je vous livre toutes les vues User ce-dessous. A noter quelques astuces : en haml quand il faut poursuivre une commande sur plusieurs ligne, il faut ajouter une barre verticale | à la fin de toutes les lignes qui composent la commande ; toujours en haml, pour insérer du texte qui ne sera pas interprété (pratique pour tester vite fait un style css) il faut faire un bloc commençant par :plain. Par exemple :
...
%style{:type => "text/css"}
:plain
.imp{padding-left:20px;}
.imp.i1{background-image:url(exclamation.png);color:red;}
.imp.i2{background-image:url(information.png);}
%ol#event_list{:style=>'list-style:none'}
...

Pour les partial, il faut penser que le nom du fichier est égal à la variable passée au partial ... Ca sera plus clair dans un prochain exemple. Le nom du fichier est égal au nom du partial précédé d’un tiret bas : @user -> _user
Voici les vues :
app/views/users/index.html.haml
%h1 User
%h3 Liste User
%table.table_index
%thead
%tr
%th Nom
%th Prenom
%th Login
%th Email
%th
%tbody
- @users.each do |u|
%tr{ :class => cycle('even', 'odd')}
%td= u.username
%td= u.lastname
%td= u.ident
%td= u.email
%td
= link_to 'show', u
= link_to 'edit', edit_user_path(u)
= link_to 'delete', u, :confirm => 'Are you sure?', :method => :delete
%br
= link_to 'New user', new_user_path

app/views/users/show.html.haml
 %h1 User
%h3 Show User
- ['username','lastname','email','ident','login_count','last_request_at', |
'last_login_at','current_login_at','last_login_ip','current_login_ip'].each do |f| |
%p
%b= f.humanize
= @user[f]
%br
= link_to 'Edit', edit_user_path
|
= link_to 'Back', users_path

app/views/users/edit.html.haml
 %h1 Edit User
%h3{:style => 'border-bottom: 1px solid #700E27'} Edit User
#bloc_form
= render @user
%br
= link_to 'Show', @user
|
= link_to 'Back', users_path

app/views/users/new.html.haml
 %h1 User
%h3 New User
#bloc_form
= render @user
%br
= link_to 'Back', users_path

app/views/users/_user.html.haml
 - semantic_form_for @user do |f| 
= f.error_messages
- f.inputs do
= f.input :ident, :label => "Login", :input_html => {:size => 20}
= f.input :username, :label => "Nom"
= f.input :lastname, :label => "Prenom", :required => false
= f.input :email
= f.input :password, :required => false
= f.input :password_confirmation, :required => false
- f.buttons do
= f.commit_button


Les fichiers css vite fait : (/public/stylesheets/sass/red.sass) :NB : tous les fichiers *.sass contenus dans ce répertoire (configurable, voir la doc) sont transformés en fichier css automatiquement.
 body
:margin 0
:padding 0
#content
//:width 60%
:margin 20px
h3
:border-bottom 1px solid #700E27
#bloc_form
:margin-left 20px
input, select,textarea
:border 2px solid #DBCCB6
form.formtastic fieldset ol li
:display block
// -----------------head
#head
:background-color #700E27
:font-size 0.9em
:margin-bottom 10
:height 34px
:color white
#logo
:float left
:font-family 'Jokerman'
:font-size 2.2em
:padding 6px 0 0 10px
#user_nav
:float right
:padding 5px 20px 5px 0
#head * a
:font-weight bold
:padding 10px 3px 10px 3px
&:link
:color white
:text-decoration none
&:hover
:background-color #A00
:border-bottom 3px red solid
&:visited
:color white
:text-decoration none
// ------------------table des index
.table_index
:border 1px solid #DBCCB6
:border-spacing 0
:padding 0
thead
:background-color #A00
:margin 0
:padding 0
:color #FFF
:font-weight bold
:text-decoration none
th, td
:margin 0
:padding 2px
tr.odd
:background-color #EEE
tbody > tr:hover
:background-color #FFD

Et le fichier login.sass :
 // -----------------login
body
:background-color #700E27
:color white
#bloc_login
:width 600px
:margin 0 auto 0 auto
:padding-top 170px
h1
:font-family 'Jokerman'
:font-size 96pt
:text-align center
fieldset
:border 0
ol
:list-style none
li
:margin-bottom .8em
input
:width 40%
#user_session_submit
:width 100%
label
:display block
:text-align right
:width 40%
:float left
:padding-top .2em
:margin-right 20px


Et c’est le moment de tester (pour plus d’info sur rake faite >rake –T ):
>rake db :create
>rake db :migrate
>mongrel_rails start –p 80


Ca roule chez moi, mais j’ai pu louper un truc en écrivant le tutorial. N’hésitez pas à intervenir dans les commentaires.

TODO :
- configuration de l’envoi d’un email d’inscription
- perte de password





2 commentaires:

  1. Merci pour ce tutorial synthétique.
    Pour info, si vous avez un problème pour installer formtastics, il faut vraisemblablement ajouter github comme source de gems pour le programme gem, avec la commande : gem sources -a http://gems.github.com

    RépondreSupprimer
  2. bonnes explications pour commencer avec ces 2 plugins...(authlogic et formtastic) MAIS
    les problèmes commencent avec la validation des données du formulaire :
    - Authlogic valide tout ce qui sert à l'authentification (login, email, password, confirmation du password)
    - Formtastic utilise les validations indiquées dans le modèle, mais semble oublier celles effectuées par Authlogic.... ?

    RépondreSupprimer