Commits

David Larlet committed 033cffd

Ajout du tutoriel de création de blog de Gilles Fabio

Comments (0)

Files changed (2)

docs/contrib/creer-un-blog-avec-django.txt

+Créer un blog avec Django
+=========================
+
+Introduction
+------------
+
+Créer un blog avec Django_, **c'est simple**. Oui, vraiment. Ce tutorial 
+s'adresse aux personnes connaissant déjà un peu le langage de programmation 
+Python_ et disposant d'un environnement de développement Django opérationnel. 
+
+Brièvement, cela se résume à :
+
+- Python (interpréteur, libraires... )
+- Un gestionnaire de bases de données et son *binding*
+- Un terminal (ou "console")
+- Un navigateur web
+- Un éditeur de texte
+
+Si vous désirez installer Django sur votre machine, n'hésitez pas à consulter 
+la section `Quick install guide`_ de la documentation. 
+
+.. _Django: http://www.djangoproject.com
+.. _Python: http://www.python.org
+.. _la documentation: http://docs.djangoproject.com/en/dev/
+.. _Quick install guide: http://docs.djangoproject.com/en/dev/intro/install/#intro-install
+
+Mise en place du projet
+-----------------------
+
+Dans ce tutorial, nous allons utiliser MySQL_ comme gestionnaire de bases de
+données mais vous pouvez utiliser n'importe quel autre gestionnaire de bases 
+de données supporté par Django. 
+
+Il faut donc, tout d'abord, créer une base de données : ::
+
+    $ mysql -u user -p
+    mysql> CREATE DATABASE blog;
+
+Remplacez ``user`` par l'utilisateur qui va bien (root ou un utilisateur ayant 
+les droits de création de base). Comme il vaut mieux éviter d'utiliser 
+l'utilisateur root, même en local, nous allons créer un utilisateur spécifique 
+pour la base et lui donner les permissions nécessaires. Nous allons nommer cet 
+utilisateur ``blog``, du même nom que la base : ::
+
+    mysql> GRANT ALL ON blog.* TO blog@localhost IDENTIFIED BY 'password';
+    mysql> FLUSH PRIVILEGES;
+    mysql> \q
+
+Bien sûr, remplacez ``password`` par un vrai mot de passe.
+
+Notre base est créée.
+
+Dans un dossier de votre répertoire personnel (si possible, dans un dossier 
+dédié à vos projets de programmation), nous allons créer le projet Django 
+``website`` et l'application ``blog``::
+
+    $ django-admin startproject website
+    $ cd website
+    $ django-admin startapp blog
+
+Dans le répertoire du projet ``website``, nous allons créer un dossier 
+``templates`` (qui contiendra nos templates) et un dossier ``media`` qui 
+contiendra les fichiers statiques : ::
+
+    $ mkdir templates
+    $ mkdir media
+
+Pour bien séparer nos applications, nous allons les placer dans un répertoire 
+``apps`` à la racine du projet : ::
+
+    $ mkdir apps
+    $ touch apps/__init__.py
+    $ mv blog apps/
+
+La commande ``tree`` devrait retourner cette arborescence : ::
+
+    .
+    |-- __init__.py
+    |-- apps
+    |   |-- __init__.py
+    |   `-- blog
+    |       |-- __init__.py
+    |       |-- models.py
+    |       `-- views.py
+    |-- manage.py
+    |-- media
+    |-- settings.py
+    |-- templates
+    `-- urls.py
+
+Le projet est en place. Exécutez la commande suivante : ::
+
+    $ python manage.py runserver
+
+Dans votre navigateur, pointez l'adresse : http://127.0.0.1:8000.
+
+Bienvenue sous Django ! Control + C pour stopper le serveur. 
+
+.. _MySQL: http://www.mysql.org
+
+Paramètres et URLs
+------------------
+
+Éditez le fichier ``settings.py`` à l'aide de votre éditeur favori. 
+
+Tout d'abord, on crée la constante ``PROJECT_PATH`` (en haut du fichier) afin
+de stocker le chemin absolu vers notre projet : ::
+
+    import os.path
+    PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))
+
+C'est pratique si vous utilisez différents systèmes d'exploitation ou
+différentes machines. Le chemin est automatiquement détecté.
+
+Ensuite, on passe à la base de données : ::
+
+    DATABASE_ENGINE = 'mysql'
+    DATABASE_NAME = 'blog'
+    DATABASE_USER = 'blog'
+    DATABASE_PASSWORD = 'password'
+
+On ajuste la *timezone* : ::
+
+    TIME_ZONE = 'Europe/Paris'
+
+On ajuste la langue par défaut : ::
+
+    LANGUAGE_CODE = 'fr-fr'
+
+On ajoute notre répertoire ``media`` : ::
+
+    MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media/')
+
+On ajoute l'URL vers les médias : ::
+
+    MEDIA_URL = '/media/'
+
+On ajuste l'URL vers les médias de l'interface d'administration : ::
+
+    ADMIN_MEDIA_PREFIX = '/media/admin/'
+
+On ajoute notre répertoire ``templates`` : ::
+
+    TEMPLATE_DIRS = (
+        os.path.join(PROJECT_PATH, 'templates'),
+    )
+
+On ajoute notre application ``blog`` à la liste ``INSTALLED_APPS`` : ::
+
+    INSTALLED_APPS = (
+        'django.contrib.auth',
+        'django.contrib.contenttypes',
+        'django.contrib.sessions',
+        'django.contrib.sites',
+        'website.apps.blog',
+    )
+
+On enregistre les modifications et on passe aux URLs.
+
+Éditez le fichier ``urls.py``. Nous allons ajouter le support des médias. Django 
+va donc prendre en charge les fichiers statiques (pratique quand on développe 
+en local mais à proscrire en production). Pour ce faire, on importe le module
+``settings`` pour récupérer ``MEDIA_ROOT`` (le chemin absolu vers le répertoire 
+``media``) et on ajoute un *urlpatterns* pour ``django.views.static.serve`` : :: 
+
+    from django.conf.urls.defaults import *
+    from django.conf import settings
+
+    # Uncomment the next two lines to enable the admin:
+    # from django.contrib import admin
+    # admin.autodiscover()
+
+    urlpatterns = patterns('',
+        # Example:
+        # (r'^website/', include('website.foo.urls')),
+
+        # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
+        # to INSTALLED_APPS to enable admin documentation:
+        # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+        # Uncomment the next line to enable the admin:
+        # (r'^admin/(.*)', admin.site.root),
+    )
+
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$',
+            'django.views.static.serve',
+            {
+                'document_root': settings.MEDIA_ROOT, 
+                'show_indexes': True,
+            },
+        ),
+    )
+
+On enregistre les modifications et on passe à l'installation de l'interface 
+d'administration.
+
+Installation de l'interface d'administration
+--------------------------------------------
+
+Django embarque une interface d'administration sympathique et pratique. 
+L'installation se fait en trois étapes : ajout de l'application dans le fichier 
+``settings.py``, ajout des URLs et synchronisation de la base de données.
+
+Éditez le fichier ``settings.py`` et ajoutez ``django.contrib.admin`` dans la liste 
+``INSTALLED_APPS`` : ::
+
+    INSTALLED_APPS = (
+        'django.contrib.auth',
+        'django.contrib.contenttypes',
+        'django.contrib.sessions',
+        'django.contrib.sites',
+        'django.contrib.admin',
+        'website.apps.blog',
+    )
+
+Enregistrez les modifications. 
+
+Éditez le fichier ``urls.py`` et ajoutez le support de l'admin en décommentant les 
+lignes indiquées dans les commentaires, soit trois lignes au total : ::
+
+    from django.conf.urls.defaults import *
+    from django.conf import settings
+
+    # Uncomment the next two lines to enable the admin:
+    from django.contrib import admin
+    admin.autodiscover()
+
+    urlpatterns = patterns('',
+        # Example:
+        # (r'^website/', include('website.foo.urls')),
+
+        # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
+        # to INSTALLED_APPS to enable admin documentation:
+        # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+        # Uncomment the next line to enable the admin:
+        (r'^admin/(.*)', admin.site.root),
+    )
+
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$',
+            'django.views.static.serve',
+            {
+                'document_root': settings.MEDIA_ROOT, 
+                'show_indexes': True,
+            },
+        ),
+    )
+
+Enregistrez les modifications.
+
+Il ne reste plus qu'à synchroniser avec la base de données (à exécuter à la 
+racine du projet) : ::
+
+    $ python manage.py syncdb
+
+Django vous guidera dans la création d'un compte super-utilisateur.
+
+Lancez le serveur : ::
+
+    $ python manage.py runserver
+
+Dans votre navigateur, pointer l'adresse : http://127.0.0.1:8000/admin/. Entrez 
+votre identifiant et votre mot de passe super-utilisateur.  Bienvenue dans 
+l'interface d'administration de Django !
+
+Écriture des tests
+------------------
+
+Oui, écrire les tests avant le code, c'est mieux. Ça permet d'éviter des bogues 
+et des prises de tête. Le but est le suivant : faire en sorte que tous les tests 
+passent. Prêt ? Alors créez un fichier ``tests.py`` à la racine de l'application 
+``blog``. On commence par importer la classe ``TestCase`` du module ``django.test`` 
+et on crée une classe ``BlogTest`` qui contiendra nos tests : ::
+
+    # -*- coding: utf-8 -*-
+    from django.test import TestCase
+
+    class BlogTest(TestCase): 
+        pass
+
+On lance les tests (à exécuter à la racine du projet) : ::
+
+    $ python manage.py test
+    Creating test database...
+    Creating table auth_permission
+    Creating table auth_group
+    Creating table auth_user
+    Creating table auth_message
+    Creating table django_content_type
+    Creating table django_session
+    Creating table django_site
+    Creating table django_admin_log
+    Installing index for auth.Permission model
+    Installing index for auth.Message model
+    Installing index for admin.LogEntry model
+    ................
+    ----------------------------------------------------------------------
+    Ran 16 tests in 2.134s
+
+    OK
+    Destroying test database...
+
+Tous les tests passent ! Normal, nous n'en avons écrit aucun. Au boulot !
+
+Notre classe de test : ::
+
+    # -*- coding: utf-8 -*-
+    from django.test import TestCase
+    from django.core.urlresolvers import reverse
+
+    class BlogTest(TestCase):
+        """
+        Tests of ``blog`` application.
+    
+        """
+        fixtures = ['test_data']
+    
+        def test_entry_archive_index(self):
+            """
+            Tests ``entry_archive`` view.
+        
+            """
+            response = self.client.get(reverse('blog'))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/entry_archive.html')
+
+        def test_entry_archive_year(self):
+            """
+            Tests ``entry_archive_year`` view.
+        
+            """
+            response = self.client.get(reverse('blog_year', args=['2009']))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/entry_archive_year.html')
+        
+        def test_entry_archive_month(self):
+            """
+            Tests ``entry_archive_month``view.
+        
+            """
+            response = self.client.get(reverse('blog_month', args=['2009', '01']))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/entry_archive_month.html')
+        
+        def test_entry_archive_day(self):
+            """
+            Tests ``entry_archive_day`` view.
+        
+            """
+            response = self.client.get(reverse('blog_day', args=['2009', '01', '28']))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/entry_archive_day.html')
+        
+        def test_entry_detail(self):
+            """
+            Tests ``entry_detail`` view.
+        
+            """
+            response = self.client.get(reverse('blog_entry', args=['2009', '01', '28', 'test-entry']))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/entry_detail.html')
+        
+        def test_entry_detail_not_found(self):
+            """
+            Test ``entry_detail`` view with an offline entry.
+        
+            """
+            response = self.client.get(reverse('blog_entry', args=['2009', '01', '28', 'offline-entry']))
+            self.failUnlessEqual(response.status_code, 404)
+        
+        def test_category_detail(self):
+            """
+            Tests ``category_detail`` view.
+        
+            """
+            response = self.client.get(reverse('blog_category', args=['test']))
+            self.failUnlessEqual(response.status_code, 200)
+            self.assertTemplateUsed(response, 'blog/category_detail.html')
+        
+        def test_category_detail_not_found(self):
+            """
+            Tests ``category_detail`` view with an offline category.
+        
+            """
+            response = self.client.get(reverse('blog_category', args=['offline']))
+            self.failUnlessEqual(response.status_code, 404)
+
+
+La fonction ``reverse`` est utilisée pour récupérer l'URL en fonction de son 
+nom (URLs nommées). Pour en savoir plus, n'hésitez pas à consulter la section 
+`URL dispatcher`_ de la documentation. Nous testons ici la réponse et le template 
+(pour vérifier que la future vue renverra bien le bon template). Si vous relancez 
+les tests, vous devriez vous faire insulter. C'est normal. Nous n'avons encore rien 
+implémenté. Donc, passons à l'implémentation.
+
+.. _URL dispatcher: http://docs.djangoproject.com/en/dev/topics/http/urls/
+
+Création des modèles
+--------------------
+
+Nous allons réaliser un blog "basique" composé de deux modèles : ``Entry`` et
+``Category``. Le premier modèle représente un billet de blog et le deuxième une 
+catégorie pour classer les billets par thème. 
+
+Un billet est composé des champs suivants :
+
+- Un titre
+- Un slug (aussi appelé "permalien")
+- Un auteur
+- Une catégorie
+- Une date de création
+- Une date de modification
+- Une date de publication
+- Un statut (en ligne / hors ligne)
+- Un corps au format HTML
+
+Une categorie est composée des champs suivants :
+
+- Un nom
+- Un slug (aussi appelé "permalien")
+- Une date de création
+- Une date de modification
+
+Éditez le fichier ``models.py`` du répertoire ``blog``. Ce fichier contiendra les 
+modèles de notre application. Pour en savoir plus, n'hésitez pas à consulter 
+la section `Writing models`_ de la documentation. 
+
+Nos modèles : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    Models of ``blog`` application.
+
+    """
+    # Standard library
+    from datetime import datetime
+    # Django
+    from django.db import models
+    from django.utils.translation import ugettext_lazy as _
+
+    class Category(models.Model):
+        """
+        A blog category.
+    
+        """
+        # Fields
+        name = models.CharField(_('name'), max_length=255)
+        slug = models.SlugField(_('slug'), max_length=255, unique=True)
+        creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+        modification_date = models.DateTimeField(_('modification date'), auto_now=True)
+    
+        class Meta:
+            verbose_name = _('category')
+            verbose_name_plural = _('categories')
+        
+        def __unicode__(self):
+            return u'%s' % self.name
+
+        @models.permalink
+        def get_absolute_url(self):
+            return ('blog_category', (), {
+                'slug': self.slug,
+            })
+
+    class Entry(models.Model):
+        """
+        A blog entry.
+    
+        """
+        # Status choices
+        STATUS_OFFLINE = 0
+        STATUS_ONLINE = 1
+        STATUS_DEFAULT = STATUS_OFFLINE
+        STATUS_CHOICES = (
+            (STATUS_OFFLINE, _('Offline')),
+            (STATUS_ONLINE, _('Online')),
+        )
+    
+        # Fields
+        title = models.CharField(_('title'), max_length=255)
+        slug = models.SlugField(_('slug'), max_length=255, unique_for_date='publication_date')
+        author = models.ForeignKey('auth.User', verbose_name=_('author'))
+        category = models.ForeignKey(Category, verbose_name=_('category'))
+        creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+        modification_date = models.DateTimeField(_('modification date'), auto_now=True)
+        publication_date = models.DateTimeField(_('publication date'), default=datetime.now(), db_index=True)
+        status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=STATUS_DEFAULT, db_index=True)
+        body = models.TextField(_('body'))
+    
+        class Meta:
+            verbose_name = _('entry')
+            verbose_name_plural = _('entries')
+    
+        def __unicode__(self):
+            return u'%s' % self.title
+
+        @models.permalink
+        def get_absolute_url(self):
+            return ('blog_entry', (), {
+                'year': self.publication_date.strftime('%Y'),
+                'month': self.publication_date.strftime('%m'),
+                'day': self.publication_date.strftime('%d'),
+                'slug': self.slug,
+            })
+
+La syntaxe du langage Python est tellement *clean* que le code parle de lui-même. 
+
+Nos modèles sont *i18n-ready* (via la fonction magique ``_()``).
+
+Avant de créer nos tables, il est recommandé de vérifier si les modèles ne 
+comportent aucune erreur. Pour ce faire, à la racine du projet, on exécute 
+la commande suivante : ::
+
+    $ python manage.py validate
+    0 errors found
+
+Si cette commande renvoie des erreurs, il suffira de les corriger.
+
+Tout est OK. On synchronise avec la base de données: :: 
+
+    $ python manage.py syncdb
+    Creating table blog_category
+    Creating table blog_entry
+    Installing index for blog.Entry model
+
+Par la suite, dans nos templates, nous afficheront uniquement les billets ayant 
+pour statut "en ligne". Lors de la récupération de nos objets, on peut très bien 
+filtrer sur ce champ. Mais parce qu'on est feignant, on va créer des *managers* 
+pour s'épargner du code. 
+
+Les méthodes d'un manager s'appliquent à une table, tandis que les méthodes d'un 
+modèle s'appliquent à un objet. Donc, si nous voulons récupérer tous les billets 
+ayant pour statut "en ligne", nous avons besoin d'un manager. Si nous voulons 
+récupérer le nom complet de l'auteur du billet, nous devons définir une méthode 
+spécifique dans le modèle.
+
+Nous avons besoin de deux managers : un pour manipuler uniquement les billets 
+"en ligne" et un autre pour manipuler uniquement les catégories ayant des billets 
+"en ligne" (c'est-à-dire que si nous rédigeons un seul billet dans une catégorie et 
+que ce billet est "hors ligne", la catégorie ne doit pas exister publiquement). 
+
+Dans le répertoire de notre application, on crée un fichier (ou plutôt, un *module*) 
+nommé ``managers.py``. N'hésitez pas à consulter la section Managers_ de la 
+documentation pour en savoir plus.
+
+Nos managers : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    Managers of ``blog`` application.
+
+    """
+    from django.db import models
+
+    class CategoryOnlineManager(models.Manager):
+        """
+        Manager that manages online ``Category`` objects.
+    
+        """
+        def get_query_set(self):
+            from website.apps.blog.models import Entry
+            entry_status = Entry.STATUS_ONLINE
+            return super(CategoryOnlineManager, self).get_query_set().filter(
+                entry__status=entry_status).distinct()
+    
+    class EntryOnlineManager(models.Manager):
+        """
+        Manager that manages online ``Entry`` objects.
+    
+        """
+        def get_query_set(self):
+            return super(EntryOnlineManager, self).get_query_set().filter(
+                status=self.model.STATUS_ONLINE)
+
+Il faut maintenant ajouter ces managers dans nos modèles : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    Models of ``blog`` application.
+
+    """
+    # Standard library
+    from datetime import datetime
+    # Django
+    from django.db import models
+    from django.utils.translation import ugettext_lazy as _
+    # Application
+    from website.apps.blog.managers import CategoryOnlineManager
+    from website.apps.blog.managers import EntryOnlineManager
+
+    class Category(models.Model):
+        """
+        A blog category.
+    
+        """
+        # Fields
+        name = models.CharField(_('name'), max_length=255)
+        slug = models.SlugField(_('slug'), max_length=255, unique=True)
+        creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+        modification_date = models.DateTimeField(_('modification date'), auto_now=True)
+    
+        # Managers
+        objects = models.Manager()
+        online_objects = CategoryOnlineManager()
+    
+        class Meta:
+            verbose_name = _('category')
+            verbose_name_plural = _('categories')
+        
+        def __unicode__(self):
+            return u'%s' % self.name
+
+        @models.permalink
+        def get_absolute_url(self):
+            return ('blog_category', (), {
+                'slug': self.slug,
+            })
+
+    class Entry(models.Model):
+        """
+        A blog entry.
+    
+        """
+        # Status choices
+        STATUS_OFFLINE = 0
+        STATUS_ONLINE = 1
+        STATUS_DEFAULT = STATUS_OFFLINE
+        STATUS_CHOICES = (
+            (STATUS_OFFLINE, _('Offline')),
+            (STATUS_ONLINE, _('Online')),
+        )
+    
+        # Fields
+        title = models.CharField(_('title'), max_length=255)
+        slug = models.SlugField(_('slug'), max_length=255, unique_for_date='publication_date')
+        author = models.ForeignKey('auth.User', verbose_name=_('author'))
+        category = models.ForeignKey(Category, verbose_name=_('category'))
+        creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+        modification_date = models.DateTimeField(_('modification date'), auto_now=True)
+        publication_date = models.DateTimeField(_('publication date'), default=datetime.now(), db_index=True)
+        status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=STATUS_DEFAULT, db_index=True)
+        body = models.TextField(_('body'))
+    
+        # Managers
+
+        objects = models.Manager()
+        online_objects = EntryOnlineManager()
+    
+        class Meta:
+            verbose_name = _('entry')
+            verbose_name_plural = _('entries')
+    
+        def __unicode__(self):
+            return u'%s' % self.title
+
+        @models.permalink
+        def get_absolute_url(self):
+            return ('blog_entry', (), {
+                'year': self.publication_date.strftime('%Y'),
+                'month': self.publication_date.strftime('%m'),
+                'day': self.publication_date.strftime('%d'),
+                'slug': self.slug,
+            })
+
+Nous avons des modèles, des managers, une interface d'administration... Ah tiens, 
+et si on ajoutait nos modèles dans l'admin ? Il serait peut-être temps de 
+rédiger quelques billets et de créer quelques catégories pour nos tests.
+
+.. _Writing models: http://docs.djangoproject.com/en/dev/topics/db/models/#topics-db-models
+.. _Managers: http://docs.djangoproject.com/en/dev/topics/db/managers/
+
+Ajout des modèles dans l'interface d'administration
+---------------------------------------------------
+
+Pour ajouter nos modèles dans l'interface d'administration, nous devons créer 
+une classe de type ``ModelAdmin`` par modèle. Chaque classe embarquera des 
+options et des méthodes propres à l'admin. Par convention, on placera ces 
+classes dans un module ``admin.py`` dans le répertoire de l'application. 
+N'hésitez pas à consulter la section `The Django admin site`_ de la 
+documentation pour en savoir plus.
+
+Créez le fichier ``admin.py`` dans le répertoire ``blog``.
+
+Nos classes admin : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    Administration interface options of ``blog`` application.
+
+    """
+    # Django
+    from django.contrib import admin
+    # Application
+    from website.apps.blog.models import Category
+    from website.apps.blog.models import Entry
+
+    class CategoryAdmin(admin.ModelAdmin):
+        """
+        Administration interface options of ``Category`` model.
+    
+        """
+        pass
+    
+    class EntryAdmin(admin.ModelAdmin):
+        """
+        Administration interface options of ``Entry`` model.
+    
+        """
+        pass
+    
+    admin.site.register(Category, CategoryAdmin)
+    admin.site.register(Entry, EntryAdmin)
+
+Pour l'instant, on se contente du minimum. Il est possible de presque tout 
+personnaliser. Nous allons quand même améliorer un peu. Voici une version un 
+peu plus peaufinée : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    Administration interface options of ``blog`` application.
+
+    """
+    # Django
+    from django.contrib import admin
+    from django.utils.translation import ugettext_lazy as _
+    # Application
+    from website.apps.blog.models import Category
+    from website.apps.blog.models import Entry
+
+    class CategoryAdmin(admin.ModelAdmin):
+        """
+        Administration interface options of ``Category`` model.
+    
+        """
+        list_display = ('name', 'slug', 'creation_date', 'modification_date')
+        search_fields = ('name',)
+        date_hierarchy = 'creation_date'
+        save_on_top = True
+        prepopulated_fields = {'slug': ('name',)}
+    
+    class EntryAdmin(admin.ModelAdmin):
+        """
+        Administration interface options of ``Entry`` model.
+    
+        """
+        list_display = ('title', 'category', 'status', 'author')
+        search_fields = ('title', 'body')
+        date_hierarchy = 'publication_date'
+        fieldsets = (
+            (_('Headline'), {'fields': ('author', 'title', 'slug', 'category')}),
+            (_('Publication'), {'fields': ('publication_date', 'status')}),
+            (_('Body'), {'fields': ('body',)}),
+        )
+        save_on_top = True
+        radio_fields = {'status': admin.VERTICAL}
+        prepopulated_fields = {'slug': ('title',)}
+    
+    admin.site.register(Category, CategoryAdmin)
+    admin.site.register(Entry, EntryAdmin)
+
+Maintenant qu'on peut créer des billets et des catégories, nous allons en 
+profiter pour créer des fixtures pour nos tests. Les fixtures sont des données 
+de test.
+
+.. _The Django admin site: http://docs.djangoproject.com/en/dev/ref/contrib/admin/
+
+Création des fixtures
+---------------------
+
+On crée deux catégories. La première : 
+
+- Titre : Test
+- Slug : test
+
+La seconde :
+
+- Titre : Offline
+- Slug : offline
+
+Et deux billets. Le premier :
+
+- Titre : Test Entry
+- Slug : test-entry
+- Catégorie : Test
+- Date de publication : 2009-01-28 00:00:00
+- Statut : en ligne
+- Corps : peu importe, ce que vous voulez
+
+Le second :
+
+- Titre : Offline
+- Slug : offline-entry
+- Catégorie : Offline
+- Date de publication : 2009-01-28 00:00:00
+- Statut : hors ligne
+- Corps : peu importe, ce que vous voulez
+
+Une fois ces données sauvegardées, on va les exporter au format JSON pour
+pouvoir les réutiliser automatiquement dans nos tests. 
+
+Créons tout d'abord un répertoire ``fixtures`` dans le répertoire de l'application. 
+
+Puis, exécutez cette commande à la racine du projet : ::
+
+    $ python manage.py dumpdata blog --indent=4 > apps/blog/fixtures/test_data.json
+
+Nos fixtures sont prêtes. Passons aux URLs.
+
+Création des URLs
+-----------------
+
+Nous n'avons même pas besoin de créer de vue pour notre application puisque nous 
+allons utiliser les vues génériques de Django. N'hésitez pas à consulter les 
+sections `Generic Views`_ et `URL dispatcher`_ de la documentation pour en 
+savoir plus.
+
+Par convention, les URLs seront contenues dans le module ``urls.py`` dans le 
+répertoire de l'application (ce fichier n'existe pas, donc pensez à le créer). 
+
+Nos URLs : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    URLs of ``blog`` application.
+
+    """
+    # Django
+    from django.conf.urls.defaults import *
+    # Application
+    from website.apps.blog.models import Entry
+    from website.apps.blog.models import Category
+
+    urlpatterns = patterns('',
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)/$',
+            'django.views.generic.date_based.object_detail',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',
+                slug_field='slug',
+            ),
+            name='blog_entry',
+        ),
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/$',
+            'django.views.generic.date_based.archive_day',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',     
+            ),
+            name='blog_day',
+        ),
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/$',
+            'django.views.generic.date_based.archive_month',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',
+            ),
+            name='blog_month',
+        ),
+        url(r'^(?P<year>\d{4})/$',
+            'django.views.generic.date_based.archive_year',
+            dict(
+                queryset=Entry.online_objects.all(),
+                make_object_list=True,
+                date_field='publication_date',
+            ),
+            name='blog_year',
+        ),
+        url(r'^category/(?P<slug>[\w-]+)/$',
+            'django.views.generic.list_detail.object_detail',
+            dict(
+                queryset=Category.online_objects.all(),
+                slug_field='slug'
+            ),
+            name='blog_category',
+        ),
+        url(r'^$',
+            'django.views.generic.date_based.archive_index', 
+            dict(
+                queryset=Entry.online_objects.all(),
+                date_field='publication_date',
+            ),
+            name='blog',
+        ),
+    )
+
+Notre projet n'est pas encore au courant de ces URLs. Éditez le fichier ``urls.py`` 
+à la racine du projet et ajoutez le module via la fonction ``include`` : ::
+
+    from django.conf.urls.defaults import *
+    from django.conf import settings
+
+    # Uncomment the next two lines to enable the admin:
+    from django.contrib import admin
+    admin.autodiscover()
+
+    urlpatterns = patterns('',
+        # Example:
+        # (r'^website/', include('website.foo.urls')),
+        (r'', include('website.apps.blog.urls')),
+    
+        # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
+        # to INSTALLED_APPS to enable admin documentation:
+        # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+        # Uncomment the next line to enable the admin:
+        (r'^admin/(.*)', admin.site.root),
+    )
+
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$',
+            'django.views.static.serve',
+            {
+                'document_root': settings.MEDIA_ROOT, 
+                'show_indexes': True,
+            },
+        ),
+    )
+
+Relançons nos tests : ::
+
+    $ python manage.py test
+
+Ça ne passe toujours pas mais vous avez certainement remarqué que les erreurs 
+sont différentes. Vous ne devriez voir que des erreurs de templates. Il y a 
+donc une progression !
+
+Passons à la création des templates. 
+
+.. _Generic Views: http://docs.djangoproject.com/en/dev/ref/generic-views/#ref-generic-views
+.. _URL dispatcher: http://docs.djangoproject.com/en/dev/topics/http/urls/
+
+Création des templates
+----------------------
+
+Nous allons, dans un premier temps, créer uniquement des templates vides. Puis, 
+nous relancerons nos tests pour vérifier si ils passent bien à présent. Il 
+restera alors juste à remplir les templates pour afficher les données.
+
+On se place à la racine du projet et on crée les fichiers : ::
+
+    $ mkdir templates/layout
+    $ mkdir templates/blog
+    $ touch templates/layout/base.html
+    $ touch templates/blog/entry_detail.html
+    $ touch templates/blog/category_detail.html
+    $ touch templates/blog/entry_archive.html
+    $ touch templates/blog/entry_archive_year.html
+    $ touch templates/blog/entry_archive_month.html
+    $ touch templates/blog/entry_archive_day.html
+    $ touch templates/404.html
+    $ touch templates/500.html
+
+Relançons les tests::
+
+    $ python manage.py test
+
+Nos tests passent ! Nous devons maintenant remplir ces templates.
+
+Fichier ``templates/layout/base.html`` : ::
+
+    <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>
+    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+        <head>
+            <title>{% block title %}{% endblock title %} - Django Blog</title>
+            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+            <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/screen.css" />
+        </head>
+        <body>
+            <div id="header">
+                <h1><a href="{% url blog %}">Django Blog</a></h1>
+            </div>
+            <div id="content">
+                {% block content %}{% endblock content %}
+            </div>
+        </body>
+    </html>
+
+Fichier ``templates/404.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% load i18n %}
+
+    {% block title %}{% trans "404 Not Found" %}{% endblock title %}
+
+    {% block content %}
+    
+        <p><strong>{% trans "404 Not Found" %}</strong></p>
+
+    {% endblock content %}
+
+Fichier ``templates/500.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% load i18n %}
+
+    {% block title %}{% trans "error 500" %}{% endblock title %}
+
+    {% block content %}
+    
+        <p><strong>{% trans "Error 500" %}</strong></p>
+
+    {% endblock content %}
+
+Fichier ``templates/blog/entry_archive.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% load i18n %}
+
+    {% block title %}{% trans "Latest entries" %}{% endblock title %}
+
+    {% block content %}
+    <h2>{% trans "Latest entries" %}</h2>
+    {% if latest %}
+        {% for entry in latest %}
+        <div class="entry">
+            <h3><a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a></h3>
+            <p class="entry-meta">
+                {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+                <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </p>
+            <div class="entry-body">
+                {{ entry.body|safe }}
+            </div>
+        </div>    
+        {% endfor %}
+    {% else %}
+        <p><strong>{% trans "No entry yet" %}.</strong></p>
+    {% endif %}
+    {% endblock content %}
+
+Fichier ``templates/blog/entry_archive_year.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ year }}{% endblock title %}
+
+    {% block content %}
+    <h2>{{ year }}</h2>
+    <ul>
+        {% for entry in object_list %}
+        <li>
+            <a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a> | 
+            <small>
+            {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </small>
+        </li>
+        {% endfor %}
+    </ul>
+    {% endblock content %}
+
+Fichier ``templates/blog/entry_archive_month.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ month|date:"Y/m" }}{% endblock title %}
+
+    {% block content %}
+    <h2>{{ month|date:"Y/m" }}</h2>
+    <ul>
+        {% for entry in object_list %}
+        <li>
+            <a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a> | 
+            <small>
+            {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </small>
+        </li>
+        {% endfor %}
+    </ul>
+    {% endblock content %}
+
+Fichier ``templates/blog/entry_archive_day.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ day|date:"Y/m/d" }}{% endblock title %}
+
+    {% block content %}
+    <h2>{{ day|date:"Y/m/d" }}</h2>
+    <ul>
+        {% for entry in object_list %}
+        <li>
+            <a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a> | 
+            <small>
+            {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </small>
+        </li>
+        {% endfor %}
+    </ul>
+    {% endblock content %}
+
+Fichier ``templates/blog/entry_detail.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ object.title }}{% endblock title %}
+
+    {% block content %}
+    <div class="entry">
+        <h2>{{ object.title }}</h2>
+        <p class="entry-meta">
+            {{ object.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ object.category.get_absolute_url }}">{{ object.category.name }}</a>
+        </p>
+        <div class="entry-body">
+            {{ object.body|safe }}      
+        </div>
+    </div>
+    {% endblock content %}
+
+Fichier ``templates/blog/category_detail.html`` : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ object.name }}{% endblock title %}
+
+    {% block content %}
+    <h2>{{ object.name }}</h2>
+    <ul>
+        {% for entry in object.entry_set.all %}
+        <li>
+            <a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a> | 
+            <small>
+            {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </small>
+        </li>
+        {% endfor %}
+    </ul>
+    {% endblock content %}
+
+Dans ce dernier template, ``object.entry_set.all`` récupére tous les billets 
+liés à cette catégorie. Et oui, tous. Y compris les billets hors ligne. Le plus 
+simple est donc de créer une propriété dans le modèle ``Category`` pour ne 
+récupérer que les billets en ligne : ::
+
+    class Category(models.Model):
+        """
+        A blog category.
+    
+        """
+        # Fields
+        name = models.CharField(_('name'), max_length=255)
+        slug = models.SlugField(_('slug'), max_length=255, unique=True)
+        creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+        modification_date = models.DateTimeField(_('modification date'), auto_now=True)
+    
+        # Managers
+        objects = models.Manager()
+        online_objects = CategoryOnlineManager()
+    
+        class Meta:
+            verbose_name = _('category')
+            verbose_name_plural = _('categories')
+        
+        def __unicode__(self):
+            return u'%s' % self.name
+
+        @models.permalink
+        def get_absolute_url(self):
+            return ('blog_category', (), {
+                'slug': self.slug,
+            })
+
+        def _get_online_entries(self):
+            """
+            Returns entries in this category with status of "online".
+            Access this through the property ``online_entry_set``.
+        
+            """
+            from website.apps.blog.models import Entry
+            return self.entry_set.filter(status=Entry.STATUS_ONLINE)
+        online_entry_set = property(_get_online_entries)
+
+Voici la nouvelle version du template : ::
+
+    {% extends "layout/base.html" %}
+
+    {% block title %}{{ object.name }}{% endblock title %}
+
+    {% block content %}
+    <h2>{{ object.name }}</h2>
+    <ul>
+        {% for entry in object.online_entry_set.all %}
+        <li>
+            <a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a> | 
+            <small>
+            {{ entry.publication_date|date:"Y/m/d @ H:i:s" }} - 
+            <a href="{{ entry.category.get_absolute_url }}">{{ entry.category.name }}</a>
+            </small>
+        </li>
+        {% endfor %}
+    </ul>
+    {% endblock content %}
+
+Nos templates sont en place. C'est très rustre mais c'est un exemple 
+d'application. 
+
+Qu'avons-nous oublié ? Mais oui, bien sûr, les fils RSS ! Un blog sans fils 
+RSS n'est pas un blog. C'est indispensable. Heureusement, Django nous permet 
+d'ajouter cette fonctionnalité en moins de deux minutes. Voyons comment procéder.
+
+Ajout des fils RSS
+------------------
+
+Django embarque une application pour la génération de fils RSS. N'hésitez pas 
+à consulter la section `The syndication feed framework`_ de la documentation 
+pour en savoir plus. Difficile de faire plus simple et efficace.
+
+Commençons par écrire nos tests. Ajoutons les méthodes ``test_rss_entries`` et 
+``test_rss_category`` à notre module ``test.py``, placé dans le répertoire de 
+l'application : ::
+
+    def test_rss_entries(self):
+        """
+        Tests entries RSS feed.
+    
+        """
+        blog_url = reverse('blog')
+        url = u'%sfeed/rss/entries/' % blog_url
+        response = self.client.get(url)
+        self.failUnlessEqual(response.status_code, 200)
+    
+    def test_rss_category(self):
+        """
+        Tests categories RSS feed.
+    
+        """
+        from website.apps.blog.models import Category
+        categories = Category.online_objects.all()
+        blog_url = reverse('blog')
+        for category in categories:
+            url = u'%sfeed/rss/category/%s/' % (blog_url, category.slug)
+            response = self.client.get(url)
+            self.failUnlessEqual(response.status_code, 200)
+
+Dans notre application ``blog``, créons un module ``feeds.py``. Dans ce module, 
+plaçons nos classes de syndication : ::
+
+    # -*- coding: utf-8
+    """
+    Feeds of ``blog`` application.
+
+    """
+    # Django
+    from django.utils.feedgenerator import Rss201rev2Feed
+    from django.utils.translation import ugettext_lazy as _
+    from django.core.exceptions import ObjectDoesNotExist
+    from django.core.urlresolvers import reverse
+    # Django contrib
+    from django.contrib.syndication import feeds
+    from django.contrib.sites.models import Site
+    # Application
+    from website.apps.blog.models import Entry
+    from website.apps.blog.models import Category
+
+    class RssEntries(feeds.Feed):
+        """
+        RSS entries.
+    
+        """
+        feed_type = Rss201rev2Feed
+        title_template = "blog/feeds/entry_title.html"
+        description_template = "blog/feeds/entry_description.html"
+    
+        def title(self):
+            """ 
+            Channel title. 
+        
+            """
+            site = Site.objects.get_current()
+            return _('%(site_name)s: RSS entries') % {
+                'site_name': site.name,
+            }
+    
+        def description(self):
+            """
+            Channel description.
+        
+            """
+            site = Site.objects.get_current()
+            return _('RSS feed of recent entries posted on %(site_name)s.') % {
+                'site_name': site.name,
+            }
+        
+        def link(self):
+            """
+            Channel link.
+        
+            """
+            return reverse('blog')
+    
+        def items(self):
+            """
+            Channel items.
+        
+            """
+            return Entry.online_objects.order_by('-publication_date')[:10]
+    
+        def item_pubdate(self, item):
+            """
+            Channel item publication date.
+        
+            """
+            return item.publication_date
+        
+    class RssCategory(RssEntries):
+        """
+        RSS category.
+    
+        """
+        def title(self, obj):
+            """
+            Channel title.
+        
+            """
+            site = Site.objects.get_current()
+            return _('%(site_name)s: RSS %(category)s category') % {
+                'site_name': site.name,
+                'category': obj.name,
+            }
+    
+        def description(self, obj):
+            """
+            Channel description.
+        
+            """
+            site = Site.objects.get_current()
+            return _('RSS feed of recent entries posted in the category %(category)s on %(site_name)s.') % {
+                'category': obj.name,
+                'site_name': site.name,
+            }
+        
+        def link(self, obj):
+            """
+            Channel link.
+        
+            """
+            return reverse('blog_category', args=[obj.slug])
+
+        def get_object(self, bits):
+            """
+            Object: the Category.
+        
+            """
+            if len(bits) != 1:
+                raise ObjectDoesNotExist
+            return Category.online_objects.get(slug=bits[0])
+    
+        def items(self, obj):
+            """
+            Channel items.
+        
+            """
+            return obj.online_entry_set
+    
+        def item_pubdate(self, item):
+            """
+            Channel item publication date.
+        
+            """
+            return item.publication_date
+
+Dans le module ``urls.py`` de l'application ``blog``, ajoutons le support des
+fils RSS : ::
+
+    # -*- coding: utf-8 -*-
+    """
+    URLs of ``blog`` application.
+
+    """
+    # Django
+    from django.conf.urls.defaults import *
+    # Application
+    from website.apps.blog.models import Entry
+    from website.apps.blog.models import Category
+    # Application feeds
+    from website.apps.blog.feeds import RssEntries
+    from website.apps.blog.feeds import RssCategory
+
+    rss_feeds = {
+        'entries': RssEntries,
+        'category': RssCategory,
+    }
+
+    urlpatterns = patterns('',
+        url(r'^feed/rss/(?P<url>.*)/$', 
+            'django.contrib.syndication.views.feed', 
+            {'feed_dict': rss_feeds},
+            name='blog_rss_feed',
+        ),
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)/$',
+            'django.views.generic.date_based.object_detail',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',
+                slug_field='slug',
+            ),
+            name='blog_entry',
+        ),
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/$',
+            'django.views.generic.date_based.archive_day',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',     
+            ),
+            name='blog_day',
+        ),
+        url(r'^(?P<year>\d{4})/(?P<month>\d{2})/$',
+            'django.views.generic.date_based.archive_month',
+            dict(
+                queryset=Entry.online_objects.all(),
+                month_format='%m',
+                date_field='publication_date',
+            ),
+            name='blog_month',
+        ),
+        url(r'^(?P<year>\d{4})/$',
+            'django.views.generic.date_based.archive_year',
+            dict(
+                queryset=Entry.online_objects.all(),
+                make_object_list=True,
+                date_field='publication_date',
+            ),
+            name='blog_year',
+        ),
+        url(r'^category/(?P<slug>[\w-]+)/$',
+            'django.views.generic.list_detail.object_detail',
+            dict(
+                queryset=Category.online_objects.all(),
+                slug_field='slug'
+            ),
+            name='blog_category',
+        ),
+        url(r'^$',
+            'django.views.generic.date_based.archive_index', 
+            dict(
+                queryset=Entry.online_objects.all(),
+                date_field='publication_date',
+            ),
+            name='blog',
+        ),
+    )
+
+Il ne reste plus qu'à créer les templates : ::
+
+    $ mkdir templates/blog/feeds
+    $ touch templates/blog/feeds/entry_title.html
+    $ touch templates/blog/feeds/entry_description.html
+
+Le template ``templates/blog/feeds/entry_title.html`` : ::
+
+    {{ obj.title }}
+
+Le template ``templates/blog/feeds/entry_description.html`` : ::
+
+    {{ obj.body|safe }}
+
+Lançons les tests pour vérifier si tout est OK : ::
+
+    $ python manage.py test
+
+Vous ne devriez pas rencontrer d'erreur. 
+
+Lancez le serveur : ::
+
+    $ python manage.py runserver
+
+Dans votre navigateur, allez à ces adresses : ::
+
+- http://127.0.0.1:8000/feed/rss/entries/
+- http://127.0.0.1:8000/feed/rss/category/test/
+
+Vous devriez voir les fils. Merci Django !
+
+.. _The syndication feed framework: http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#ref-contrib-syndication
+
+Conclusion
+----------
+
+Cet exemple d'application Django est un blog simpliste, basique, lambda. Moult 
+fonctionnalités ont été volontairement omises (formatage du contenu des billets 
+avec une syntaxe wiki, gestion multi-catégories, support des tags, support du 
+format Atom pour les fils de syndication, amélioration de l'interface 
+d'administration, amélioration des templates à l'aide d'includes, support des 
+commentaires, ajout d'une sidebar... ) pour ne pas transformer ce tutorial en 
+ouvrage technique. La documentation de Django est complète et claire. N'hésitez 
+pas à la consulter au moindre problème.
+
+Télécharger_ le source de l'application.
+
+.. _Télécharger: http://media.gillesfabio.com/public/files/website.tar.bz2

site/djangofr/contributors/models.py

     url = models.URLField('Site personnel', null=True, blank=True, verify_exists=False)
     bio = models.TextField('Informations', help_text='En HTML, facultatives.', null=True, blank=True)
 
-    class Admin:
-        list_display = ('name', 'email', 'url')
-        search_fields = ('name', 'bio')
-
     class Meta:
         verbose_name = 'Contributeur'