ka.da

Aller au contenu | Aller au menu | Aller à la recherche

vendredi, avril 1 2011

Django : passage aux vues génériques basées sur des classes, par l'exemple

Introduction

Si vous utilisez Django vous connaissez probablement déjà les vues génériques/generic views : elles permettent de faciliter la création de certaines tâches monotones et répétitives qu'on retrouve souvent sur les applications web comme sont les listes, affichage, modification et création d'objets...

Jusqu'à Django 1.2 les vues génériques étaient des fonctions, et depuis Django 1.3 (qui est sorti il y a peu) elles sont maintenant basées sur des classes. Je vais tenter ici, modestement, de montrer l'intérêt de ce changement, donner des exemples simples pour illustrer la migration d'un type vers l'autre, et donner quelques repères pour comprendre leur fonctionnement.

Les vues génériques basées sur des fonctions (qu'on notera FBV) fonctionnent très bien pour des cas simples, puis on doit rapidement faire de l'encapsulation ('wrapper') pour certains cas spéciaux, mais on arrive encore à notre but, mais à un moment ou à un autre, malheureusement on se retrouve coincé sans pouvoir faire ce qu'on veut et on doit alors recréer nos propres vues et se passer de ce qui existe déjà dans Django.

Cette limite est connue depuis longtemps et le travail a été long (le ticket #6735 a trois ans) mais l'arrivée des CBV permet justement de s'affranchir de ses limites.

Exemple de passage des FBV aux CBV

Prenons un cas simple où l'on veut lister des objets, et où la liste de ceux-ci dépendra d'un paramètre dynamique (ici l'utilisateur connecté) :

note : j'ai volontairement allégé le code en enlevant les parties classiques ou évidentes, néanmoins si vous voyez une énormité merci de le signaler

## urls.py
from myproject.views import listview

urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
)

## models.py
from django.contrib.auth.models import User

class MyObject(models.Model):
    ...
    author = models.ForeignKey(User)

## views.py
@login_required
def listview(request,
          queryset=MyObject.objects.all(),
          template_name='myproject/myobject_list.html'):
    qs = queryset.filter(author=request.user)
    return object_list(request,
                       queryset=qs,
                      template_name=template_name)

On a ici l'exemple typique de l'utilisation d'une vue générique avec un wrapper. Jusque là ça ne pose pas de problème particulier et tout peut être fait avec Django 1.2 et ses FBV.

Par contre, vu que dans notre modèle author est un utilisateur, on imagine très bien qu'il faut qu'à l'enregistrement ce champ soit initialisé avec l'utilisateur connecté, donc la valeur de request.user, et là les vues génériques montrent leur limitation : il est en effet possible de surcharger la fonction save() du formulaire mais il n'est pas possible d'indiquer à la vue générique create_object de lui passer le paramètre qui nous sera utile au moment de son enregistrement.

On est donc obligé de créer notre propre vue pour gérer ce qui est finalement une différence mineure mais un cas que l'on retrouve fréquemment.

On aura alors quelque chose comme

## urls.py
urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
    (r'^create/', createview, {}, 'myobject_create'),
)

## forms.py
class MyObjectForm(ModelForm):
    class Meta:
        model = MyObject
        exclude = ('author',)

    def save(self, user=None):
        myobject = super(MyObjectForm, self).save(commit=False)
        myobject.author = user
        myobject.save()

## views.py
from myproject.forms import MyObjectForm

@login_required
def createview(request, form_class=MyObjectForm):
    if request.method == 'POST':
        form = form_class(request.POST)
        if form.is_valid():
            myobject = form.save(user=request.user)
            return HttpResponseRedirect(myobject.get_absolute_url())
    else:
        form = form_class()
    return render_to_response('myproject/myobject_form.html',
                              {'form': form},
                              context_instance=RequestContext(request))

Voyons maintenant comment les deux exemples précédents sont implémentables en utilisant les vues basées sur les classes.

## views.py

class ListView(generic.ListView):
    queryset = MyObject.objects.all()
    template_name = "myproject/myobjects_list.html"

    def get_queryset(self):
        return self.queryset.filter(author=self.request.user)
listview = login_required(ListView.as_view())

pour la liste d'objets, ok le code est un peu plus court, mais ça ne change pas grand-chose finalement. Voyons maintenant la vue implémentant la création d'objet avec l'enregistrement de l'utilisateur connecté dans le champ author

## views.py

from myproject.forms import MyObjectForm

class CreateView(generic.CreateView):
    form_class = MyObjectForm
    template_name = "myproject/myobject_form.html"

    def form_valid(self, form):
        self.object = form.save(user=self.request.user)
        return super(CreateView, self).form_valid(form)
createview = login_required(CreateView.as_view())   

Ici on arrive à réutiliser toute le code fourni par Django et ne redéfinir que quelques valeurs (form_class, template_name) et juste la fonction form_valid() c'est-à-dire celle qui définit ce qui se passe quand le formulaire est valide et qu'on veut le sauvegarder.

Utilisation

Comme je l'ai dit en introduction, les class-based views ont été implémentées dans Django 1.3 mais vous avez de la chance puisque que le code a été backporté dans Django 1.2 et vous pouvez donc d'ores et déjà les essayer/utiliser même si vous n'avez pas encore migré en 1.3.

Donc si vous êtes en 1.3 vous devez ajouter l'import suivant dans vos views.py

from django.views import generic

Si vous êtes encore en 1.2, il vous faut installer l'application django-cbv

$ pip install django-cbv

Ajouter le middleware qui va bien dans settings.py

MIDDLEWARE_CLASSES = (
    ...
    'cbv.middleware.DeferredRenderingMiddleware',
)

et, dans vos views.py utiliser l'import suivant :

import cbv as generic

Cette technique, directement tirée de l'article de Bruno Renié (que je remercie puisque c'est cet article et la découverte de cette possibilité qui m'a fait enfin m'intéresser sérieusement aux CBV, tout en restant en Django 1.2), vous permet ensuite de aisément migrer en 1.3 en n'ayant besoin de changer que l'import.

Aller plus loin

Bon, ça c'était un exemple simple mais bien évidemment les CBV sont à la fois plus complexes et plus puissantes. Je vous encourage fortement à aller lire la doc (présentation et référence) ainsi que le code, mais en gros voici comment ça se passe (de ce que j'en ai compris pour l'instant, n'hésitez pas à me corriger si je dis une connerie) :

  • chaque FBV qui existait auparavant existe maintenant sous la forme d'une CBV et vous pouvez retrouver simplement le même comportement
  • chaque vue générique est formée de un ou plusieurs "mixin"
  • chaque mixin implémente une fonctionnalité comme générer un template, afficher une liste d'objets, créer et afficher des formulaires
  • les vues génériques utilisent les capacités d'héritage des classes pour, en mélangeant (mixant ;-) un ou plusieurs mixins, produire le comportement désiré
  • pour redéfinir le comportement d'une vue, il suffit de redéfinir un ou plusieurs paramètres, voire une ou plusieurs fonctions provenant des mixins, comme je l'ai fait dans l'exemple ci-dessus
  • si vous avez besoin d'utiliser plusieurs fois le même comportement, il vous suffit de définir vos propres mixins et de créer vos propres vues en héritant de ceux-ci et de ceux prédéfinis.

Conclusion

Les exemples donnés ici sont simples, et l'article n'a pas pour vocation à vous transformer en 'powerusers' des CBV mais j'espère qu'il vous permettra de vous donner une première vision de leur fonctionnement et vous encouragera à lire la documentation et le code associé pour découvrir leur puissance.

bonus : si quelqu'un sait (ou trouve) comment utiliser les CBV pour gérer une vue avec plusieurs formulaires en même temps je suis preneur de l'information...

dimanche, novembre 21 2010

gestion et surveillance de processus système avec supervisor, pour gérer des projets Django, Ruby on Rails, WSGI...


Dans cet article, je vais vous présenter supervisor un système de supervision et gestion de processus. Je l'utilise pour gérer les différents processus utilisés pour servir des applications Django, Ruby on Rails ou WSGI. Je vais vous présenter l'outil en lui-même, ses avantages, comment l'installer et le configurer, comment définir les processus qu'on veut gérer avec supervisor en donnant quelques exemples, la commande de gestion supervisorctl et son utilisation possible pour des déploiements automatisés (avec Fabric par exemple).

Introduction

Dans deux précédents articles, j'ai parlé de ma migration de Apache vers Nginx. Ainsi que de l'utilisation de gunicorn et unicorn pour "servir" mes projets Django et Ruby on Rails respectivement. J'avais créé deux scripts init.d pour gérer le lancement automatique de ceux-ci.

Mais cette solution n'était pas entièrement satisfaisante et on m'a alors parlé de supervisor, runit, god... qui sont des logiciels permettant de gérer le lancement de processus personnalisés avec un énorme plus : la supervision des processus lancés avec ce type de programmes : si un processus crashe il est relancé automatiquement !

J'ai donc choisi de tester supervisor et, vu qu'il m'a convaincu, je l'ai gardé. Et en plus il est écrit en Python :)

Installation et configuration

Pour l'installation, vous avez le choix entre les outils fournis par votre distribution (# aptitude install supervisor pour moi) ou bien avec pip install supervisor. Je pars du principe que vous allez utiliser la version fournie par votre distribution (et que vous utilisez Debian/Ubuntu bien sûr !) : en soi l'installation avec pip n'aurait un intérêt que pour avoir une version plus récente du logiciel or supervisor est stable, et ne dois donc plus évoluer beaucoup. L'avantage d'utiliser la version Debian est que tout est déjà prêt : les fichiers init pour le lancement automatique du démon supervisord au démarrage du serveur, l'arborescence dans /etc/supervisord et même une configuration minimale suffisante pour démarrer.
Le seul hic c'est que Debian Lenny (l'actuelle stable) n'a pas supervisor dans ses dépôts, mais une version existe dans testing/Squeeze et est parfaitement installable sur Lenny.

La configuration de supervisor en lui-même se fait dans le fichier supervisord.conf dont la configuration par défaut suffit pour un fonctionnement standard, mais n'hésitez pas à regarder si vous voulez changer quelques options (comme lancer un mini serveur web de contrôle de supervisor).

Définition des processus et exemples

La configuration de chaque processus supervisé se fait par contre via un fichier distinct pour chaque processus et ceux-ci sont stockés sous Debian dans /etc/supervisor/conf.d avec un nom terminant par .conf.

Je ne vais pas entrer dans le détail du fonctionnement de supervisor et donc de la syntaxe et des possibilités des fichiers de configuration mais plutôt vous donnez quelques exemples en expliquant les points intéressants.

application WSGI sous Gunicorn (ici l'interface web de Mercurial)

[program:code_domain_tld]
command=/usr/bin/gunicorn code_domain_tld:application -c /var/www/code_domain_tld/gunicorn.conf.py
directory=/var/www/code_domain_tld
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/code_domain_tld.gunicorn.log

program est le nom du service pour supervisor, c'est celui qui est utilisé ensuite pour le contrôle (cf plus bas). J'ai tendance à mettre le nom de domaine qui est servi par ce service[1]
command est le nom de la commande qui va être lancée et contrôlée par supervisor, ici c'est simplement gunicorn en indiquant le chemin du fichier de configuration. Pour le code_domain_tld.application c'est l'application WSGI qui va être lancée par gunicorn. Pour ce cas précis, c'est un fichier code_domain_tld.py contenant

#!/usr/bin/env python

import os
import sys
from mercurial.hgweb.hgwebdir_mod import hgwebdir
from mercurial.hgweb.request import wsgiapplication

os.environ["HGENCODING"] = "UTF-8"

def make_web_app():
    return hgwebdir("/etc/mercurial/hgweb.code.domain.tld.config")

def application(environ, start_response):
    environ['wsgi.url_scheme'] = environ.get('HTTP_X_URL_SCHEME', 'http')
    app = wsgiapplication(make_web_app)
    return app(environ, start_response)

directory est le répertoire de travail de la commande précédente
user permet de spécifier l'utilisateur qui sera utilisé pour lancer le programme : veuillez à ce qu'il ait les droits sur les fichiers nécessaires et surtout évitez root !
autostart et autorestart me semblent assez clairs : indiquer si le programme (la commande) doit être lancé automatiquement et relancé automatiquement en cas de crash
startsecs est le nombre de secondes à partir desquelles supervisor va vérifier l'état du programme pour voir si il s'est lancé correctement. Si un programme met beaucoup de temps à se lancer on peut jouer sur cette valeur pour que supervisor attende avant de le considérer comme crashé
les options suivantes concernent la journalisation (logs) de ce programme par supervisor

application Django sous Gunicorn

[program:domain_tld]
command=/opt/django/domain_tld/_venv/bin/python /opt/django/domain_tld/_venv/bin/gunicorn_django -c /opt/django/domain_tld/gunicorn.conf.py
directory=/opt/django/domain_tld/projet
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/domain_tld.gunicorn.log

command la différence se situe ici avec l'utilisation d'un interpréteur Python dans un virtualenv. Le reste est similaire à l'exemple au-dessus.

application Ruby on Rails sous Thin

[program:dev_domain_tld
command=/var/lib/gems/1.8/bin/thin start -C /opt/redmine/config/thin.yml
directory=/opt/redmine
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/dev_domain_tld.thin.log

Ici c'est pareil, seule la commande change pour lancer un programme différent Thin[2] pour faire tourner une application Ruby on Rails.

Il est très important de noter que pour que supervisor puisse contrôler vos processus (leur état, les arrêter, les redémarrer) il faut que ceux-ci ne partent pas en tâche de fond ('daemonize'). Pour gunicorn par exemple, il faut spécifier daemon=False dans le fichier de configuration.

L'application de gestion supervisorctl

Supervisor apporte aussi un outil de grande utilité : sa commande supervisorctl qui permet de vérifier l'état des différents processus ainsi que de les contrôler. Voyons quelques exemples de son utilisation en mode interactif.

$ supervisorctl
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41
supervisor>

On voit que quand on lance la commande, on a tout de suite l'information sur l'état des processus gérés par supervisor.

supervisor> status
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41

La commande status renvoie cette même information, mais on peut utiliser status <nom d'un processus> pour avoir uniquement l'état d'un processus

supervisor> stop code_domain_tld
code_domain_tld: stopped
supervisor> start code_domain_tld
code_domain_tld: started
supervisor> restart code_domain_tld
code_domain_tld: stopped
code_domain_tld: started

Assez logiquement les commandes start, stop, restart suivies du nom d'un processus agissent sur le processus en question en l'arrêtant, le démarrant ou le redémarrant.

D'autres commandes peuvent être utiles, tapez help dans supervisorctl pour en savoir d'avantage.

L'énorme avantage de supervisorctl c'est qu'en plus d'être utilisable en mode interactif comme on l'a vu au dessus il est également utilisable en mode non-interactif. Vous prenez les commandes évoquées au-dessus et les indiquez directement en tant qu'arguments à la commande supervisorctl : la commande est lancée par supervisorctl qui vous rend la main aussitôt. Exemple :

$ supervisorctl status
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41
$

J'utilise cette possibilité notamment dans mes scripts Fabric pour déployer rapidement des projets Django et les redémarrer. Un bout de code comme suit mettra à jour la configuration supervisor du projet si nécessaire, et relancera le processus en utilisant la nouvelle configuration :

def wsgiserver():
    filename = '%s.conf' % env.app_name
    dir = '/etc/supervisor/conf.d'
    if exists('%s/%s' % (dir, filename)):
        choice = prompt('supervisord conf file already exists, overwrite (Y/N)?', default='N', validate=r'[YyNn]')
        if choice.upper() == 'N':
            return
    put(filename, '%s/%s' % (dir, filename))
    run('sudo supervisorctl reread')
    run('sudo supervisorctl update')

Liens

Notes

[1] en substituant les . par des _ comme vous l'avez sûrement remarqué : je ne me souviens plus pourquoi mais je pense que à l'origine ça m'était nécessaire pour Python ou Gunicorn et j'ai donc gardé cette habitude

[2] oui avant j'utilisais Unicorn mais j'avais des problèmes qui se sont résolus en changeant de serveur

lundi, avril 26 2010

conférence Boite à outils Django à la DjangoCong 2010

Ce week-end, c'était donc la DjangoCong conférence Django à Marseille. J'y ai présenté une conférence intitulé "Boîte à outils Django" présentant notamment la django-debug-toolbar, django-command-extensions ainsi que Virtualenv, Pip et Fabric.

Ma présentation est disponible ici : Boite à outils Django - DjangoCong 2010.

Merci encore aux organisateurs : David et Jean-Michel et à tous les participants.

mardi, avril 6 2010

Héberger des projets Django avec Nginx et Gunicorn

Mise à jour : j'ai finalement décidé d'utiliser supervisor pour gérer/monitorer ce genre de services,voyez ce billet.

Je continue l'optimisation de mon serveur web (cf le premier épisode) avec cette fois-ci mes projets Django que j'ai migré vers Nginx et Gunicorn. (veuillez noter que Gunicorn permet également de faire tourner d'autres types d'applications Python que Django)

Virtualenv

Pour gérer mes projets Django, j'utilise virtualenv et toute cette doc part donc du principe que vous l'utilisez également. Si ce n'est pas le cas 1. je vous encourage à le faire ;) 2. vous devrez adapter cette doc en conséquence.

Gunicorn

On commence par installer Gunicorn dans chacun des environnements virtuels hébergeant vos projets Django (cf plus bas pour les détails)

$ pip install gunicorn

ou

$ easy_install gunicorn

Ensuite pour chaque projet Django qui sera propulsé par Gunicorn, le plus simple est de créer un fichier de configuration gunicorn.conf.py pour indiquer les options du serveur à son lancement. Voici un exemple de fichier :

backlog = 2048
bind = "unix:/var/run/gunicorn/monprojet.sock"
pidfile = "/var/run/gunicorn/monprojet.pid"
daemon = True
debug = False
workers = 2
logfile = "/opt/django/www_domain_tld/log/gunicorn.log"
loglevel = "info"

Pour bind vous pouvez soit spécifier un socket Unix soit une adresse et un port TCP/IP. Veuillez consulter la doc configuration pour les détails.

Vous pouvez alors lancer gunicorn avec la commande suivante :

$ gunicorn_django -c gunicorn.conf.py

Bien sûr, comme pour l'article précédent que se passe-t-il en cas de redémarrage du serveur ? Il faut relancer les processus gunicorn un par un. Pour éviter ce genre de désagréments j'ai donc créé un script destiné au système de démarrage sysvinit de Debian.

Ce script a comme prérequis :

  • d'avoir pour chaque projet Django
    • un virtualenv dédié dans un répertoire www_domaine_tld
    • dans ce virtualenv un répertoire nommé monprojet avec les sources de celui-ci et notamment les fichiers importants pour Django (settings.py, urls.py...)
  • d'avoir un répertoire /etc/gunicorn/sites
  • dans ce répertoire vous faites un lien vers le virtualenv de chaque projet en lui donnant le nom du répertoire du projet, exemple :
# cd /etc/gunicorn/sites
# ln -s /opt/django/www_domaine_tld monprojet
  • le fichier gunicorn.conf.py est situé dans le virtualenv (donc dans le répertoire parent de celui des sources du projet)
  • il faut que le pid spécifié dans ce fichier soit de la forme monprojet.pid puisque son nom est ainsi extrapolable par rapport au nom du lien dans /etc/gunicorn/sites

Ce qui donne (pour éclaircir tout ça) une arborescence comme suit :

/etc/gunicorn/sites/
                    monprojet1 -> /opt/django/www_domaine1_tld      # lien vers /opt/django/www_domaine1_tld
                    monprojet2 -> /opt/django/www_domaine2_tld      # lien vers /opt/django/www_domaine2_tld

/opt/django/
            www_domaine1_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet1/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn
            www_domaine2_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet2/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn

Et voici le script /etc/init.d/gunicorn

#!/bin/sh 

### BEGIN INIT INFO
# Provides:       gunicorn
# Required-Start: $local_fs $syslog
# Required-Stop:  $local_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: Gunicorn processes
### END INIT INFO

USER=www-data
NAME="gunicorn"
DAEMON="gunicorn_django"
CONFDIR=/etc/gunicorn/sites
PIDDIR=/var/run/gunicorn
VENV_ACTIVATION="source ../bin/activate"
CONFFILE="../gunicorn.conf.py"
OPTIONS="-c $CONFFILE"
RETVAL=0

# source function library
. /lib/lsb/init-functions

# pull in default settings
[ -f /etc/default/gunicorn ] && . /etc/default/gunicorn

start()
{
    echo $"Starting $NAME."
    cd $CONFDIR;
    for d in *; do
        echo -n $d;
        PIDFILE=$PIDDIR/$d.pid
        if [ -f $PIDFILE ]; then
            echo ": already started!"
        else
            cd $d;
            cd $d;
            su -c "$VENV_ACTIVATION; $DAEMON $OPTIONS" $USER && echo ": OK";
        fi
    done
    echo "done"
}

stop()
{
    echo $"Stopping $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -QUIT `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

reload()
{
    echo $"Reloading $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -HUP `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        reload
        ;;
    reload)
        reload
        ;;
    force-reload)
        stop && start
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        RETVAL=1
esac
exit $RETVAL

Nginx

Pour Nginx, on retrouve un fichier de configuration semblable à celui-ci :

upstream www_domaine_tld {
    server      unix:/var/run/gunicorn/monprojet.sock fail_timeout=0;
}

server {
    listen      80;
    server_name www.domaine.tld;

    access_log  /var/log/nginx/www.domaine.tld.access.log;
    error_log   /var/log/nginx/www.domaine.tld.error.log;

    location /media {
        root    /opt/django/www.domaine.tld/lib/python2.5/site-packages/django/contrib/admin/;
      expires       30d;
    }

    root            /opt/django/www.domaine.tld/monprojet/media;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;       

        if (!-f $request_filename) {
            proxy_pass http://www_domaine_tld;
            break;
        }
    }
}

Et voili.

dimanche, février 14 2010

Django : gérer plusieurs domaines avec un seul projet et les mêmes données

Problématique

Toujours en plein dans Django, mon projet actuel est de refaire tous mes sites web hébergés sur divers domaines en Django. Jusque là tout va bien je dirais. Ça se complique un peu quand l'idée est d'utiliser le même projet pour gérer tous ces sites web.

Pourquoi utiliser le même projet ? Plusieurs (bonnes) raisons à cela :

  • simplifier le développement, la maintenance et le déploiement : un projet à gérer c'est mieux que plusieurs
  • partager les mêmes données : c'est à mon avis le point essentiel qui m'intéresse : pouvoir créer du contenu (billet de blog, photo,...) et pouvoir facilement l'affecter à tel ou tel site ou à plusieurs à la fois (cela dépend également des applications, cf plus bas)
  • tout administrer de façon centralisée : c'est la suite du point précédent : pouvoir, dans une interface d'administration unique, créer du contenu et l'affecter à tel ou tel site web sans devoir changer d'interface d'admin

Je vais donc vous proposer ici la solution que j'ai retenu pour servir 2 domaines différents avec Django. Ceux-ci seront pour l'exemple www.domaine1.tld et www.domaine2.tld (ouaouh ! que c'est original ! - ça marche bien évidemment pour un sous-domaine...).

L'historique de cette configuration est expliqué en bas de ce billet pour ceux que ça intéresse.

note importante : puisqu'on m'a signalé que la solution présentée ici n'est pas forcément "propre" au sens Django puisqu'elle implique de changer le nom de divers fichiers de base d'un projet Django, et puisqu'après réflexion je suis d'accord avec cette remarque... voici quelques éclaircissements :

Si vous faites les choses le plus possible dans le style Django, en pensant application réutilisable il est en effet préférable de penser chaque domaine comme un projet séparé et donc d'isoler les fichiers plutôt de cette façon

www_domaine1_tld/
                 __init__.py
                 manage.py
                 settings.py
                 urls.py

www_domaine2_tld/
                 __init__.py
                 manage.py
                 settings.py
                 urls.py

ce qui permet de ne pas modifier les fichiers manage, settings et urls. Cela est encore facilité si votre projet est composé d'applications réutilisables, installables à la manière de modules Python (ce qui doit d'ailleurs être au maximum le cas).

La documentation ci-dessous est donc toujours valable mais il est probablement préférable d'utiliser cette manière de faire... à vous de voir maintenant...

Arborescence

Le premier point à voir est l'organisation des fichiers dans le répertoire du project. Afin de pas trop encombrer celui-ci, les fichiers ayant la même fonction (les fichiers settings.py, urls.py, les templates) sont déplacés dans des sous-répertoires, les fichiers manage.py restant eux à la racine, comme suit :

monprojet/
          www_domaine1_tld_manage.py
          www_domaine2_tld_manage.py
          settings/
                   __init__.py
                   www_domaine1_tld.py
                   www_domaine2_tld.py
                   global_settings.py
          urls/
               __init__.py
               www_domaine1_tld.py
               www_domaine2_tld.py
          templates/
                    www_domaine1_tld
                    www_domaine2_tld

il ne faut pas oublier les fichiers __init__.py aux endroits indiqués.

Fichiers manage.py

Pour exécuter des commandes Django, on peut utiliser $ django-admin.py <commande> <options> avec certaines contraintes (spécifier le fichier settings...) ou le "raccourci" $ ./manage.py <commande> <options>.

Dans le cas où l'on veut gérer plusieurs domaines, et où le fichier "settings" ne s'appelle pas settings.py, il faut créer un fichier manage.py différent par domaine.

Exemple avec www_domaine1_tld_manage.py.

#!/usr/bin/env python
from django.core.management import execute_manager
try:
    import settings.www_domaine1_tld
except ImportError:
    import sys
    sys.stderr.write("Error: Can't find the file 'settings.www_domaine1_tld.py' in the directory containing %r...." % __file__)
    sys.exit(1)

if __name__ == "__main__":
    execute_manager(settings.www_domaine1_tld)

Il suffira alors de lancer une commande comme la suivante pour exécuter des commandes Django sur un certain domaine

$ ./www_domaine1_tld_manage.py <commande> <options>

mise à jour : vu qu'on me l'a signalé, cette manipulation concernant les fichiers manage.py n'est pas nécessaire puisqu'il suffit d'ajouter l'option --settings=settings.www_domaine1_tld pour pouvoir utiliser ./manage.py de manière classique

Fichiers urls.py

Pour les fichiers urls, il suffit de créer les fichiers suivant l'arborescence indiquée au-dessus en utilisant des fichiers urls.py "classiques", avec la différence que si vous devez importer les "settings" du domaine il faut indiquer le module sous la forme settings.www_domaine1_tld

Framework "sites" de Django

Si vous voulez utiliser les mêmes applications sur plusieurs domaines sans pour autant devoir publier le même contenu partout, il faut alors utiliser des applications qui utilisent le framework "sites".

Vous devez dans ce cas :

  • créer des sites dans la base de données
  • et spécifier le SITE_ID qui va bien dans le fichier "settings" (voir plus bas)

Fichiers settings.py

Pour gérer les paramètres de chaque domaine/site on aura un fichier pour chacun ainsi qu'un fichier pour les paramètres globaux (pour les bases de données, les applications installées...).

Voici les paramètres spécifiques à chaque domaine, avec des commentaires.

# paramètres Django pour le site www.domaine1.tld

import os

# on charge ici les paramètres spécifiés dans global_settings.py
from global_settings import *

# on initialise BASE_DIR avec le répertoire de base du projet,
#  pour l'utiliser pour d'autres paramètres
BASE_DIR = os.path.join(os.path.dirname(__file__), '..')

# il faut indiquer ici l'identifiant du site (au sens Django), ansi que dans la base de données
# cf http://docs.djangoproject.com/en/dev/ref/contrib/sites/
SITE_ID = 1

# chemin absolu vers le répertoire des médias du domaine
MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'www_domaine1_tld')

# URL pour gérer les médias venant de MEDIA_ROOT
MEDIA_URL = 'http://www.domaine1.tld/static/'

# il faut ici initialiser une clé secrète distincte pour chaque domaine
SECRET_KEY = 'xxx'

# on spécifie ici le fichier urls.py
ROOT_URLCONF = 'monprojet.urls.www_domaine1_tld'

# on spécifie ici le répertoires des templates
TEMPLATE_DIRS = (
    os.path.join(BASE_DIR, 'templates', 'www_domaine1_tld')
)

# ce qui suit n'est pas obligatoire mais permet d'avoir un fichier spécifique
# en local afin par exemple de surcharger quelques paramètres pour le dev
try:
     from local_settings import *
except ImportError:
     pass

Et voici un exemple de fichier global_settings.py avec les paramètres qui peuvent/doivent être partagés.

DATABASE_ENGINE = ''
DATABASE_NAME = ''
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
)

Serveur web/Python/WSGI

Pour cette partie, j'utilise pour ma part (pour l'instant) Apache+mod_wsgi mais je ne vais pas rentrer dans les détails puisque ça n'est pas spécifique au cas particulier qui nous intéresse ici.

J'ai un fichier pour Apache qui indique un VirtualHost pour chaque domaine et pointe vers un fichier WSGI pour chacun également.

Dans le fichier WSGI il faut faire attention à étendre le PYTHONPATH si nécessaire et indiquer le fichier settings correctement, un peu comme ce qui suit :

import sys

sys.path.append('/opt/django/monprojet/')
os.environ['DJANGO_SETTINGS_MODULE'] = 'monprojet.settings.www_domaine1_tld'

Historique

Au début de ma recherche sur cette problématique je me suis tout de suite dit que c'était le boulot de Django de faire ça, et quand j'ai compris que ce n'était pas le cas naturellement, je me suis orienté vers des applications qui, étendant Django, lui permettait de gérer cela. J'ai trouvé deux applications dont le but semble de combler ce manque :

Je ne me souviens plus pourquoi j'ai mis de côté django-multisite mais mes tests ont porté sur django-threaded-multihost. L'application faisait ce pourquoi elle était prévu mais avait un problème majeur : il n'y avait qu'un settings.py, un urls.py... pour tous les domaines servis et là je me suis rendu compte que ce n'était pas vraiment ce dont j'avais besoin puisque le but était de pouvoir servir le même contenu mais bien évidemment de façon différente... Retour à la case départ !

Heureusement je suis tombé sur une discussion sur IRC #django-fr entre david`bgk et cyberdelia concernant ce thème et cyberdelia m'a alors expliqué comment ses divers domaines (pour http://www.croisedanslemetro.com il me semble (j'aime le concept de ce site :) soit dit en passant) pointant vers un même projet étaient gérés... c'était ce qu'il me fallait ! Le temps que ça mûrisse, celui de m'y replonger, d'implémenter la solution et de la tester, je me suis dit qu'il serait intéressant de poster cette solution et me voilà donc... (en ajoutant le temps de rédaction de ce billet ;-)... j'espère que ça pourra être utile à d'autres.

lundi, mars 2 2009

django-userthemes : une application pour gérer des thèmes utilisateur pour Django

Un petit billet pour annoncer la sortie de la version 0.1 de l’application django-userthemes. C’est une “application réutilisable” pour Django[1].

L’origine de cette application est le besoin de créer une système de thèmes pour un projet sur lequel je travaille. Après moultes recherches[2], j’ai décidé de créer ma propre implémentation en essayant au maximum de suivre cette philosophie de ‘reusable apps’.

django-userthemes permet donc de définir pour chaque utilisateur enregistré dans une application un thème favori qui sera chargé quand il se connectera à celle-ci. On peut définir le répertoire où seront stockés les thèmes et aussi celui par défaut qui sera chargé quand aucun utilisateur n’est connecté ou que sa préférence n’est pas fixée. Je vous encourage à lire la doc (et le code) pour comprendre comment ça fonctionne en détail.

Vous trouverez le projet à cette adresse : http://bitbucket.org/daks/django-us… où vous pourrez récupérer le code source (via Mercurial ou un fichier archive), rapporter des bugs…

La license utilisée en la GNU GPLv2.

Le projet est encore en phase de développement donc tout retour est bienvenue.

Si vous vous décidez à l’utiliser dans un de vos projets, merci de le dire en utilisant un widget ohloh comme celui-ci :

Notes

[1] voyez cette vidéo de James Bennett et cette convention de Eric Holscher pour plus d’infos

[2] me conduisant vers des projets morts ou ne correspondant pas à nos besoins/désirs : django-themes, django-skins, django-userskins, django-dbtemplates