Cet article est une mise à jour d’une précédente publication : créer un blog avec Django, basée sur la version 1.1 du framework. La version 1.2 requière de légères modifications.
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.
Mise en place du projet
Dans ce tutoriel, 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.
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 :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'blog',
'USER': 'blog',
'PASSWORD': 'mot-de-passe',
'HOST': '',
'PORT': '',
}
}
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',
'django.contrib.messages',
'django.contrib.admin',
'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/(.*)', include(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/(.*)', include(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=['2010']))
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=['2010', '07']))
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=['2010', '07', '21']))
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=['2010', '07', '21', '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=['2010', '07', '21', '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.
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.
"""
from datetime import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Category(models.Model):
"""
A blog category.
"""
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_OFFLINE = 0
STATUS_ONLINE = 1
STATUS_DEFAULT = STATUS_OFFLINE
STATUS_CHOICES = (
(STATUS_OFFLINE, _('Offline')),
(STATUS_ONLINE, _('Online')),
)
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.
"""
from datetime import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from website.apps.blog.managers import CategoryOnlineManager
from website.apps.blog.managers import EntryOnlineManager
class Category(models.Model):
"""
A blog category.
"""
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)
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_OFFLINE = 0
STATUS_ONLINE = 1
STATUS_DEFAULT = STATUS_OFFLINE
STATUS_CHOICES = (
(STATUS_OFFLINE, _('Offline')),
(STATUS_ONLINE, _('Online')),
)
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'))
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.
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.
"""
from django.contrib import admin
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.
"""
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
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.
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 : 2010-07-21 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 : 2010-07-21 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.
"""
from django.conf.urls.defaults import *
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/', include(admin.site.urls)),
)
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.
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.
"""
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)
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.
"""
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
from django.contrib.syndication import feeds
from django.contrib.sites.models import Site
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 :
"""
URLs of blog application.
"""
from django.conf.urls.defaults import *
from website.apps.blog.models import Entry
from website.apps.blog.models import Category
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',
),
...
)
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 !
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 barre de navigation… ) pour ne pas transformer ce tutoriel 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.