Python on Rails?
Par klnavarro le 01/04/2007 08:15
Catégories : bd,reseau,autre
Version Python : 2.4
Python on Rails?
- Tour d'horizon
- Epreuve du feu
- Time on Rails
- TimeGears
- Django's Timesheet
- Conclusions
Dans le cadre de mon travail j'ai dû me plonger dans le développement d'applications web.
Le but de cet article est donc de faire le tour des solutions disponibles.
| Auteur: | Mikaël NAVARRO klnavarro@gmail.com |
|---|---|
| Remerciements: | Frederick ROS, Yannick RIBAGNAC, Jeremy PARENTE, Miguel DARDENNE, Serge BEUZIT pour leurs relecture assidue. |
| Licence: |
Cette création est mise à disposition selon le Contrat Paternité-NonCommercial-ShareAlike 2.5 disponible en ligne http://creativecommons.org/licenses/by-nc-sa/2.5/ ou par courrier postal à Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA. |
Tour d'horizon
Je connaissais déjà des solutions basées sur PHP (en fait le quatuor LAMP [1]) mais, comme Perl, je ne trouve pas ce langage naturel et la séparation entre logique et présentation n'y est pas claire.
| [1] | Linux + Apache + MySQL + PHP |
M'attachant à touver une solution libre et portable, je laisse volontairement de côté les solutions basées sur IIS/Asp ou les solutions (RAD [2]) propriétaires (CodeChargeStudio, ...).
| [2] | Rapid Application Development |
Coté Java les solutions basées sur la platforme J2EE sont légions (Sofia, ...), mais je souhaiterais quelque chose qui ne soit pas une usine à gaz !
C'est à ce moment là que l'on m'a fait découvrir Rails [3]
| [3] | Ruby On Rails (RoR) |
Ruby on Rails
Vous avez sûrement entendu parler de Ruby on Rails. C'est le framework du moment, utilisant le langage Ruby, qui est en train de révolutionner la façon de faire des applications web !
"Rails is a full-stack framework for developping database-backed web applications according to the Model-View-Controller pattern".
- Serveurs web recommandés : Apache (1.3.x ou 2.x), lighttpd ou tout serveur supportant le FastCGI (ou CGI). Pour le développement un serveur, WEBrick, intégré est disponible.
- Supporte les bases de données MySQL, PostgreSQL, SQLite, Oracle, SQL Server, DB2 et Firebird/Interbase.
- Système de template HTML avec du code Embedded Ruby, similaire à la syntaxe de JSP.
- Inclut un environnement de test unitaire et fonctionnel (avec une base de données dédiée) et un environnement d'intégration.
- Maintenu par une communautée réactive et accompagné d'une documentation bien fournie.
Cependant, étant tombé sous le charme du langage Python, je me suis demandé s'il n'existait pas d'équivalent en Python.
Zope
Il y a Zope, le serveur d'applications (aussi appelé serveur d'objets) pour la gestion de contenu (CMS [4]) et le travail collaboratif.
| [4] | Content Management System |
Zope fournit, en tant que serveur d'applications, un framework multi-plate-formes, la génération des pages HTML conformes, la sécurité, l'accès aux bases de données, la gestion des sessions utilisateurs, le clustering, les transactions et la possibilitée de déploiement de composants réutilisables.
Son architecture est de type trois tiers : gestion des données (par un mécanisme de persistance d'objets), logique applicative (via des scripts) et présentation (par des gabarits ou templates [5]).
[5] ZPT : Zope Page Template Avec Zope, l'intégralité des tâches de gestion de contenu et d'administration des sites et une grande partie des tâches de développement peuvent s'effectuer depuis une interface web (ZMI [6]).
[6] Zope Management Interface En ce qui concerne la gestion de contenu, Zope répond à cette problématique avec le produit CMF [7] sur lequel se greffent d'autres produits (en ajoutant la notion de workflow) tels que CPS [8] de la sociétée Nuxeo, Plone ou Zwook par exemple.
[7] Content Management Framework
[8] Collaborative Portal Server Zope 3 sera la prochaine version majeure de Zope, elle simplifiera la création de nouvelles classes d'objets et facilitera la réutilisation des composants en adoptant un modèle MVC.
Le seul reproche que je trouve en Zope, c'est qu'il est inutilement complexe et démesuré pour de simples applications web et comme Java, je le laisse de coté.
En cherchant plus avant des solutions Python je suis tombé sur les environnements de développement (frameworks) TurboGears, Django project, Subway et Paste.
TurboGears
"TurboGears is a Python-based framework that enables you to quickly build database-driven web applications".
TurboGears est destiné, comme Rails, au développement d'applications web en Python de façon aisée et plus maintenable.
C'est un megaframework rassemblant un ensemble de composants matures tels que : SQLObject, Kid et CherryPy.
Créé en 2005 par Kevin Dangoor (Zesty News) et délivré en Open-Source à la fin du mois de Septembre.
- TG est collé à la glue, à la différence de RoR, il rassemble divers composants matures (SQLObject, Kid template, CherryPy, Mochikit).
- Le système de template est conforme à la norme XHTML (XML).
- Approche pythonic des objets SQL.
- Il a aussi une belle vidéo : http://turbogears.org/docs/wiki20/20MinuteWiki.mov
- Nécessite Python 2.4 (décorateurs).
- CherryPy est incompatible avec mod_python pour Apache.
Django
"Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design".
Comme Rails, Django a d'abord été utilisé en production avant d'etre distribué publiquement.
Originellement développé pour controler plusieurs sites de news (the World Company of Lawrence, Kansas), il a été publié sous licence BSD en juillet 2005.
Attention la compatibilitée avec les versions antérieures n'est pas assurée jusqu'à la version 1.0 !
- Console d'administration générée automatiquement.
- Utilisation de mod_python pour l'intégration dans Apache (mais pas SCGI pour lighttpd).
- Ne gère pas Oracle.
- Gestion des URLs via des regexp.
- Templates élégants avec filtres et héritage (possibilité d'utiliser Zope 3 Page Template).
- Les vues (list, edit) sont des abstractions de modèles génériques.
- Manque de documentation.
- Vidéo : http://www.throwingbeans.org/django_screencasts.html
Beaucoup pensent que ce sera la prochaine application Zope killer.
Subway
Né en même temps que Rails, il en est le concurrent direct.
- Utilise CherryPy : environnement stable, simple et robuste pour le protocole HTTP.
- SQLObject, le mondialement connu ORM pour Python.
- Cheetah est ultilisé comme moteur de template.
A priori ce n'est rien de plus qu'un générateur de squelette : trop simpliste.
Paste
- WSGI.
- SQLObject.
- Emprunté à Zope, ZPT en tant que système de template.
TurboGears et Django étant, me semble-t-il, les plus aboutis je ne m'attarderai que sur ceux-ci dans la suite de cet article en laissant les autres à votre sagacité.
Maypole
"Maypole is a Perl framework for MVC-oriented web applications".
- Maypole fourni un wrapper au dessus de modules Perl pour Apache, Class::DBI et pour les templates.
Catalyst
"Catalyst will make web development something you had never expected it to be: Fun, rewarding and quick".
Catalyst est similaire à Rails et est originellement basé sur Maypole, il suit le concept de DRY [9] et rassemble une bibliothèque d'outils CPAN :
[9] Don't Repeat Yourself
- Multi-plate-formes : FreeBSD, Linux, Mac OS X, Solaris et Windows.
- Accès et modification du contenu (les données) via Class::DBI, DBIx::Class, Plucene, Net::LDAP...
- Présentation du contenu via Template Toolkit, Mason, HTML::Template...
- Contrôle des requêtes : Catalyst.
- Possibilitée d'utiliser d'autres composants de la bibliothèque CPAN (par exemple pour manipuler plusieurs types de bases de données, ou gérer plusieurs types de templates).
- Gestion aisée des URLs, même avec des expressions régulières.
- Supporte FastCGI, mod_perl, Apache::Request, IIS, Zeus et lighttpd.`
- Inter-opérations via un objet Context partagé.
- Fonction d'Auto-Discovery des composants installés.
- Catalyst fourni un environnement de test intégré basé sur un serveur HTTP.
- Scripts pour les unit tests.
Perl n'étant pas mon langage de prédilection, je les écarterai, de-facto, de cette étude.
Epreuve du feu
J'essayerai d'évaluer, et cela n'engage que moi, ces différents frameworks suivant les critères suivants :
Installation :
On devra ici vérifier la procédure d'installtion et de mise à jour sur les principales platformes de développement (*nix, *BSD, Windows, Mac OS X).
Modèle MVC :
- Model (ORM) : On s'attachera ici au modèle ORM.
- View (templates) : On étudiera ici les possibilitées des différents gabarits mis à disposition.
- Controllers (logic) : On explorera ici en détail l'implémentation de la logique.
Cas test : Timesheet
Afin d'évaluer les différent frameworks dans des conditions comparables, attachons nous à construire une application de Time Reporting dont voici le cahier des charges :
Notre but sera de créer une application permettant de suivre le temps passé sur différentes activités (par exemple pour rapporter le temps passé en support).
Elle devra être facile à utiliser, flexible, disponible 24/7/365 à partir de n'importe quel poste possédant un accès internet.
Au niveau technique, nous utiliserons une base de données MySQL sur un système GNU/Linux (en l'occurence une Debian 3.1 Sarge).
En gros on souhaite suivre des tâches (projet, catégorie) avec les infos suivantes :
- Demandeur (contact) ;
- Date de debut / fin / durée ;
- Description (résolution, mémo) ;
- Status.
Les actions que l'on souhaitera pouvoir effectuer sur les taches sont des plus classiques :
- Démarrer une nouvelle tache ;
- Editer les champs d'une tache ;
- Effacer une tache ;
- Produire un rapport d'activité.
Ensuite on peut avoir deux approches concernant l'interface de saisie :
- Soit, un formulaire classique avec la liste des champs à remplir ;
- Soit, un tableau contenant la liste des taches, avec un menu dynamique permettant de démarrer / mettre en pause / arrêter un compteur (timer).
Time on Rails
Passons dans le vif du sujet, mais avant tout commençons par faire un tour chez la concurrence en évaluant Rails.
Qu'est-ce que Ruby ?
Ruby est un langage de programmation interprété entièrement orienté objet. Sa syntaxe, qui se rapproche de celle d'Ada et Perl, reprend des conceptes proches de ceux offerts par Smalltalk, propose les facilités d'utilisation et d'apprentissage de Python et réinvestit des idées de Lisp et CLU.
Yukihiro "Matz" Matsumoto a commencé l'écriture de ce langage en 1993 et a publié une première version en 1995. La syntaxe de Ruby est cohérente et conçue pour éviter autant que possible les mauvaises surprises, selon le principe PoLS [10]. Le nom Ruby n'est pas un acronyme, mais un jeu de mot sur Perl.
| [10] | Principle of Least Surprise : principe de moindre surprise |
Qu'est-ce que Rails ?
"Rails is a full-stack framework for developping database-driven web applications according to the Model-View-Controller pattern".
Les principes sous-jacents au framework sont : "Ne pas se répéter" et "Convention plutôt que Configuration" :
Ne pas se répéter signifie que chaque définition ne devrait être faite qu'une fois. Comme Rails est un framework en couches complet, les composants sont intégrés de telle sorte que les liens entre eux n'ont pas à être configurés à la main. Par exemple, RoR peut trouver dans la base de données le nom des colonnes et les définir dans le programme.
Convention plutôt que Configuration signifie que le programmeur doit uniquement configurer ce qui n'est pas conventionnel. Par exemple, s'il existe une classe Utilisateur dans le modèle, la table correspondante dans la base de données s'appelle utilisateurs.
La première version de Ruby on Rails date de juillet 2004. Le framework a été extrait de Basecamp, un outil de gestion de projets développé par David Heinemeier Hansson. La première version stable (1.0) est sortie le 14 décembre 2005.
Architecture MVC
| Model: | Dans le modèle orienté objet, les applications web suivant l'architecture Model-View-Controller (MVC), le Model consiste en un ensemble de classes représentant les tables RDMS [11]. Dans Rails le modèle est géré par la classe Active Record. |
|---|
| [11] | Relational DataBase Management System |
| View: | Dans MVC, View est l'affichage de la logique, ou comment les données du Controller doivent être affichées. La méthode utilisé par Rails est d'utiliser du Embedded Ruby (fichiers .rhtml) qui consiste en du code HTML avec du code Ruby (ASPish, horribles voir très Cthulhiens, casse la structure HTML, comme le DTML de Zope) mais des plugins existent (rxml, liquid, ...). |
|---|---|
| Controller: | Le Controller répond aux interactions de l'utilisateur (via le navigateur) et appelle la logique de l'application, il manipule les données du Model et les affiche via le View. |
Serveurs web supportés
Pour les tests et le développement, un serveur légé est fournit avec Ruby : WEBrick.
Pour la production, Apache ou lighttpd avec du FastCGI est recommandé.
Mais, tout serveur web avec du support CGI ou FastCGI est supporté.
Avec Apache, mod_ruby permet d'augmenter considérablement les performances mais son usage est déconseillé à cause de problèmes de sécurité avec plusieurs instances d'applications RoR.
Support bases de données
L'usage d'un système RDMS est conseillé pour le stockage des données, mais Rails supporte aussi SQLite si la mise en place d'un serveur n'est pas possible.
L'accès à la base de données est complètement encapsulé dans des objets classe pour le programmeur permettant de favoriser la portabilité. Aucune ligne de code SQL n'est nécessaire et la connection à une base existante est aussi prévue.
Actuellement, les systèmes RDMS suivant sont supportés : MySQL, PostgreSQL, SQLite, Oracle, SQL Server, DB2 et Firebird.
Autres modules
Rails dispose d'une grosse documentation (livres, tutoriaux) ainsi que d'une communautée réactive. Une présentation sous forme de vidéo est d'ailleur disponible à l'adresse http://media.rubyonrails.org/video/rails_take2_with_sound.mov
Rails inclut un environnement de tests unitaires et fonctionnels (avec la possibilitée d'utiliser une base de test) ainsi qu'un environnement d'intégration.
Module scaffolding permettant de construire automatiquement une grande partie de la logique et des vues nécessaires pour les opérations standard (CRUD [12]).
| [12] | Create, Read, Update, Delete. |
Rails permet aussi l'implémentation d'Ajax [13] ou de RJS [14], à travers différents helpers.
| [13] | http://fr.wikipedia.org/wiki/AJAX |
| [14] | Remote JavaScript |
Et, en addition, Rails offre d'autres modules, tel que Action Mailer pour l'envoi d'email ou Action Web Service pour le support de SOAP et XML-RPC.
Installation
Pour commencer on a juste besoin d'installer le langage Ruby (Ruby 1.8.4 est recommandé pour Rails 1.0) et RubyGems le gestionnaire de paquets.
Ensuite il suffit d'installer Rails avec toutes ses dépendences:
$ gem install rails --include-dependencies
PS: Rails est aussi disponible en paquet indépendant à l'adresse http://rubyforge.org/frs/?group_id=307 , qu'il suffit de décompacter à l'endroit souhaité (sautez, dans ce cas, directement au démarrage du serveur).
Initialisation du projet
Commençons donc par générer l'architecture des répertoires:
$ rails timeonrails
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
...
create log/development.log
create log/test.log
$ cd timeonrails
Et démarrons le serveur:
$ ruby script/server
Ceci démarrera un serveur web (Webrick, si aucun autre serveur, comme lighttpd, n'est trouvé) à l'adresse : http://127.0.0.1:3000 ou http://localhost:3000
Configuration de la base
Maintenant nous devons configurer la base de données :
Dans un premier temps nous devons créer une nouvelle database appelée timesheet dans MySQL (la base doit être créée avant de pouvoir y accéder via RoR).
Ensuite nous devons dire à RoR comment se connecter à cette base (c'est la seule configuration qu'on sera ammené à faire) en éditant le fichier config/database.yml:
# MySQL (default setup). Versions 4.1 and 5.0 are recommended. # # Get the fast C bindings: # gem install mysql # (on OS X: gem install mysql -- --include=/usr/local/lib) # And be sure to use new-style password hashing: # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql database: timesheet username: klnavarro password: xxxxxxxx socket: /var/run/mysqld/mysqld.sock # Connect on a TCP socket. If omitted, the adapter will connect on the # domain socket given by socket instead. #host: localhost #port: 3306 # Warning: The database defined as 'test' will be erased and # re-generated from your development database when you run 'rake'. # Do not set this db to the same as development or production. test: adapter: mysql database: timesheet_test username: klnavarro password: xxxxxxxx socket: /var/run/mysqld/mysqld.sock production: development
Création des catégories
Commençons par rajouter une table categories qui contiendra la liste des catégories auxquelles appartiendront nos différentes tâches.
Et rajoutons-y une clef primaire id (int(11) avec auto incrémentation) et un champ name (varchar(20)) qui recevra le titre de la catégorie (Rails préfère les noms des colonnes en minuscule, mais nous verrons cela plus en détails juste après).
Continuons dans notre lancée et ajoutons deux champs représentant respectivement la date de création et la date de dernière modification (de type timestamp).
Remarque : Les conventions sont recommandées sur la configuration :
Cela veut dire qu'en respectant certaines conventions Rails permet de gérer automatiquement certains champs :
- Le nom des tables doivent être au pluriel ;
- Toutes les tables doivent avoir une clé primaire appelé id (dans MySQL, un numérique avec auto incrémentation) ;
- Les liens vers d'autres tables doivent suivre la convention table + _id ;
- Les champs created_at/created_on ou updated_at/updated_on sont maintenus à jour automatiquement ;
- Les underscores dans le nom des champs sont remplacés par des espaces (pour être plus human friendly dans l'affichage) ;
- Attention Rails est sensible aux minuscules / majuscules ;
- Enfin, un champ appelé lock_version (integer default 0) permet de gérer automatiquement un système de vérrous optimiste.
Ensuite on génère le modèle (vide):
$ ruby script/generate model category
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/category.rb
create test/unit/category_test.rb
create test/fixtures/categories.yml
Scaffold
Générons le coeur de notre application Rails, le controller:
$ ruby script/generate controller category
exists app/controllers/
exists app/helpers/
create app/views/category
exists test/functional/
create app/controllers/category_controller.rb
create test/functional/category_controller_test.rb
create app/helpers/category_helper.rb
Editons le fichier app/controllers/category_controller.rb en rajoutant simplement la ligne suivante:
class CategoryController < ApplicationController scaffold :category end
Et voilà notre application est désormais fonctionnelle en seulement une ligne de code !
Nous pouvons dès à présent manipuler la table categories dans la base de données avec le standard CRUD [15]:
| [15] | Create, Read, Update, and Delete |
Amélioration du modèle
A ce stade nous souhaitons rajouter quelques contraintes sur les champs de notre table categories :
- En particulier on souhaite que le champ name soit non null et pas trop long ;
- De plus nous souhaitons qu'il soit unique.
Pour cela éditons le modèle via le fichier app/models/category.rb:
class Category < ActiveRecord::Base validates_length_of :name, :within => 1..20 validates_uniqueness_of :name, :message => "already exist!" end
Et voici le résultat si nous tentons de rajouter une catégorie qui existe déjà par exemple :
Afin de compléter notre modèle, ajoutons un champ description contenant un descriptif de la catégorie.
Pour cela rajoutons une colonne, juste après le nom, via phpMyAdmin :
Raffraichissons la liste de nos catégories dans notre navigateur :
Et, miracle, le nouveau champ est pris en compte automatiquement : cool !
Complétion du modèle
Nos listes de tâches seront stockées dans une table tasks auxquelles seront associées une category et une note.
Table des items
Notre liste de tâches aura les champs suivants :
- Un titre;
- Une description sommaire (texte libre);
- Une durée (différence entre la date de début et de fin);
- Un lien vers la catégorie à laquelle elle appartient;
- Un lien vers des notes additionnelles;
- Et, un status (open, closed).
Générons le modèle correspondant:
$ ruby script/generate model task
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/task.rb
create test/unit/task_test.rb
create test/fixtures/tasks.yml
Et décrivons les relations avec les autres tables dans le fichier app/models/task.rb:
class Task < ActiveRecord::Base validates_presence_of :title belongs_to :category validates_associated :category end
Table des notes
Cette table ne contiendra qu'un champ de saisie de texte afin de pouvoir fournir des informations supplémentaires.
Générons le modèle:
$ ruby script/generate model note
Et rajoutons-y (app/models/note.rb):
class Note < ActiveRecord::Base validates_presence_of :comment end
Nous ne devons pas oublier de rajouter le lien vers cette nouvelle table dans la table des tâches (app/models/task.rb):
class Task < ActiveRecord::Base [...] belongs_to :note end
Contrôleur
Générons le contrôleur pour la table tasks:
$ ruby script/generate controller task
Et rajoutons du scaffold dans ce contrôleur (app/controllers/task_controller.rb):
class TaskController < ApplicationController scaffold :task end
Opérons de la même manière avec la table note...
PS: A tout moment on peut extraire, via Rails, notre schéma de base en SQL:
$ rake db_structure_dump
...
$ cat db/development_structure.sql
CREATE TABLE `categories` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(20) NOT NULL default '',
`description` text,
`created_on` timestamp(14) NOT NULL,
`updated_on` timestamp(14) NOT NULL default '00000000000000',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) TYPE=MyISAM;
CREATE TABLE `notes` (
`id` int(11) NOT NULL auto_increment,
`comment` text,
`created_on` timestamp(14) NOT NULL,
`updated_on` timestamp(14) NOT NULL default '00000000000000',
PRIMARY KEY (`id`)
) TYPE=MyISAM;
CREATE TABLE `tasks` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(20) NOT NULL default '',
`description` text,
`duration` time NOT NULL default '00:00:00',
`category_id` int(11) default NULL,
`note_id` int(11) default NULL,
`status` smallint(6) default NULL,
`created_on` timestamp(14) NOT NULL,
`updated_on` timestamp(14) NOT NULL default '00000000000000',
PRIMARY KEY (`id`),
UNIQUE KEY `title` (`title`)
) TYPE=MyISAM;
Et voilà pour notre squelette, passons dans le vif du sujet...
Surcharge des actions scaffold
C'est bien tout ça, mais pour l'instant les templates générés ne sont pas très beau !
Aussi nous allons maintenant créer nos propres vues, et c'est là que ça deviend intéressant : A chaque fois que l'on créera une action elle écrasera celle du scaffold ; cela nous permettra de travailler par couches successives et une fois que nous aurons tout redéfini nous pourrons ensuite l'enlever définitvement.
Liste des items
La page qui montre la liste des tâches n'est décidémment pas très jolie et nécéssite des améliorations. Pour cela rajoutons une méthode list dans le contrôleur app/controllers/task_controller.rb:
class TaskController < ApplicationController scaffold :task def list end end
Ré-ouvrons la page des tâches :
Comme nous avons définit notre propre definition de l'action list, Rails n'utilise plus le scaffold et il essaye donc de trouver un gabarit associé pour le rendu : app/views/task/list.rhtml
Rails utilise comme format de gabarit du HTML avec du code Ruby placé entre les tags <% %> et <%= %> (très ASPiche !):
<html>
<head>
<title>All Tasks</title>
</head>
<body>
<h1>Online Timesheet - All Tasks</h1>
<table border="1">
<tr>
<th>Title</th>
<th>Description</th>
<th>Duration</th>
<th>Status</th>
</tr>
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'edit', :id => task.id %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
</tr>
<% end %>
</table>
<%= link_to 'New task', :action => 'new' %>
</body>
</html>
Complétons notre contrôleur pour récupérer la liste de nos tâches:
class TaskController < ApplicationController
scaffold :task
def list
@tasks = Task.find(:all)
end
end
Et voyons le résultat dans notre navigateur:
Mais comme vous pouvez le constater, la colonne catégorie n'apparait pas dans notre liste aussi rajoutons-la:
class Category < ActiveRecord::Base has_many :tasks [...] end
Par le biais des instructions belongs_to et has_many, à ce stade, une tâche est associée à une seule catégorie et une catégorie peut etre liée à plusieurs tâches.
NB: Avant d'aller plus loin, assurez-vous que chacune des tâches dans la database à une catégorie associée, sinon nous risquons d'avoir des erreurs par la suite !
Notre template list.rhtml deviens alors:
[...]
<table>
<tr>
<th>Title</th>
<th>Category</th>
<th>Description</th>
<th>Duration</th>
<th>Status</th>
</tr>
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'edit', :id => task.id %></td>
<td><%= task.category.name %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
</tr>
<% end %>
</table>
[...]
Opérons de la même manière avec la note additionelle (avec une petite variante dans le template puisque la présence de la note n'est pas obligatoire):
[...]
<table>
<tr>
<th>Title</th>
<th>Category</th>
<th>Description</th>
<th>Duration</th>
<th>Status</th>
<th>Notes</th>
</tr>
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'edit', :id => task.id %></td>
<td><%= task.category.name %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
<% if task.note.nil? %>
<td>None</td>
<% else %>
<td><%=h task.note.comment %></td>
<% end %>
</tr>
<% end %>
</table>
[...]
Gabarit de l'application
Pour éviter de se répéter, nous allons maintenant décomposer le gabarit de notre application en 3 composants :
- Le Layout fournira le code commun utilisé par toutes les actions (typiquement l'entête et le pied de page du code HTML) ;
- Le Template contiendra le code spécifique à l'action ;
- Et, les Partials factoriseront les fonctions communes à plusieurs actions.
Commençons par le Layout ; Rails, par convention, charge automatiquement le fichier, dans app/views/layouts/, qui porte le même nom que le contrôleur, soit dans notre cas le fichier task.rhtml:
<html>
<head>
<title>Online Timesheet - Tasks: <%= controller.action_name %></title>
</head>
<body>
<%= @content_for_layout %>
</body>
</html>
Notez que nous avons rajouté quelques fonctions Ruby :
- action_name est une méthode de la classe ActionController qui retourne le nom de l'action que le contrôleur est en train d'exécuter ;
- @content_for_layout est l'instruction qui insère le template correspondant à l'action.
Template
Mettons à jour notre template list.rhtml de façon à enlever l'inutile (maintenant dans le layout):
<h1>Online Timesheet - All Tasks</h1>
<table border="1">
<tr>
<th>Title</th>
<th>Category</th>
<th>Description</th>
<th>Duration</th>
<th>Status</th>
<th>Notes</th>
<th></th>
</tr>
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'edit', :id => task.id %></td>
<td><%= task.category.name %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
<% if task.note_id.nil? %>
<td>Note</td>
<% else %>
<td><%=h task.note.comment %></td>
<% end %>
</tr>
<% end %>
</table>
<%= link_to 'New task', :action => 'new' %>
Format de la table
Plaçons la liste des items (tâches) dans le partial app/views/task/_task_list.rhtml:
<table border="1">
<tr>
<th>Title</th>
<th>Category</th>
<th>Description</th>
<th>Duration</th>
<th>Status</th>
<th>Notes</th>
<th></th>
</tr>
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'edit', :id => task.id %></td>
<td><%= task.category.name %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
<% if task.note_id.nil? %>
<td>None</td>
<% else %>
<td><%=h task.note.comment %></td>
<% end %>
</tr>
<% end %>
</table>
Et notre template se résume alors à:
<h1>Online Timesheet - All Tasks</h1> <%= render_partial 'task_list' %> <%= link_to 'New task', :action => 'new' %>
Feuille de style
Pour gérer notre feuille de style, rajoutons (dans la partie <head> du layout) l'instruction <%= stylesheet_link_tag 'timesheet' %> qui génèrera le code HTML suivant:
<link href="/stylesheets/timesheet.css" media="screen" rel="stylesheet" type="text/css" />
Modifions le partial pour utiliser une classe CSS:
<table class="task_list">[...]
Editons ensuite le fichier public/stylesheets/timesheet.css:
body {
background-color: #f6f3f6;
}
h1 {
font-size: 150%;
}
/* Task list table */
table.task_list {
background-color: #efefef;
border: solid 1px #000000;
border-collapse: collapse;
}
table.task_list th {
color: #efefef;
background-color: #8a9cb9;
border: dotted 1px #000000;
padding: 1px 2px;
}
table.task_list td {
border: dotted 1px #000000;
padding: 1px 2px;
}
Gestion des tâches
Effacement d'un item
Rajoutons simplement un lien qui appelle la méthode destroy:
[...]
<% @tasks.each do |task| %>
<tr>
<td><%= link_to task.title, :action => 'show', :id => task.id %></td>
<td><%= task.description %></td>
<td><%= task.duration %></td>
<td><%= task.status %></td>
<% if task.note_id.nil? %>
<td>None</td>
<% else %>
<td><%=h task.note.comment %></td>
<% end %>
<td><%= link_to 'delete', { :action => 'destroy', :id => task.id },
:confirm => "Are you sure you want to delete '#{task.title}'?" %></td>
</tr>
<% end %>
[...]
Ajout d'un item
Nous allons ici écrire la méthode new du controleur:
class TaskController < ApplicationController
[...]
def new
@categories = Category.find(:all)
@task = Task.new
end
end
Cette méthode s'occupe de récupérer la liste de toutes les catégories (ça nous sera utile pour afficher une liste des catégories disponibles) et crée une nouvelle tâche.
Le template new.rhtml se résumera à:
<h1>Online Timesheet - New Task</h1> <%= start_form_tag :action => 'create' %> <%= render_partial 'edit' %> <%= submit_tag 'Save' %> <%= end_form_tag %> <%= link_to 'Back', :action => 'list' %>
Tandis que le partial _edit.rhtml contiendra le formulaire en lui-même:
<table>
<tr>
<td><label for="task_title">Title:</label></td>
<td><%= text_field 'task', 'title', :size => 20, :maxlength => 20 %></td>
</tr>
<tr>
<td><label for="task_description">Description:</label></td>
<td><%= text_area 'task', 'description' %></td>
</tr>
<tr>
<td><label for="task_duration">Date/Duration:</label></td>
<td><%= datetime_select 'task', 'duration' %></td>
</tr>
<tr>
<td><label for="task_category_id">Category:</label></td>
<td><select id="task_category_id" name="task[category_id]">
<%= options_from_collection_for_select @categories, 'id', 'name', @task.category_id %>
</select>
</td>
</tr>
<tr>
<td><label for="task_note_id">Note:</label></td>
<td><%= text_field 'task', 'note_id' %></td>
</tr>
<tr>
<td><label for="task_status">Status?</label></td>
<td><%= check_box 'task', 'status' %></td>
</tr>
</table>
On remarquera que Rails nous fournit un ensemble de helpers qui nous facilite grandement la tâche pour l'écriture des formulaires.
Edition d'un item
Enfin pour l'action d'édition nous réutilisons le partial et updatons le controleur:
class TaskController < ApplicationController
[...]
def edit
@categories = Category.find(:all)
@task = Task.find(params[:id])
end
end
Le template quant à lui sera de la forme:
<h1>Online Timesheet - Edit Task</h1> <%= start_form_tag :action => 'update', :id => @task %> <%= render_partial 'edit' %> <%= submit_tag 'Update' %> <%= end_form_tag %> <%= link_to 'Back', :action => 'list' %>
PS: On remarque que le submit de l'action de création (new) ou d'édition renvoie vers l'action show du scaffold, pour éviter cela et renvoyer directement vers la liste, rajoutons la définition de l'action show:
class TaskController < ApplicationController
[...]
def show
redirect_to :action => 'list'
end
end
Les notes additionnelles
Intégrité lors de l'effacement
Mais que se passe-t-il si on détruit une tâche à laquelle une note est attachée ? Nous devons nous assurer que la note liée, s'il y a lieu, est bien effacée avant d'effacer la tâche selectionnée.
Pour accomplir cela nous allons utiliser notre modèle app/models/task.rb:
class Task < ActiveRecord::Base
[...]
def before_destroy
unless note_id.nil?
Note.find(note_id).destroy
end
end
end
De la même façon, si nous enlevons une note, nous devons aussi enlever toutes références vers elle (app/models/note.rb):
class Note < ActiveRecord::Base
[...]
def before_destroy
Task.find_by_note_id(id).update_attribute('note_id', NIL)
end
end
Edition d'une note
Nous allons modifier notre template pour ajouter un lien sous la colonne Notes qui pointera vers le formulaire de création ou d'édition de la note suivant le cas où nous souhaiterions rajouter une note ou éditer la note attachée à l'item.
Pour cela, modifions le template list.rhtml comme ceci:
[...] <% if task.note_id.nil? %> <td><%= link_to 'None', :controller => 'note', :action => 'new', :id => task.id %></td> <% else %> <td><%= link_to task.note.comment, :controller => 'note', :action => 'edit', :id => task.note_id %></td> <% end %> [...]
Et créons notre template d'édition de la note (app/views/note/edit.rhtml):
<h1>Online Timesheet - Edit Note</h1> <%= start_form_tag :action => 'update', :id => @note %> <%= render_partial 'edit' %> <%= submit_tag 'Update' %> <%= end_form_tag %> <%= link_to 'Back', :controller => 'task', :action => 'list' %>
Ainsi que le partial associé (_edit.rhtml):
<table>
<tr>
<td><label for="note_comment">Comment:</label></td>
<td><%= text_area 'note', 'comment' %></td>
</tr>
</table>
Du côté du contrôleur rajoutons l'action edit:
class NoteController < ApplicationController
scaffold :note
def edit
@note = Note.find(params[:id])
end
end
Lorsque la note n'est pas créée, nous devons en plus rattacher celle-ci sur la tâche courante:
class NoteController < ApplicationController
[...]
def create
@note = Note.new(params[:note])
@note.save
@task = Task.find(session[:task_id])
@task.update_attribute(:note_id, @note.id)
redirect_to :controller => 'task', :action => 'list'
end
def new
session[:task_id] = params[:id]
@note = Note.new
end
end
Pour cela nous passons par la session afin de récupérer l'id de la tâche à laquelle il faut rattacher la note. L'action new stoque cette information à partir du template list (instruction :id => task.id) et l'action create récupère l'information de la session et met à jour la tâche correspondante afin de pointer vers la note nouvellement créée.
Resultat final
Notre application est maintenant terminée, sommaire mais fonctionnelle :
On notera que bien des évolutions sont possibles, notamment en ce qui concerne la gestion de la date et de la durée de la tâche ou encore l'intégration d'AJAX pour la création ou la modification des items in place plutôt que de passer par un formulaire...
Des points concernant le design peuvent aussi etre discutables comme le fait de créer des contrôleurs aussi pour les catégories et les notes ; le fait de tout mettre dans un seul contrôleur nous permettrait, en effet, de nous affranchir de la session pour communiquer les données.
Je doit l'admettre Rails est vraiment un framework très complet, bien pensé et qui facilite grandement la tâche du développeur.
Regardons maintenant si les solutions Python sont à la hauteur.
Un an après Rails (le 3 janvier 2007) la version 1.0 de TurboGears est arrivée à maturité...
TimeGears
"TurboGears is a Python-based framework that enables you to quickly build database-driven web applications".
C'est un megaframework rassemblant un ensemble de composants arrivés à maturité tels que : SQLObject, Kid et CherryPy.
Créé en 2005 par Kevin Dangoor (Zesty News) il a été livré en Open-Source à la fin du mois de Septembre de la même année.
Le but de TurboGears est de créer des applications web aboutis rapidement et facilement :
- En vous permettant de mettre à disposition des fonctionalitées sur le web aussi facilement qu'on écrit des fonctions ;
- En vous fournissant des moyens d'écrire du code HTML agréablement et en fournissant une API pour JavaScript ;
- En vous permettant de travailler avec n'importe quel outil XHTML pour travailler sur le rendus de vos pages ;
- En vous laissant accéder à votre base de données sans écrire aucune ligne de SQL ;
- En comblant les failles de JavaScript, pour vous enthousiasmer à en écrire ;
- En utilisant un language clair, conçis et dynamique.
TurboGears stack
TurboGears rassemble les meilleurs librairies disponibles et les combine entre elles :
SQLObject
Le Model : Gère les données à travers des classes Python ; Permet de créer une base de données ou de s'interfacer avec des données existantes ou de multiples serveurs de données.
Il permet une approche pythonic des objets SQL.
Kid
La View : Moteur de gabarit XHTML (conforme à la structure XML) avec des éléments en code Python.
CherryPy
Le Controller: Un module intéressant permettant à l'application web d'être contrôlée en programmant des événements qui renvoient des données dans le template.
CherryPy, peut aussi lui-même être un serveur web ou être lancé par n'importe quel environnement WSGI (incluant Apache 2).
Attention CherryPy est incompatible avec mod_python pour Apache.
MochiKit
Partie optionnelle, elle permet la programmation JavaScript de façon plus pythonique. Généralement utilisée pour l'implémentation d'AJAX.
Autres composants
- ElementTree : Une resplendissante librairie XML.
- FormEncode : Un framework simple et extensible de validation.
- TestGears : Utilise les unittest standard de Python pour construire vos tests plus facilement.
- json-py : Conversion entre Python et le format JSON
- CatWalk : Une interface web pour gérer la base.
- ...
Installation
Prérequis: Nécessite Python 2.4 (à cause des decorator).
Installation avec tgsetup :
Téléchargez le script tgsetup.py
Exécutez
sudo python2.4 tgsetup.py
Qui se chargera de télécharger toutes les libs nécéssaires au bon fonctionnement de TurboGears.
Vérifiez votre installation
tg-admin info
Quickstart
Pour commencer nous allons initialiser notre projet
$ tg-admin quickstart
Enter project name: TimeGears
Enter package name [timegears]: timegears
Do you need Identity (usernames/passwords) in this project? [no] no
Selected and implied templates:
TurboGears#tgbase tg base template
TurboGears#turbogears web framework
Variables:
egg: TimeGears
identity: none
package: timegears
project: TimeGears
sqlalchemy: False
Creating template tgbase
Creating directory ./TimeGears
Recursing into +einame+.egg-info
[...]
Recursing into +package+
Creating ./TimeGears/timegears/
[...]
Recursing into static
Creating ./TimeGears/timegears/static/
[...]
Recursing into templates
[...]
Creating template turbogears
Recursing into +package+
Recursing into config
[...]
Copying controllers.py_tmpl to ./TimeGears/timegears/controllers.py
Copying json.py_tmpl to ./TimeGears/timegears/json.py
Copying model.py_tmpl to ./TimeGears/timegears/model.py
Recursing into sqlobject-history
[...]
Recursing into static
[...]
Recursing into templates
[...]
Recursing into tests
Creating ./TimeGears/timegears/tests/
[...]
Copying README.txt_tmpl to ./TimeGears/README.txt
[...]
Running /usr/bin/python2.4 setup.py egg_info
Adding TurboGears to paster_plugins.txt
running egg_info
writing requirements to TimeGears.egg-info/requires.txt
writing TimeGears.egg-info/PKG-INFO
writing top-level names to TimeGears.egg-info/top_level.txt
writing dependency_links to TimeGears.egg-info/dependency_links.txt
reading manifest file 'TimeGears.egg-info/SOURCES.txt'
writing manifest file 'TimeGears.egg-info/SOURCES.txt'
Donc après avoir saisi un nom pour notre projet et décidé du répertoire où initialiser le projet nous pouvons maintenant démarrer un serveur
$ cd TimeGears $ python2.4 start-timegears.py
Le script de démarrage du serveur TG regarde si un fichier setup.py est présent, dans ce cas il utilise la database de développement dev.cfg sinon il utilisera prod.cfg.
Remarque: Il est possible de spécifier sur la ligne de commande quelle database utiliser.
Donc, comme nous sommes dans un environnement de développement, éditons le fichier dev.cfg afin de décommenter la ligne suivante
sqlobject.dburi="mysql://klnavarro:xxxxxxxx@localhost:3306/timesheet"
Et redémarrons le serveur.
PS: Ne pas oublier de créer la base timesheet dans phpMyAdmin par exemple !
Gestion des tâches
Description du modèle
Décrire pour commencer notre modèle Task, pour cela rajoutons dans le fichier timegears/model.py la classe suivante
from datetime import datetime
class Task(SQLObject):
title = StringCol(alternateID=True, length=20)
description = StringCol()
duration = DateTimeCol(default=datetime.now)
status = IntCol(default=0)
Soit une colonne title qui nous servira de clef (l'option alternateID=True assure que la colonne est unique et nous permettra de rechercher facilement une tâche), une colonne description pour contenir l'objet de la tâche, une durée (date + temps) et un status.
Et nous pouvons maintenant créer la table dans la base de données
$ tg-admin sql create
Remarque: tg-admin sql est un wrapper autour de la commande sqlobject-admin de SQLObject, elle regarde dans le fichier de configuration pour trouver comment accéder à la base de données et crée les tables comme spécifié dans le modèle.
Notez aussi que l'on peut vérifier à tout moment que les définitions dans le modèle correspondent bien à la description de la table via la commande tg-admin sql status.
Affichage d'une tâche
Commençons par dupliquer un nouveau template (task.kid) à partir du fichier timegears/templates/welcome.kid
cd timegears/templates cp welcome.kid task.kid cd ../..
Et remplaçons le contenu du body par les lignes suivantes
<div style="float:right; width: 20em"> Viewing <span py:replace="title">Task title goes here</span> | <a href="/">Back</a> </div> <div py:replace="XML(description)">Content goes here.</div>
Notez que ce template est tout à fait valide et qu'il peut être ouvert sans problème avec votre navigateur web (ce qui est bien pratique lors de la phase de design de la page).
Nous avons maintenant besoin de donner la visibilité sur le modèle depuis le contrôleur controllers.py
import turbogears from model import Task from docutils.core import publish_parts
PS: La seconde ligne donne accès à une fonction qui nous sera utile pour la mise en page.
Il ne nous reste plus qu'à rajouter la méthode show dans notre contrôleur :
- On déclare quel template utiliser ;
- On définit une tâche par défaut ;
- On récupère les tâches depuis la base de données ;
- On formate le texte à afficher ;
- Et, on retourne les données pour le template.
@expose(template="timegears.templates.task")
def show(self, title="TG evaluation"):
task = Task.byTitle(title)
content = publish_parts(task.description, writer_name="html")["html_body"]
return dict(title=task.title, description=content)
Ok, rafraichissons notre navigateur sur la page http://localhost:8080/show.
Oups, nous obtenons une erreur. Etant donné que nous sommes en mode développement, CherryPy nous affiche alors la pile d'appel où l'on peut voire qu'une exception SQLObjectNotFount est levée ; Et en effet nous n'avons aucune donnée dans la base de données ! Rajoutons donc une tâche nommée TG evaluation via la console de turboGears
$ tg-admin shell >>> Task(title="TG evaluation", description="Comparaison TurboGears vs Rails.", status=0)
Rafraîchissons la page :
Et voilà !
Amélioration de la page
Affichons les autres colonnes de la table task.
timegears/templates/task.kid
<div style="float:right; width: 20em"> Viewing <span py:replace="title">Task title goes here</span> | <a href="/">Back</a> </div> <div py:replace="XML(description)">Content goes here.</div> <div style="font-size: 80%"> Duration: <span py:replace="duration">duration goes here</span> (<span py:replace="status">status</span>) </div>
timegears/controllers.py
@expose(template="timegears.templates.task")
def show(self, title="TG evaluation"):
task = Task.byTitle(title)
content = publish_parts(task.description, writer_name="html")["html_body"]
return dict(title=task.title, description=content,
duration=task.duration, status=task.status)
Modification d'une tâche
Rajoutons une page d'édition.
Pour cela, comme d'habitude on copie notre page task.kid en edit.kid et on remplace le contenu du body par
<form action="save" method="post"> <input type="hidden" name="title" py:attrs="value=title" /> <textarea name="description" py:content="description" rows="10" cols="60" /> <input type="submit" name="submit" value="Update" /> </form>
Et rajoutons notre méthode edit
@expose(template="timegears.templates.edit")
def edit(self, title):
task = Task.byTitle(title)
return dict(title=task.title, description=task.description,
duration=task.duration, status=task.status)
A ce stade nous devons encore rajouter dans le template task.kid un moyen pour accéder à la page d'édition
<p><a href="${tg.url('/edit', title=title)}">Edit this task</a></p>
Cette dernière ligne fera l'affaire.
La page d'édition s'affiche correctement mais nous devons encore implémenter la méthode save du contrôleur
@expose()
def save(self, title, description, submit):
task = Task.byTitle(title)
task.description = description
turbogears.flash("Changes saved!")
raise turbogears.redirect('/show', title=title)
- Dans ce cas nous n'utilisons aucun template car nous redirigerons directement vers la page principale ;
- L'instruction task.description = description est tout ce qu'il y a à faire pour provoquer la mise à jour SQL ;
- turbogears.flash() est un message de notification ;
- Comme redirect est appelé via une exception, tout autre processus est court-circuité !
Mignon, non ?
Nouvelles tâches
Que se passe-t'il si on essaye d'éditer une page qui n'existe pas ? Dans ce cas nous souhaitons qu'on soit redirigé vers une page pour la créer.
Pour cela changeons la méthode show afin de gérer l'exception
from sqlobject import SQLObjectNotFound
try:
task = Task.byTitle(title)
except SQLObjectNotFound:
raise turbogears.redirect('/new', title=title)
PS: Nous avons aussi besoin d'importer la déclaration de l'exception elle-même via l'instruction from sqlobject import SQLObjectNotFound.
Rajoutons la méthode new et réutilisons le template edit.kid pour cela:
@expose(template="timegears.templates.edit")
def new(self, title):
return dict(title=title, description="", status=0)
On doit adapter la méthode save pour pouvoir sauver ces nouvelles tâches
@expose()
def save(self, title, description, submit):
try:
task = Task.byTitle(title)
task.description = description
except SQLObjectNotFound:
task = Task(title=title, description=description)
turbogears.flash("Changes saved!")
raise turbogears.redirect('/show', title=title)
Essayons, nous devons pouvoir créer de nouvelles tâches :
Liste des tâches
Ajoutons une page affichant la liste des tâches (list.kid)
<h2>All Of Our Tasks</h2>
<ul>
<li py:for="title in titles">
<a href="${std.url('/show?title=%s' % title)}" py:content="title">Task title here</a>
</li>
</ul>
Et rajoutons la méthode list
@expose(template="timegears.templates.list")
def list(self):
titles = [task.title for task in Task.select(orderBy=Task.q.title)]
return dict(titles=titles)
En page principale
Notre liste fonctionne, mais ce qu'on voudrait c'est que se soit la liste qui s'affiche par défaut et que le détail n'arrive qu'après, aussi nous devons rediriger la méthode index vers la méthode list
@expose()
def index(self):
raise turbogears.redirect('/list')
Toolbox
Un petit aparté concernant les outils fournis avec TurboGears, commençons par lancer le serveur des outils:
$ tg-admin toolbox
Et visitions la page http://localhost:7654 :
Comme on peut le voir, TurboGears est livré avec un ensemble d'outils qui vont grandement nous faciliter la vie, avec notamment :
- Des informations sur le système ;
- Un acces à la console (de la même manière que tg-admin shell) ;
- La gestion de l'internationalisation ;
- Une description des widgets utilisables dans les templates ;
- Et un modeleur (ModelDesigner) ainsi qu'un explorateur (CatWalk) de base de données.
Catégories et notes
Gestion des catégories
Mettons à jour notre modèle afin d'ajouter nos catégories (avec un nom, une description et un lien 1-N (MultipleJoin) vers les tâches). Pour cela utilisons le ModelDesigner accessible à l'adresse http://localhost:7654/designer/ et ajoutons une nouvelle classe Category :
Puis rajoutons nos colonnes :
Ajout des notes
Opérons de la même manière avec les notes additionnelles, mais cette fois-ci c'est une relation 1-1 (SingleJoin) que l'on doit déclarer :
Remarquez que les foreign keys sont crées automatiquement dans la table `task :
Génération du modèle
L'onglet suivant nous affiche le code généré à partir de la description du modèle :
Il nous suffit alors de générer le fichier model.py et de demander la création des tables correspondantes...
Enfin le dernier onglet nous présente la représentation des tables avec leurs relations de façon graphique :
CatWalk
CatWalk est une simple page HTML avec de l'AJAX qui permet d'interagir avec le modèle de notre application et d'en gérer les données : http://localhost:7654/catwalk/
Grace à cet accès nous rajoutons simplement des catégories, des notes et nous les lions à nos tâches.
Mise à jour des vues
Modifions notre template task.kid afin d'afficher la catégorie ainsi que les notes aditionelles
<div style="float:right; width: 20em">
Viewing <span py:replace="title">Task title goes here</span>
| <a href="/">Back</a>
<br />
Category: <span py:content="category" title="${category_desc}">Category name goes here</span>
</div>
<div py:replace="XML(description)">Content goes here.</div>
<div py:replace="XML(note)">Comments goes here.</div>
[...]
Et faisons en sorte que la méthode show du contrôleur renvoie les données nécessaires
@expose(template="timegears.templates.task")
def show(self, title="TG evaluation"):
try:
task = Task.byTitle(title)
except SQLObjectNotFound:
raise turbogears.redirect('/new', title=title)
content = publish_parts(task.description, writer_name="html")["html_body"]
category = task.category
if task.remark:
note = publish_parts(task.remark.comment, writer_name="html")["html_body"]
else:
note = publish_parts("", writer_name="html")["html_body"]
return dict(title=task.title, description=content,
category=category.name, category_desc=category.description, note=note,
duration=task.duration, status=task.status)
PS : Ne pas oublier d'importer les modèles Category et Note !
De même avec l'edit
@expose(template="timegears.templates.edit")
def edit(self, title):
task = Task.byTitle(title)
categories = Category.select(orderBy='name')
category = task.category
if task.remark:
note = task.remark.comment
else:
note = None
return dict(title=task.title, description=task.description,
category=category.name, category_desc=category.description, note=note,
category_id=category.id, categories=categories,
duration=task.duration, status=task.status)
Ici, nous récupérons les notes additionnelles et rajoutons la liste des categories qui nous servira à générer le select dans le template edit.kid
[...]
<select name="category">
<option py:for="cat in categories"
py:content="cat.name" value="${cat.id}"
title="${cat.description}"
py:attrs="selected=std.selector(cat.id == category_id)">Category name goes here</option>
</select>
<textarea name="note" py:content="note" rows="5" cols="60" />
[...]
Mettons à jour maintenant notre méthode save pour sauvegarder nos remarques et notre categorie en fonction de l'id selectionné
@expose()
def save(self, title, description, category, submit):
try:
task = Task.byTitle(title)
task.description = description
task.category = Category.get(category)
if task.remark:
task.remark.comment = note
else:
task.remark = Note(comment=note)
except SQLObjectNotFound:
task = Task(title=title, description=description,
category=Category.get(category), remark=note,
duration=None, status=1)
turbogears.flash("Changes saved!")
raise turbogears.redirect('/show', title=title)
De même remettons au goût du jour la méthode new de façon à ce qu'elle gère correctement les paramètres non définis
@expose(template="timegears.templates.edit")
def new(self, title):
categories = Category.select(orderBy='name')
return dict(title=title, description="",
category=None, note=None,
category_id=0, categories=categories,
duration=None, status=0)
Et voilà, notre application bien que sommaire est opérationnelle.
Working with Forms and Widget
Amusons-nous maintenant un peu avec les concept de widget de TurboGears.
Pour simplifier le travail avec les Forms, TurboGears introduit la notion de Widget et de Validators.
Voyons cela sur un exemple, pour cela reprenons notre vue d'édition. Pour nous simplifier la vie créons un helper dans notre controlleur
from turbogears import widgets, validators as v, error_handler
categories = Category.select(orderBy='name')
edit_form = widgets.ListForm(
fields = [
widgets.HiddenField(name="title"),
widgets.TextArea(name="description", rows="10", cols="60",
validator=v.All(v.NotEmpty, v.UnicodeString)),
widgets.SingleSelectField(name="category",
options=[ (cat.id, cat.name) for cat in categories ]),
widgets.TextArea(name="note", rows="5", cols="60",
validator=v.UnicodeString),
],
submit_text='Update'
)
Modifions nos méthodes edit et new







