Débuter une application avec Catalyst

user_icon admin | icon2 Catalyst | icon4 24/2/2008 21h16| Type doc: article| Type File: txt| icon3 6 Comments

Pour Touns: Cette doc n'est pas terminée, mais elle permet de débuter avec Catalyst

Construire une application avec Catalyst


1. Le plan

L'objectif de ce document est de décrire par étapes successives la création d'une application avec Catalyst. Il s'agit de créer une simple interface de gestion documentaire. Par simple j'entend qu'il lui faudra uniquement gérer le nom des documents et de leurs auteurs. Voyons voir le déroulement du processus :

  • Définition du Modèle et des constructeurs

  • Création de l'ébauche

  • Gestion des utilisateurs et de leurs roles

  • Gestion des documents

Après ces étapes il sera sans doute possible d'y ajouter un workflow, l'enregistrement des documents, ...

2. Définition du Modèle et des constructeurs

Je rappelle que le Modèle stocke les données et les constructeurs contrôlent le flux applicatif.

Penchons nous dabord sur le cas des utilisateurs. Il leur faudra un login, un mot de passe, nom, prénom et leur assigner un role. Le Role de l'utilisateur lui donnera un certain statut (admin/auteur/lecteur...) Pour manipuler ces données nous devrons disposer d'un modèle reflétant leurs structures. Le modèle n'étant pas necéssairement une base de données, cela aurait pu être un ldap, un structure de fichiers, ..., on appelle cela le MVC. Le Modèle est le seul point d'entrée vers les données, il est donc facilement interchageable, Les Controleurs se contenterons de les manipuler et les Vues de les afficher.

Puisque nous devons assigner un rôle aux utilisateurs, une table 'role' sera créée. Notons aussi qu'un utilisateur pourra avoir plusieurs roles, la relation entre les deux se fera par une table de jointure (user_role).

Voilà à quoi notre structure va ressembler ( il s'agit du fichier ' /tmp/db.sql' ):


-- Users
CREATE TABLE user (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(30) NOT NULL,
        password VARCHAR(40) NOT NULL,
        firstname VARCHAR(40),
        lastname VARCHAR(40)
);

-- Roles


CREATE TABLE role (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(30)
);

-- Mapping
CREATE TABLE user_role (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        user INTEGER REFERENCES user,
        role INTEGER REFERENCES role

);

On lui donne quelques données à grignoter que nous pourrons visualiser lors des prochaines étapes.

-- Users (pass: 12345)
INSERT INTO user VALUES (1, 'admin', '12345','Robert','Dupont');

INSERT INTO user VALUES (2, 'user2', '12345','Gaston','Lagaffe');

INSERT INTO user VALUES (3, 'user3', '12345','Luky','Luke');

INSERT INTO user VALUES (3, 'user4', '12345','Jean','Martin');

-- Roles

INSERT INTO role VALUES (1, 'admin');
INSERT INTO role VALUES (2, 'auteur');

INSERT INTO role VALUES (3, 'lecteur');
INSERT INTO role VALUES (4, 'validateur');

-- User Roles
-- chacun son role

INSERT INTO user_role VALUES (1, 1, 1);
INSERT INTO user_role VALUES (2, 2, 2);
INSERT INTO user_role VALUES (3, 3, 3);

INSERT INTO user_role VALUES (4, 4, 4);

Les documents, ou du moins leurs représentations, seront définis par les paramètres : son nom et son statut. Le status sera traité plus tard.

On pourra par la suite leur ajouter une date de création, validation ... Pour en connaitre l'auteur, le(s) lecteurs/approbateurs/validateurs nous passerons encore une fois par une table intermédiaire (user_doc)

En voilà le SQL:

CREATE TABLE doc (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        nom VARCHAR(30) NOT NULL,
        statut VARCHAR(30) NOT NULL,

)


CREATE TABLE user_doc (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        user INTEGER REFERENCES user,
        doc INTEGER REFERENCES doc
)


- Docs

INSERT INTO doc VALUES (1, 'Mon premier document');
INSERT INTO doc VALUES (2, 'Mon second document');

INSERT INTO doc VALUES (3, 'Mon second document');

-- Users Docs
-- doc1 : user2 et user3
INSERT INTO user_doc VALUES (1, 2, 1);

INSERT INTO user_doc VALUES (2, 3, 1);

-- doc2 : user2 (auteur)
INSERT INTO user_doc VALUES (3, 2, 2);

-- doc3 : user3 (lecteur) / user4 (validateur)
INSERT INTO user_doc VALUES (3, 3, 3);

INSERT INTO user_doc VALUES (3, 4, 3);

Le Modèle étant défini, il est temps de penser à la manière de gérer les flux applicatifs et donc les controleurs.

Tout dabord lorsqu'un utilisateur se connecte sur le site il doit s'identifier. Il est redirigé vers la page '/login' si ce n'est pas le cas.

Lorsqu'il est authentifié, il pourra en fonction du son role, ajouter , approuver et/ou valider un/des document(s).

Nous aurons donc le controleur 'doc' accédé par l'url '/doc' avec des méttodes d'ajout, de suppression, lecture

Si l'utilisateur connecté possède le role 'admin', il peut ajouter/supprimer/editer les 'utilisateurs', 'roles' et 'documents'.

Le décord est maintenant posé ...

3. Création de l'ébauche

L'application se nommera 'Docs', on la créée immédiatement.


$ catalyst.pl Docs
$ cd Docs

Le SQL du chapitre précédent sera copié dans le fichier 'db/db.sql. Une base de donnée SQLite sera créée.

$ mkdir db
$ cp /tmp/db.sql db/
$ sqlite3 db/docs.db < db/db.sql

LE MODELE :

Ici la magie de Catalyst apparait, On va lui faire découvrir automatiquement les tables et les relations entre elles.


$ perl script/docs_create.pl model DB DBIC::Schema DB::Schema create=static dbi:SQLite:db/docs.db

Mais que s'est-il passé ?

Le répertoire lib/DB est né et le fichier ./lib/Docs/Model/DB.pm a été créé. Jetons un oeil à ce dernier.

package Docs::Model::DB;

use strict;
use base 'Catalyst::Model::DBIC::Schema';

__PACKAGE__->config(
    schema_class => 'DB::Schema',
    connect_info => [
        'dbi:SQLite:db/docs.db',

    ],
);

Catalyst ( et DBIx ) à créé la classe Docs::Model::DB nécessaire à la configuration de notre base de donnée. Si nous utilisions une autre base de donnée ( et de nombreuses sont reconnues ) c'est ici que la modification devrait s'effectuer ( connect_info ).

La clé ' schema_class' indique quel est le schema à utiliser. Celui-ci doit être en concordance avec la déclaration de la base.

Dans notre cas la classe du schema est lib/DB/Schema.pm ( car je l'ai décidé lors de l'exécution du doc_create.pl )


package DB::Schema;

# Created by DBIx::Class::Schema::Loader v0.03009 @ 2007-11-15 14:33:41

use strict;
use warnings;

use base 'DBIx::Class::Schema';

__PACKAGE__->load_classes;

1;

'load_classes' permet de spécifier les tables à charger. Par defaut toutes les tables du schema sont prisent en compte mais nous aurions pu utiliser que la table 'user' et 'role'


__PACKAGE__->load_classes(qw/ User Role /);

Mais ou sont elles déclarées ? Dans le répertoire lib/DB/Schema/ :

ls lib/DB/Schema/
Doc.pm  Role.pm  UserDoc.pm  User.pm  UserRole.pm

Héhé toutes nos tables ont été découvertes :)

Regardons la déclaration de la table lib/DB/Schema/User.pm :


package DB::Schema::User;

# Created by DBIx::Class::Schema::Loader v0.03009 @ 2007-11-15 15:08:06

use strict;
use warnings;

use base 'DBIx::Class';

__PACKAGE__->load_components("PK::Auto", "Core");
__PACKAGE__->table("user");
__PACKAGE__->add_columns(
  "id",
  { data_type => "INTEGER AUTO_INCREMENT", is_nullable => 0, size => undef },
  "username",
  { data_type => "VARCHAR", is_nullable => 0, size => 30 },
  "password",
  { data_type => "VARCHAR", is_nullable => 0, size => 40 },
  "firstname",
  { data_type => "VARCHAR", is_nullable => 0, size => 40 },
  "lastname",
  { data_type => "VARCHAR", is_nullable => 0, size => 40 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
  "user_roles",
  "DB::Schema::UserRole",
  { "foreign.user" => "self.id" },
);
__PACKAGE__->has_many(
  "user_docs",
  "DB::Schema::UserDoc",
  { "foreign.user" => "self.id" },
);

1;

Que nous dit se fichier ?

Et bien que la table 'user' :

  • posséde les champs ' id', ' username', ' password' ' firtname' et ' lastname'.

  • Que la clé primaire est 'id'

  • Qu'il existe une relation ' has_many' avec le champ ' user' de la table ' user_roles'. Un utilisateur peut donc avoir plisieurs rôles.

  • Qu'il existe une relation ' has_many' avec le champ ' user' de la table ' user_doc'. Un utilisateur peut avoir plusieurs documents.

A l'inverse les classes des tables 'user_role' et 'user_doc' comportent des relations ' belong_to'. En effet chaque 'id' de ces tables fait référence à une autre table. Il existe aussi les relations ' has_a' et ' many_to-many'.

LA VUE:

Notre vue utilisera par default les templates toolkit pour afficher le résultat des requêtes. Il nous faut le préciser dans lib/Docs.pm :


__PACKAGE__->config( name => 'docs',
                     default_view => 'TT');

et ensuite créer la vue par défaut

$ perl script/docs_create.pl view TT TTSite

La encore Catalyst via le 'Templates toolkit' à fait des siennes :)

Il nous a créé une classe 'TT' déclarée dans lib/Docs/View/TT.pm .


package Docs::View::TT;

use strict;
use base 'Catalyst::View::TT';

__PACKAGE__->config({
    CATALYST_VAR => 'Catalyst',
    INCLUDE_PATH => [
        Docs->path_to( 'root', 'src' ),
        Docs->path_to( 'root', 'lib' )
    ],
    PRE_PROCESS  => 'config/main',
    WRAPPER      => 'site/wrapper',
    ERROR        => 'error.tt2',
    TIMER        => 0
});

On y apprend que la variable 'CATALYST_VAR' est redéfinie en 'Catalyst'. Par défaut elle est appelée 'c' et j'aime autant ce raccourci. Donc je supprime cette ligne. Par contre cela va m'obliger à modifier tous les templates créé par TTSite.

On y apprend aussi que les templates doivent être placés dans ' root/src' et ' root/lib' ( INCLUDE_PATH ).

Tout le nécessaire est fait pour afficher correctement les pages (entetes, erreur, messages, contenu ). Nous allons tout de suite le constater.

LES CONTROLEURS:

Il est enfin temps de mettre les mains dans le camboui car jusqu'à maintenant tout à été fait par Catalyst. ( Excepté le plus important qu'est la structure de la base de données )

Si on démarre maintenant le serveur de test on voit apparaitre la page de bienvenue de 'Catalyst'. Et pourtant aucun controleur n'a encore été créé. Il s'agit du controleur par défaut lib/Docs/Controller/Root.pm :


sub default : Private {
    my ( $self, $c ) = @_;

    # Hello World
    $c->response->body( $c->welcome_message );
}

Voilà pourquoi la page de bienvenue est affichée.

Créons maintenant nos cinq controleurs 'login', 'logout','doc', 'user' et 'role' :

$ perl script/docs_create.pl controller login
$ perl script/docs_create.pl controller logout

$ perl script/docs_create.pl controller doc
$ perl script/docs_create.pl controller user
$ perl script/docs_create.pl controller role

Tous nos controleurs ont été générés dans lib/Docs/Controller.

L'AUTHENTIFICATION :

Et si je vais sur http://localhost:3000/doc catalyst m'affiche ' Matched Docs::Controller::doc in doc.' Alors que je souhaite être redirigé vers ' /login' si je ne suis pas authentifié. Pour s'en prémunir j'ajoute une méthode 'auto' au controleur 'Root' qui va faire ce boulot..

vi lib/Docs/Controller/Root.pm :


  sub auto : Private {
    my ($self, $c) = @_;

    # Allow unauthenticated users to reach the login page.  This
    # allows anauthenticated users to reach any action in the Login
    # controller.  To lock it down to a single action, we could use:
    #   if ($c->action eq $c->controller('Login')->action_for('index'))

    # to only allow unauthenticated access to the C<index> action we
    # added above.
    if ($c->controller eq $c->controller('login')) {
        return 1;
    }

    # If a user doesn't exist, force login

    if (!$c->user_exists) {
        # Dump a log message to the development server debug output
        $c->log->debug('***Root::auto User not found, forwarding to /login');
        # Redirect the user to the login page
        $c->response->redirect($c->uri_for('/login'));
        # Return 0 to cancel 'post-auto' processing and prevent use of application

        return 0;
    }
}

Si on redémarrait le serveur on aurait l'erreur suivante : Can't locate object method "user_exists" via package "Docs"

Et en effet ' user_exist' est exécutée par la méthode ' auto' de ' Root'.

C'est le module 'Authentification' de Catalyst qui prend en charge cette méthode. Ajoutons le au module prise en compte.

Dans le fichier de configuration de l'application lib/Docs.pm modifier la ligne suivante:


use Catalyst qw/-Debug ConfigLoader Static::Simple/;

par

use Catalyst qw/-Debug
                ConfigLoader
                Static::Simple

                Authentication
                Authentication::Store::DBIC
                Authentication::Credential::Password

                Session
                Session::Store::FastMmap
                Session::State::Cookie
                /;

Et pour initialiser l'authentification, ajouter au fichier docs.yml les lignes suivantes:

authentication:
    dbic:
        user_class: DB::User
        user_field: username
        password_field: password

Et là la redirection sur '/login' fonctionne :) mais affiche simplement Matched Docs::Controller::login in login.

Il nous faut donc créer le formulaire de login 'login.tt' et modifier la méthode 'index' au controleur 'login' ( lib/Docs/Controller/login.pm )


sub index : Private {
    my ($self, $c) = @_;

    # Get the username and password from form
    my $username = $c->request->params->{username} || "";
    my $password = $c->request->params->{password} || "";


    # If the username and password values were found in form

    if ($username &amp;&amp; $password) {
        # Attempt to log the user in
        if ($c->login($username, $password)) {
            return;
        } else {
            # Set an error message

            $c->stash->{error} = "Mauvais utilisateur ou mot de passe.";
        }
    }

    # If either of above don't work out, send to the login page
    $c->stash->{template} = 'login.tt2';
}

Si la connexion est correcte alors nous poursuivons ... voir dessous la redirection vers /doc/list ...

j'en profite pour modifier la méthode ' index' du controleur ' logout' :


sub index : Private {
    my ($self, $c) = @_;

    # Clear the user's state
    $c->logout;

    # Send the user to the starting point
    $c->response->redirect($c->uri_for('/'));
}

Il nous manque encore le template ' root/src/login.tt2'


[% META title = 'Login' %]
    
<!-- Login form -->
<form method="post" action=" [% c.uri_for('/login') %] ">
  <table>
    <tr>
      <td>Username:</td>

      <td><input type="text" name="username" size="40" /></td>
    </tr>

    <tr>
      <td>Password:</td>
      <td><input type="password" name="password" size="40" /></td>

    </tr>
    <tr>
      <td colspan="2"><input type="submit" name="submit" value="Submit" /></td>

    </tr>
  </table>
</form>

J'ai aussi un peu modifié les fichier root/src/error.tt2 et . /root/lib/site/layout pour afficher les erreurs ( $c->stash->{error} )

Je créer la méthode ' list' du controleur lib/Docs/Controller/doc.pm pour utiliser le template root/src/doc/list.tt2 :


sub list : Local {
    my ( $self, $c ) = @_;

    $c->stash->{template} = 'doc/list.tt2';
    $c->stash->{docs} = [$c->model('DB::Doc')->all];

}

Et enfin pour que toutes les rèquêtes inconnues soietn redirigées vers /doc/list j'ajoute la méthode 'begin' à lib/Docs.pm


sub begin : Private {
    my ( $self, $c ) = @_;

    # Pour les accents français
    $c->res->headers->content_type( 'text/html; charset=iso-8859-1' );

    $c->forward('/doc/list');
}

Voilà par défaut tout est redirigé vers /doc/list

Excepté lorsque l'on est pas idfentifié et dans ce cas redirection vers /login :

Il est temps de faire une pose et d'enregistrer notre travail ...

Cette ébauche pourra être réutilisée dans de nombreux sites :) Nous avons déjà au moins d'authentification.

Voilà les sources de ce qui à été fait jusqu'ici.

4. Gestion des utilisateurs et de leurs rôles

Nous pouvons donc maintenant lister les documents enregistrées en base mais nous ne pouvons encore les éditer, ajouter ou supprimer. En effet nous n'avons pas encore gérer les rôles des utilisateurs.

Nota: Pour que l'on parle bien des mêmes lignes de code et éviter toute confusion reprenons avec les sources du derniers chapitre.

L'objectif étant maintenant de pouvoir gérer les utilisateurs et leurs rôles.

LES AUTORISATIONS :

Les autorisations seront gérés avec le modules Authorization::Roles que nous ajoutons à lib/Docs.pm :

             ...  
             Authorization::Roles
             ...

Et modifions le fichier d'initialisation docs.yml pour ajouter:

authorization:
    dbic:
        role_class: DB::Role
        role_field: name
        role_rel: user_roles
        user_role_user_field: user

Et voilà les autorisations sont prisent en compte :)

Pour s'en convaincre ajoutons ces quelques lignes au fichier root/lib/site/layout :


    <p>bonjour [% c.user.username %], Vous avez les rôles suivants</p>

    <ul>
      [% # Dump list of roles -%]
      [% FOR role = c.user.roles %]<li>[% role %]</li>[% END %]
    </ul>

Le nom de l'utilisateur connecté et ses rôles sont affichés :)

Uilisons les rôles pour restreindre un affichage.

Dans un template :

[% IF c.check_user_roles('admin') %]
   Vous êtes l'admin
[% END %]

Et dans un controleur :

if ($c->check_user_roles('admin')) {
   ...
}

GERONS LES UTILISATEURS:

Avec ce que nous savons, nous pouvons nous ateler à la modification du controleur 'user'. Comme pour les documents, nous souhaitons les lister mais aussi les ajouter, modifier et supprimer. Et pour ajouter ou modifier nous devons disposer de formulaire de saisie.

Pour nous aider dans cette tâche nous ferons appel, encore une fois, à un module (plugin) de Catalyst : Catalyst::Controller::FormBuilder.

Commençons par la méthode, l'action, 'add' : On y accédera par l'url /user/add, un formulaire d'ajout d'utilisateur devra alors s'afficher. Après avoir fournis les données sur l'utilisateur et après l'appui sur 'Valider', celles-ci sont analysées pour validation. Si les données sont correctes alors nous sommes redirigés vers la liste des utilisateurs. Dans le cas contraire les données erronnées sont mises en avant.

Catalyst::Controller::FormBuilder utilise des fichiers 'fb' pour la création et la validation des formulaires. Ainsi pour notre formulaire de la méthode /user/add nous devons créer un fichier ' root/forms/user/add.fb'.

$ mkdir -p root/forms/user

cat root/forms/user/add.fb << __EOF__

Tout ce fera dans le fichier lib/Docs/Controller/user.pm. Ajoutons les méthodes manquantes:

Commentaires:

user_icontoins icon4 25/2/2008 - 0h37
Quand tu dis "2. Définition du Modèle et des constructeurs" tu parles plutot de "controleurs" non ?
user_icondab icon4 25/2/2008 - 1h54
Oui c'est exact, je corrigerai ... plus tard Je mettrai à jour cette doc avec quelques astuces expliquées dans le livre de Jonathan Rockway.
user_iconTouns icon4 4/3/2008 - 14h24
un petit souci à la ligne : if ($username && $password) { il faut remplacer les "$amp;" par &&. Le HTML est mal interpreté. Merci pour ce très bon tuto ! :)
user_icondab icon4 4/3/2008 - 16h11
Merci Effet pb d'interprétation de certains caractères spéciaux. Nota: depuis l'écrtiure de cet article, la méthode d'authentification à évoluer, tu peux jeter un oeil ici: http://catwiki.toeat.com/gettingstarted/tutorialsandhowtos/interim_authorization_and_authentication_example
user_iconTouns icon4 4/3/2008 - 17h27
J'ai fini de suivre ton tuto jusqu'à la partie 3, mais j'ai un problème de redirection : Couldn't render template "file error - doc/list.tt2: not found" et c'est normal, on le ne génère nul part. Tu utilises quelque chose pour généer l'affichage de la liste peut-être ? non ? Ca y ressemble beaucoup d'après ta capture d'écran. Merci ! :)
user_icondab icon4 4/3/2008 - 18h57
Il existe un lien vers les sources de cet article en fin de paragraphe 3.

Add a comment

Validator_logo
Catapulse v0.06
( 0.115665 s)