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.*)$', '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.*)$', '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\d{4})/(?P\d{2})/(?P\d{2})/(?P[\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\d{4})/(?P\d{2})/(?P\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\d{4})/(?P\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\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[\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.*)$', '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`` : :: {% block title %}{% endblock title %} - Django Blog
{% block content %}{% endblock content %}
Fichier ``templates/404.html`` : :: {% extends "layout/base.html" %} {% load i18n %} {% block title %}{% trans "404 Not Found" %}{% endblock title %} {% block content %}

{% trans "404 Not Found" %}

{% endblock content %} Fichier ``templates/500.html`` : :: {% extends "layout/base.html" %} {% load i18n %} {% block title %}{% trans "error 500" %}{% endblock title %} {% block content %}

{% trans "Error 500" %}

{% endblock content %} Fichier ``templates/blog/entry_archive.html`` : :: {% extends "layout/base.html" %} {% load i18n %} {% block title %}{% trans "Latest entries" %}{% endblock title %} {% block content %}

{% trans "Latest entries" %}

{% if latest %} {% for entry in latest %}

{{ entry.title }}

{{ entry.body|safe }}
{% endfor %} {% else %}

{% trans "No entry yet" %}.

{% endif %} {% endblock content %} Fichier ``templates/blog/entry_archive_year.html`` : :: {% extends "layout/base.html" %} {% block title %}{{ year }}{% endblock title %} {% block content %}

{{ year }}

{% endblock content %} Fichier ``templates/blog/entry_archive_month.html`` : :: {% extends "layout/base.html" %} {% block title %}{{ month|date:"Y/m" }}{% endblock title %} {% block content %}

{{ month|date:"Y/m" }}

{% endblock content %} Fichier ``templates/blog/entry_archive_day.html`` : :: {% extends "layout/base.html" %} {% block title %}{{ day|date:"Y/m/d" }}{% endblock title %} {% block content %}

{{ day|date:"Y/m/d" }}

{% endblock content %} Fichier ``templates/blog/entry_detail.html`` : :: {% extends "layout/base.html" %} {% block title %}{{ object.title }}{% endblock title %} {% block content %}

{{ object.title }}

{{ object.body|safe }}
{% endblock content %} Fichier ``templates/blog/category_detail.html`` : :: {% extends "layout/base.html" %} {% block title %}{{ object.name }}{% endblock title %} {% block content %}

{{ object.name }}

{% 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 %}

{{ object.name }}

{% 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.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': rss_feeds}, name='blog_rss_feed', ), url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\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\d{4})/(?P\d{2})/(?P\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\d{4})/(?P\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\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[\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