Une architecture possible pour organiser les tests d'une application consiste à
créer, dans un dossier tests
, un fichier de tests
(test_views.py
, test_models.py
, ...) par fichier de
l'application (views.py
, models.py
, ...).
├── library
│ ├── __init__.py
│ ├── forms.py
│ ├── models.py
│ ├── views.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_forms.py
│ │ ├── test_models.py
│ │ └── test_views.py
Les tests automatisés sont les premières briques indispensables pour garantir une application fiable et éviter les régressions au fil du temps.
Voici un modèle très basique :
# models.py
class Author(models.Model):
firstname = models.CharField(max_length=100, null=True, blank=True)
lastname = models.CharField(max_length=100)
def __str__(self):
return u'%s %s' % (self.firstname, self.lastname)
L'idée n'est pas de tester Django (création d'instance, vérification que les
différents fonctionnent, ...) mais bien de tester notre code personnel. Ici,
seule la fonction __str__
est donc à tester.
Il faut prendre soin de tester les différents cas possibles d'exécution ( en
l'occurrence, la présence d'un firstname
ou non).
# tests/test_models.py
from django.test import TestCase
from library.models import Author
class AuthorAsStringTest(TestCase):
def test_with_first_name(self):
author = Author.objects.create(firstname='René',
lastname='Descartes')
self.assertEqual(str(author), 'René Descartes')
def test_without_first_name(self):
author = Author.objects.create(lastname='Platon')
self.assertEqual(str(author), 'Platon')
$ ./manage.py test library
Creating test database for alias 'default'...
.F
======================================================================
FAIL: test_without_first_name (library.tests.test_models.AuthorAsStringTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/al/makina-slides/django/example_projects/libraryproject/library/tests/test_models.py", line 16, in test_without_first_name
self.assertEqual(str(author), 'Platon')
AssertionError: 'None Platon' != 'Platon'
- None Platon
+ Platon
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)
Destroying test database for alias 'default'...
class Author(models.Model):
firstname = models.CharField(max_length=100, null=True, blank=True)
lastname = models.CharField(max_length=100)
def __str__(self):
if self.firstname:
return u'%s %s' % (self.firstname, self.lastname)
else:
return self.lastname
Exécution des tests :
$ ./manage.py test library.tests.test_models
Creating test database for alias 'default'...
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Destroying test database for alias 'default'...
from datetime import date
from django.test import TestCase
from library.models import Book
class BookViewTest(TestCase):
def test_recent_books_view(self):
recent_date = date.today()
recent_book = Book.objects.create(title='Titre du livre récent',
published=recent_date)
old_date = recent_date.replace(year=recent_date.year - 1)
old_book = Book.objects.create(title='Titre du vieux livre',
published=old_date)
response = self.client.get("/library/recent/")
self.assertNotContains(response, old_book.title)
self.assertContains(response, recent_book.title)
class Book(models.Model):
title = models.CharField(max_length=200)
published = models.DateField(null=True, blank=True)
from django.shortcuts import render
from datetime import date
from .models import Book
def recent_books(request):
today = date.today()
threshold = today.replace(year=today.year - 1)
results = Book.objects.filter(published__gt=threshold)
return render(request, 'library/book_list.html', {
'results': results
})
On a vérifié :
# forms.py
from django import forms
class PeriodForm(forms.Form):
begin = forms.DateField()
end = forms.DateField()
def __init__(self, *args, **kwargs):
super(PeriodForm, self).__init__(*args, **kwargs)
begin = self.initial.get('begin', None)
if begin:
self.initial['end'] = begin.replace(month=begin.month + 1)
Comme pour le modèle, l'idée n'est pas de tester ce qui est du ressort de Django.
Ici, il est simplement nécessaire de s'assurer que la fonction __init__
fonctionne correctement dans les différents cas possibles (présence ou non d'une valeur
initiale pour le champ begin
).
# tests/test_forms.py
from datetime import date
from django.test import TestCase
from library.forms import PeriodForm
class PeriodFormTest(TestCase):
def test_init_without_begin(self):
f = PeriodForm()
self.assertIsNone(f.initial.get('end'))
def test_init_with_begin(self):
initial = {'begin': date(2014, 1, 1)}
f = PeriodForm(initial=initial)
self.assertEqual(f.initial.get('begin'), date(2014, 1, 1))
self.assertEqual(f.initial.get('end'), date(2014, 2, 1))
Révèle quelles parties du code sont couvertes par les tests
$ coverage run --branch --source=library ./manage.py test
$ coverage report
Name Stmts Miss Branch BrPart Cover
----------------------------------------------------------------------
library/__init__.py 0 0 0 0 100%
library/admin.py 1 0 0 0 100%
library/apps.py 3 0 0 0 100%
library/forms.py 9 0 2 0 100%
library/migrations/0001_initial.py 6 0 0 0 100%
library/migrations/__init__.py 0 0 0 0 100%
library/models.py 11 0 2 0 100%
library/tests/__init__.py 0 0 0 0 100%
library/tests/test_forms.py 12 0 0 0 100%
library/tests/test_models.py 9 0 0 0 100%
library/tests/test_views.py 12 0 0 0 100%
library/urls.py 3 0 0 0 100%
library/views.py 8 0 0 0 100%
----------------------------------------------------------------------
TOTAL 74 0 4 0 100%
La gestion des utilisateurs Django est principalement gérée par le module
django.contrib.auth
. Ce module introduit plusieurs concepts :
User
: classe représentant un utilisateur Django ;Permission
: classe d'assigner à un utilisateur le droit de faire une certaine action ou non (booléen);Group
: classe permettant d'associer plusieurs permissions à un sous-ensemble d'utilisateurs ;Pour disposer de cette fonctionnalité, le module django.contrib.auth
doit
être présent dans les INSTALLED_APPS
du projet (cf settings.py
).
La classe User
est le coeur du système d'authentification Django. Une instance
de User
représente un utilisateur, une personne qui interagit avec le site. Elle
permet plusieurs choses comme :
La classe User
fournit quelques propriétés de base comme first_name
, last_name
,
username
, password
, email
. D'autres propriétés, plus fonctionnelles,
sont à connaitre :
is_active
: booléen précisant si le compte est actif ;is_staff
: booléen précisant si l'utilisateur peut accéder à l'interface d'administration ;is_superuser
: booleén spécifiant si l'utilisateur est un super-utilisateur.Django fournit un système de permissions assez simple. Il consiste à assigner des permissions particulières à des utilisateurs ou/et à des groupes.
L'interface d'administration peut notamment utiliser les permissions génériques
add
, change
et delete
sur chaque modèle existant dans le projet Django.
Pour le modèle my_model
de l'application my_app
, les permissions suivantes
pourront être créées par un ./manage.py syncdb
:
Il est aussi possible de créer ses propres permissions.
Quelques fonctions de la classe User
permettent de travailler avec ces permissions,
notamment :
user.get_all_permissions()
user.has_perm(perm)
L'objectif des groupes et de catégoriser des sous-ensembles d'utilisateurs afin de leur assigner une liste commune de permissions. Exemple :
Django apporte nativement quelques vues facilitant l'authentification et la gestion du mot de passe des utilisateurs, principalement :
login
logout
logout_then_login
password_change
password_reset
Quelques settings permettent aussi de simplifier cette gestion :
LOGIN_URL
: URL vers la vue de connexionLOGIN_REDIRECT_URL
: URL de redirection après l'authentification de l'utilisateurLOGOUT_URL
: URL de la vue de déconnexionSans utiliser directement les vues prêtes à l'emploi, il est aussi possible de baser
des vues personnalisées sur des formulaires présents dans la bibliothèque
django.contrib.auth.forms
.
Ces formulaires réalisent de base certaines vérifications très utiles (unicité du nom d'utilisateur, vérification de la ressaisie du mot de passe, ...).
AuthenticationForm
: formulaire d'authentificationUserChangeForm
: formulaire d'édition du compte utilisateurPasswordChangeForm
: formulaire de changement de mot de passePasswordResetForm
: formulaire de réinitialisation de mot de passeSetPasswordForm
: formulaire de création de mot de passeLa bibliothèque django.contrib.auth.backends
apporte un système de backend
d'authentification relativement simple et très souple.
Deux backends par défaut sont disponibles:
ModelBackend
: backend d'authentification par défaut utilisant le nom d'utilisateur / mot de passe de l'utilisateurRemoteUserBackend
: permet de gérer une authentification depuis une source externe via les entête HTTPIl est assez facile d'écrire son propre backend pour personnaliser l'authentification des utilisateurs en écrivant une simple classe qui implémente certaines fonctions comme :
authenticate
get_user
Queryset
Un Queryset
représente une collection d'objets provenant de la base de données. Cette collection peut être filtrée, limitée, ordonnée, ... grâce à des méthodes qui correspondent à des clauses SQL.
Il est donc possible de construire des requêtes en base de données via ce QuerySet.
>>> Book.objects.filter(title__icontains='django') \
.exclude(relase__lte=date('2014-01-01')) \
.order_by('price')
QuerySet
values()
Cette méthode retourne un ValuesQuerySet
qui liste des dictionnaires plutôt que des instances du modèle. Chaque dictionnaire représente une instance ; ses clés correspondent aux attributs de l'instance. Il est possible de spécifier les clés que l'on souhaite récupérer.
>>> Book.objects.filter(name__icontains='django') \
.values('title' , 'release')
[{'title': 'Two scoops of django', 'release': date(2013, 08, 31)}, ]
Un ValuesQuerySet
peut être très intéressant quand le nombre d'attributs dont on a besoin est faible, car on évite de charger les instances sous forme de modèles python.
Attention, dans le cas d'un attribut de type ForeignKey
, la clé et la valeur retournées seront le nom de le colonne et la valeur trouvée en base de données (ex: 'author_id': 12
)
QuerySet
values_list()
Cette méthode est semblable à la précédente mais elle retourne une liste de tuples plutôt qu'une liste de dictionnaires.
>>> Book.objects.filter(name__icontains='django') \
.values_list('title' , 'release')
[('Two scoops of django', date(2013, 08, 31)),
('Django avancé', date(2013, 05, 15))]
Si un seul attribut est précisé, il est possible d'ajouter le paramètre flat=True
pour obtenir une liste non imbriquée.
>>> Book.objects.filter(name__icontains='django') \
.values_list('title', flat=True)
['Two scoops of django', 'Django avancé']
QuerySet
La méthode basique pour créer une instance est d'instancier le modèle puis de faire appel à la méthode save()
de cette instance, mais une autre solution très pratique existe.
create() et get_or_create()
La méthode create()
permet de réaliser l'opération ci-dessus en une seule instruction :
book = Book.objects.create(title="New django book", price="42€")
La méthode get_or_create()
permet de tenter de récupérer un objet (via get()
), et de le créer si il n'existe pas. Elle retourne un tuple comprenant un booléen qui précise si l'instance vient d'être créée, et l'instance elle-même :
book, created = Book.objects.get_or_create(
title="New django book", date(2013, 05, 15),
defaults={'price': '42€'})
Les valeurs passées directement en paramètres sont utilisées lors de l'appel de le méthode get()
, les valeurs passées dans defaults
sont utilisées lors de la création éventuelle de l'instance pour initialiser la valeur des propriétés correspondantes.
Manager
Un Manager
est l'interface à travers laquelle les opérations de requêtage en base de données sont mises à disposition d'un modèle Django. Chaque modèle possède un Manager
par défaut accessible via la propriété objects
.
from django.db import models
class Book(models.Model):
#...
objects = models.Manager()
Ce Manager
par défaut fournit nativement quelques méthodes très souvent utilisées, comme :
>>> Book.objects.get(pk=12)
>>> Book.objects.all()
>>> Book.objects.filter(title__icontains='django')
>>> Book.objects.exclude(date__lt=date(2013, 01, 01))
Il peut cependant être utile d'écrire son propre Manager
pour principalement
deux raisons :
QuerySet
initial retourné par le Manager
.Un Manager
personnalisé est une classe héritant de Manager
que l'on instancie dans un attribut du modèle.
from django.db import models
class CustomBookManager(models.Manager):
# ...
class Book(models.Model):
#...
custom_books = CustomBookManager()
Écrire un Manager
personnalisé est la bonne solution pour ajouter des méthodes de niveau table (qui renvoit des informations sur un ensemble d'instances) contrairement aux méthodes du modèle dites de niveau ligne (qui renvoit des informations sur une instance).
from django.db import models
class AuthorManager(models.Manager):
def with_nb_books(self):
self.get_query_set() \
.annotate(nb_books=Count('books')) \
.order_by('nb_books')
class Author(models.Model):
#...
objects = AuthorManager()
>>> authors_with_nb_books = Author.objects.with_nb_books()
>>> authors_with_nb_books[0]
<Author : John Doe>
>>> authors_with_nb_books[0].nb_books
42
QuerySet
initialLa méthode Manager.get_query_set()
renvoit un QuerySet
par défaut qui correspond à Model.objects.all()
.
Il peut être intéressant de créer un Manager
pour surcharger cette méthode et retourner un QuerySet
personnalisé.
from django.db import models
class EnglishBookManager(models.Manager):
def get_query_set(self):
return Manager.get_queryset(self).filter(lang='EN')
class FrenchBookManager(models.Manager):
def get_query_set(self):
return Manager.get_queryset(self).filter(lang='FR')
class Book(models.Model):
#...
objects = models.Manager()
english_books = EnglishBookManager()
french_books = FrenchBookManager()
QuerySet
initialQuand on surcharge le QuerySet
initial, il est souvent préférable de ne pas remplacer l'attribut objects
par le Manager
personnalisé.
En effet, objects
est le Manager
utilisé par défaut (dans l'administration par exemple). Il est donc très risqué d'altérer son comportement.
En revanche, remplacer objects
par un Manager
personnalisé qui ne fait qu'ajouter des méthodes personnalisées ne pose pas de problème, puisque le comportement naturel n'est pas altéré.
Manager
personnalisé permettant de lister les tâches urgentes et non réaliséesCe type d'héritage est souvent utilisé pour mettre en commun un certain nombre d'informations et/ou de comportements entre plusieurs modèles. Cette classe abstraite ne sera pas utilisé de manière autonome.
# models.py
class CommonInfo(models.Model):
creation_date = models.DateField()
modification_date = models.DateField()
class Meta:
abstract = True
class Book(CommonInfo):
title = models.CharField(max_length=100)
# ...
class Author(CommonInfo):
name = models.CharField(max_length=100)
# ...
Pour spécialiser un modèle déjà existant (éventuellement d'une application externe) ou/et si on souhaite que les modèles aient des tables séparées, il faut utiliser l'héritabe multi-tables.
OneToOneField
# models.py
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author)
class Comic(Book):
illustrator = models.CharField(max_length=100)
# ...
class Biography(Book):
personage = models.CharField(max_length=100)
# ...
Grâce aux modèles proxy, il est possible de modifier le comportement d'un objet (Manager
personnalisé, ajout d'une méthode, ...) sans toucher aux données (champs) et donc sans créer une nouvelle table pour ce modèle dérivé.
# models.py
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author)
class OrderedByAuthorBook(Book):
class Meta:
proxy = True
ordering = ['author']
On accède à une relation dans une boucle ce qui entraine :
Exemple :
{% for task in object_list %}
<li>
<a href="{% url 'task_detail' task.pk %}">{{ task }}</a>
Liste: {{task.todo_list.label }}
</li>
{% endfor %}
class TaskList(ListView):
model = Task
def get_queryset(self):
queryset = super(TaskList, self).get_queryset()
return queryset.select_related("todo_list")
L'ORM fait une seule requête avec une jointure :
{% for list in object_list %}
<li>
<a href="{% url 'todolist_detail' list.pk %}">{{ list }}</a>
Users: {% for user in list.users.all %}
{{ user.username }}
{% endfor %}
</li>
{% endfor %}
class TodoListList(ListView):
model = TodoList
def get_queryset(self):
queryset = super(TodoListList, self).get_queryset()
return queryset.prefetch_related("users")
L'ORM ne fait qu'une seule requête supplémentaire avec une clause IN
:
Permet d'appeler du code quand certains événements se produisent dans l'application :
Exemple :
from django.db.models.signals import post_save
from django.dispatch import receiver
from todo.models import Task
@receiver(post_save, sender=Task)
def db_update_callback(sender, instance, created, **kwargs):
print('Task "{0}" saved!'.format(instance.name))
Une vue basée sur une classe Django permet de structurer le code et de le réutiliser en exploitant notamment l'héritage et les mixins.
Django fournit de multiples socles plus ou moins avancés pour construire ce type de vues.
Ces vues sont aussi généralement écrites dans le fichier views.py
de l'application.
# some_app/views.py
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "about.html"
Les vues basées sur des classes possèdent des avantages sur les vues classiques :
Django propose une biobliothèque riche permettant de travailler avec des vues basées sur des classes, dont la classe View
est le point central
as_view()
est appelée par l'URLDispatcher
;dispatch()
de l'instance créée ;get()
, post()
, ... en fonction de la méthode HTTP entrante (GET, POST, ...) ;HttpResponse
est relayée par dispatch()
.Il faut probablement utiliser une vue basée sur une classe ...
Il faut probablement utiliser une vue basée sur une fonction ...
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# traitements
return HttpResponse('result')
from django.http import HttpResponse
from django.views.generic.base import View
class MyView(View):
def get(self, request):
# traitements
return HttpResponse('result')
Dans django.views.generic.base
:
View
est la classe mère et fourni le workflow vu précédemment.TemplateView
est une classe permettant très simplement de faire le rendu d'une template.Dans django.views.generic.edit
:
FormView
facilite la gestion d'un formulaire en permettant une bonne organisation du code et en limitant l'indentation.Dans django.views.generic
:
ListView
permet de lister très simplement des instances d'un modèle.DetailView
permet d'afficher le détail d'une instance d'un modèle.Dans django.views.generic.edit
:
CreateView
et UpdateView
sont très utiles pour la création/modification d'instance, de l'affichage du formulaire jusqu'à l'enregistrement de l'instance.DeleteView
facilite l'implémentation de vues pour la suppression d'intance.Un excellent site permettant d'avoir un aperçu complet : http://ccbv.co.uk/
Les décorateurs sont des fonctions Python dont le rôle est de modifier le comportement par défaut d'autres fonctions ou classes.
Il est par exemple possible de protéger une vue avec un ou plusieurs décorateurs.
require_http_methods
permet de limiter l'accès à une vue sur certaines méthodes HTTP précises ;login_required
permet de limiter l'accès à une vue aux utilisateurs connectés ;permission_required
permet de limiter l'accès à une vue aux utilisateurs possédant la permission précisée.login_required
from django.contrib.auth.decorators import login_required
@login_required()
def my_view(request):
# Seul un utilisateur connecté peut accéder à cette vue
# ...
require_http_methods
from django.contrib.auth.decorators import login_required
@require_http_methods(["GET", "POST"])
def my_view(request):
# On ne peut pas accéder à cette vue qu'en GET ou POST
# ...
# views.py
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
class MyProtectedView(TemplateView):
template_name = 'my_protected_view.html'
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(MyProtectedView, self).dispatch(*args, **kwargs)
URLConf
# urls.py
from django.contrib.auth.decorators import login_required
from my_app.views import MyProtectedView
urlpatterns = patterns('',
(r'^secret/', login_required(MyProtectedView.as_view())),
)
Il est important de gérer les erreurs selon les concepts du protocole HTTP. Une ressource non trouvée sur un site doit donc retourner une erreur de type 404. On utilise pour cela une exception de type Http404
.
from django.http import Http404
def book_detail(request, book_id):
try:
book = Book.objects.get(pk=book_id)
except Book.DoesNotExist:
raise Http404
return render(request, 'library/book_detail.html', {
'book': book
})
Comme pour l'erreur 404, Il est important de gérer les erreurs en accord avec le protocole HTTP. Un problème de permission doit donc engendrer une erreur de type 403. On utilise pour cela une exception de type PermissionDenied
.
from django.core.exceptions import PermissionDenied
def book_detail(request, book_id):
if not library.is_registered(user):
raise PermissionDenied
book = Book.objects.get(pk=book_id)
return render(request, 'books/detail.html', {'book': book})
Plusieurs types d'erreurs peuvent être générées manuellement ou automatiquement par Django, principalement : 400, 403, 404 et 500.
Par défaut, chaque erreur correspond à une vue dont Django fait le rendu quand l'exception est levée :
django.views.defaults.bad_request
django.views.defaults.permission_denied
django.views.defaults.page_not_found
django.views.defaults.server_error
debug
Le réglage TEMPLATE_DEBUG
(dans settings.py
) permet d'activer ou non
l'affichage de la page de débogage pedant le développement. Cette page vient en
remplacement des vues d'erreurs listées ci-dessus. Il est donc important de
la désactiver en production.
Pour personnaliser simplement l'affichage, il suffit de nommer la template 403.html, 404.html, ... et Django fera le rendu de cette template automatiquement.
Si on souhaite que le traitement de l'erreur soit plus complexe, il est possible
de créer une vue dont il faudra préciser le nom dans l'URLConf
:
# views.py
def my_403_view(request):
send_mail_to_admin()
# ...
return render(request, '403.html')
# urls.py
urlpatterns = patterns('',
# ...
)
handler403 = 'my_app.views.my_403_view'
Altérer le traitement des requêtes de manière globale
class SimpleMiddleware(object):
def __init__(self, get_response):
# Initialisation
self.get_response = get_response
def __call__(self, request):
# Code exécuté pour chaque requête avant la vue
response = self.get_response(request)
# Code exécuté pour chaque requête après la vue
return response
Les middlewares activés par défaut
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Voir la documentation des middlewares standards
Une chaîne est dite sécurisée quand elle a été marquée comme n'ayant pas besoin
d'échappement lors d'un rendu HTML, c'est à dire que les caractères qui ne
doivent pas être interprétés par le moteur HTML ont déjà été transformés en leurs
entités appropriées.
Une chaîne sécurisée est représentée par un objet SafeString
.
Plusieurs filtres et tag permettent de gérer l'échappement des chaînes :
escape
: transforme les caractères HTML d'une chaîne en entités (ex: "<" en "<
")safe
: marque la chaîne comme n'ayant pas besoin d'échappement{% autoescape on|off %}
: précise si les chaînes doivent être ou non échappées systématiquement à l'intérieur de ce blocLes filtres personnalisés doivent être écrits dans une module templatetags
de l'application.
├── my_app
│ ├── __init__.py
│ ├── admin.py
│ ├── forms.py
│ ├── models.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── my_todo_app_filters.py
Le nom du fichier en lui-même n'a pas d'importance, mais il sera utilisé pour charger les filtres au niveau des templates.
Un filtre personnalisé est une simple fonctions python qui prend un ou deux arguments :
Un filtre sera sans argument supplémentaire sera appelé de la manière suivante :
{{ variable|my_simple_filter }}
et un filtre avec argument sera utilisé ainsi :
{{ variable|my_filter:"foo" }}
# filters.py
def lower(value):
"""Converts a string into all lowercase"""
return value.lower()
{# template #}
{{ variable|lower }}
# filters.py
def cut(value, arg):
"""Removes all values of arg from the given string"""
return value.replace(arg, '')
{# template #}
{{ variable|cut:'0' }}
La classe Library
permet d'ajouter les filtres personnalisés à la bibliothèque
de filtres Django pour pouvoir ensuite les charger et les utiliser dans les
templates. Deux solutions :
from django import template
register = template.Library()
def my_filter(value):
# code du filtre
register.filter('my_filter', my_filter)
from django import template
register = template.Library()
@register.filter()
def my_filter(value):
# code du filtre
L'échappement (ou non) de la chaîne retournée en sortie du filtre doit être contrôlé.
Si le filtre n'introduit pas de caractère HTML (comme "&" ou "<"), il peut être
marqué comme sécurisé au moment de l'enregistrement grâce à l'équipement is_safe
.
@register.filter(is_safe=True)
def my_filter(value):
# code du filtre
Django traîtera alors l'échappement de la chaîne en entrée en sachant que le filtre personnalisé n'aura pas d'impact.
Il est aussi possible de marqué la chaîne comme sécurisée manuellement en sortie du filtre (nécessaire si le filtre introduit du HTML).
from django.utils.safestring import mark_safe
@register.filter(is_safe=True)
def my_filter(value):
# code du filtre
return mark_safe(output)
Il existe une méthode très simple pour initialiser les champs d'un formulaire :
la méthode __init__()
peut prendre en argument un dictionnaire "initial
"
dont les clés doivent correspondre aux noms des champs du formulaire.
Exemple :
# forms.py
class AccountForm(forms.Form):
lastname = forms.CharField(max_length=100)
firstname = forms.CharField(max_length=100)
#views.py
def create_account(request):
initial = {
'lastname': request.user.last_name,
'firstname': request.user.first_name
}
form = AccountForm(initial=initial)
__init__()
Pour aller plus loin, il est possible de surcharger la méthode __init__()
pour réaliser des traitements particuliers (initialiser des valeurs complexes,
limiter les choix d'un champ select, cacher dynamiquement des champs, ...).
Exemple :
# forms.py
class PeriodForm(forms.Form):
begin = forms.DateField()
end = forms.DateField()
def __init__(self, *args, **kwargs):
super(PeriodForm, self).__init__(*args, **kwargs)
begin = self.initial.get('begin', None)
if begin:
self.initial['end'] = begin + delta(months=1)
Un formulaire Django dispose d'un mécanisme de validation assez poussé qui consiste à valider chaque champ un par un, puis à exécuter une méthode réalisant une validation plus globale.
Un échec de validation doit engendrer une exception de type ValidationError
.
Pour valider un champ de formulaire, il suffit de créer une méthode de formulaire
nommée par le nom du champ préfixé de clean_
.
Exemple :
# forms.py
class SearchBookForm(forms.Form):
search_text = forms.CharField(max_length=100)
def clean_search_text(self):
search_text = self.cleaned_data['search_text']
if 'django' not in search_text:
msg = 'You should search Django books :)!'
raise forms.ValidationError(msg)
return search_text
Implémenter la méthode clean
permet de faire une validation globale du formulaire,
utile notamment pour faire des vérifications sur plusieurs champs dépendants les uns
des autres.
Exemple :
# forms.py
class PeriodForm(forms.Form):
begin = forms.DateField()
end = forms.DateField()
def clean(self):
cleaned_data = super(PeriodForm, self).clean()
begin = cleaned_data.get('begin')
end = cleaned_data.get('end')
if begin and end and begin >= end:
msg = 'End date must be later than begin date!'
self._errors.setdefault('end', []).append(msg)
return cleaned_data
Les sites web ont très souvent besoin de servir des fichiers dits statiques, principalement des images, CSS et JS.
Django fournit un module django.contrib.staticfiles
qui facilite cette gestion.
Comme toujours, pour que l'application soit utilisable, il faut qu'elle soit présente
dans les INSTALLED_APPS
du projet.
STATIC_URL
permet ensuite de spécifier l'URL à partir de laquelle ces fichiers
statiques seront disponibles.
# settings.py
INSTALLED_APPS = (
...
'django.contrib.staticfiles',
...
)
STATIC_URL = '/static/'
Les fichiers statiques doivent être stockés dans un répertoire static
de
l'application. Les scripts par défaut configurés dans STATICFILES_FINDERS
(cf settings.py
) pourront alors retrouver les fichiers statiques de chaque application.
Il est aussi possible de stocker des fichiers statiques dans d'autres dossiers,
il faut alors ajouter ceux-ci à la liste STATICFILES_DIRS
.
La commande collectstatic
permet d'aggréger ces fichiers dans un répertoire unique défini par STATIC_ROOT
:
$ ./manage.py collecstatic
Le tag {% static %}
permet de créer une URL dynamiquement vers un fichier
statique.
Exemple pour une image :
{# my_app/templates/my_app/my_template.html #}
{% load static %}
...
<img src="{% static "my_app/img/myexample.jpg" %}" alt="My image"/>
Exemple pour un CSS :
{# base.html #}
{% load static %}
<html>
<head>
<link href="{% static "my_app/css/styles.css" %}" />
En cours de développement (DEBUG = True
), le serveur standalone de Django
se charge de servir les fichiers statiques lui-même via une vue dédiée :
django.contrib.staticfiles.views.serve
.
Cette méthode est peu efficace et peu sécurisée, et ne doit pas être utilisée en production
Le paramètre STATIC_ROOT
permet de spécifier le chemin vers
le répertoire des fichiers statiques sur le système de fichiers.
Grâce à ce réglage, la commande collectstatic
copie tous les fichiers statiques
vers le chemin précisé.
Il faut ensuite paramétrer le serveur web pour qu'il serve lui-même ces fichiers.
Les fichiers dits media sont les fichiers uploadés par les utilisateurs.
Comme pour les statiques, deux réglages principaux sont à connaître :
MEDIA_URL
: URL à laquelle il faut mettre à disposition les fichiers mediaMEDIA_ROOT
: chemin vers lequel les fichiers media doivent être stockésDe manière interne, la gestion des fichiers est faite via le module django.core.files
qui apporte notamment une classe File
et des sous-classes comme ImageFile
disposant de propriétés (name
, size
, ...) et de méthodes très utiles
(open()
, read()
, save()
).
Deux champs FileField
et ImageField
sont fournis pour pouvoir associer
facilement un fichier ou une image à une instance de modèle.
# models.py
class Book(models.Model):
# ...
summary = models.FileField(upload_to='summaries')
class Author(models.Model):
# ...
photo = models.ImageField(upload_to='avatars')
Les instance de `Book
pourront donc chacun avoir un fichier attaché :
>>> book = Book.objects.get(pk=12)
>>> book.summary
<FieldFile: summaries/summary_12.pdf>
>>> book.summary.url
u'http://my_site.com/media/summaries/summary_12.pdf'
Il existe des champs de formulaires correspondant aux champs de modèles vus précédemment. Il est donc très facile d'obtenir un champ d'upload dans un formulaire.
# forms.py
class MyForm(forms.Form):
# ...
my_file = forms.FileField()
L'utilisation de ce type de formulaire implique quelques spécificités.
Dans la vue, l'objet request.FILES
doit être fourni à l'initialisation du
formulaire :
# views.py
def my_view(request):
# ...
form = MyForm(request.POST, request.FILES)
# ...
Dans la template, il faut préciser l'attribut enctype
du <form>
:
{# my_app/templates/my_app/my_form_template.html #}
<form action="" enctype="multipart/form-data" method="POST">
...
</form>
Plusieurs réglages dans settings.py
permettent d'activer ou non certaines
fonctionnalités liées à l'internationalisation et la localisation :
USE_I18N
: active ou non le module de traductionUSE_L10N
: active ou non l'affichage des dates et des nombres selon la langueUSE_TZ
: active ou non la gestion des fuseaux horairesLANGUAGE_CODE
: langue par défautLANGUAGES
: liste des langues connues par l'applicationLOCALE_PATHS
: chemins vers les fichiers de traductiongettext
Pour traduire les différents textes de l'interface, on utilise les fonctions ugetttext
,
ou plus souvent ugettext_lazy
. Pour la simplicité de l'écriture, on importe
généralement cette fonction avec l'alias '_'.
from django.utils.translation import ugettext_lazy as _
# models.py
from django.utils.translation import ugettext_lazy as _
class Book(models.Model):
name = models.CharField(max_length=100,
verbose_name=_('Title'))
class Meta:
db_table = 'task'
verbose_name = _('Book')
verbose_name_plural = _('Books')
# forms.py
from django.utils.translation import ugettext_lazy as _
class BookSearchForm(models.Model):
search_text = models.CharField(label=_('Search text'))
# views.py
from django.utils.translation import ugettext_lazy as _
def confirmation(request, result):
if result == 'OK':
confirmation = _('Verification succeeded')
else:
confirmation = _('Verification failed')
# Render form
return render(request, 'confirmation.html', {
'confirmation': confirmation,
})
Deux tags permettant de traduire l'interface directement dans les templates sont disponibles :
Il permet de traduire une chaine de caractères simple ou le contenu d'une variable.
{% load i18n %}
<title>{% trans "List of books" %}</title>
<title>{% trans page_title %}</title>
Il permet de mixer chaînes de caractères et variables pour traduire des chaînes complexes.
{% load i18n %}
{% blocktrans with book_t=book|title author_t=author|title %}
<p>This is {{ book_t }} by {{ author_t }}</p>
{% endblocktrans %}
La commande makemessages
permet de créer le fichier traduction pour une langue
donnée (fichier texte avec l'extension ".po" contenant les identifiants de messages
et les traductions correspondantes). Cette commande doit être lancée depuis la racine
de l'application ou du projet pour lequel on crée le fichier car elle génère une
arborescence de dossiers locale/LANG/LC_MESSAGES
.
$ django-admin.py makemessages -l fr
La commande compilemessages
permet de compiler le fichier de traduction
pour qu'il soit utilisable dans le code Python.
$ django-admin.py compilemessages -l fr
django-admin.py
et manage.py
Les commandes django-admin.py
sont très utilisées pour l'administration d'un
pojet Django (création d'une application, synchronisation de la base, compilation
des fichiers de traduction, ...).
Le point d'entrée ./manage.py
se greffe autour de django-admin.py
et
s'exécute dans le contexte du projet (chargement des settings
, ajout du projet
dans sys.path
).
Lancer le script sans argument permet de lister les commandes disponibles :
$ ./manage.py
[django]
check
cleanup
compilemessages
createcachetable
...
Note : Il faut que l'environnement virtualisé soit démarré pour que Django soit chargé et les différents modules soient chargés.
Écrire une commande standalone peut être très utile dans le cadre de tâches d'administration qui peuvent être lancées périodiquement et automatiquement (cron).
Les commandes doivent être des fichiers Python placés dans un module
management/command
de l'application.
├── my_app
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── management
│ │ ├── __init__.py
│ │ ├── commands
│ │ │ ├── __init__.py
│ │ │ ├── my_test_command.py
Pour créer une commande personnalisée, il faut écrire une classe Command
qui hérite
de django.core.management.base.BaseCommand
.
# my_app/management/command/my_test_command.py
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
args = '...'
help = 'Do specific administration stuff'
def handle(self, *args, **options):
self.stdout.write('Command started')
# Traitements
self.stdout.write('Command ended')
Il est bien sûr possible de lancer une commande personnalisée à la main, tout
simplement en utilisant directement dans le terminal le point d'entrée ./manage.py
:
$ ./manage.py my_test_command
Il peut être aussi très utile d'automatiser cette execution via une tâche cron :
# Cron tasks
0 * * * * /project_path/manage.py my_test_command
from django.core import management
management.call_command("my_test_command")
Pour activer l'interface d'administration il faut commencer par :
INSTALLED_APPS
;AdminSite
et connecter une URL vers cette page ;admin.py
des applications.ModelAdmin
La classe est la représentation d'un modèle dans l'interface d'administration.
ModelAdmin
Si on ne souhaite pas personnaliser la représentation du modèle dans l'interface d'administration, il existe une version simplifiée de déclaration :
# admin.py
from django.contrib import admin
from myproject.myapp.models import Book
admin.site.register(Book)
Il est cependant possible de surcharger le ModelAdmin
d'un modèle afin de
personnaliser son comportement :
# admin.py
from django.contrib import admin
from myproject.myapp.models import Book
class BookAdmin(admin.ModelAdmin):
# Personnalisations
admin.site.register(Book, BookAdmin)
Les propriétés list_display
et list_display_links
permettent respectivement
de spécifier les colonnes que l'on souhaite voir apparaitre dans la liste et de préciser
lesquelles d'entre elles doivent être cliquables.
L'attribut list_filter
permet de mettre en place une recherche type recherche
à facettes dans une barre latérale à droite.
Si le modèle à une propriété de type Date
ou Datetime
, la propriété date_hierarchy
peut être précisée pour créer un index par date.
L'attribut search_fields
permet de lister les champs sur lesquels la recherche
doit être exécutée.
# admin.py
from django.contrib import admin
from myproject.myapp.models import Book
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'release']
list_display_links = ['title']
list_filter = ['author']
date_hierarchy = 'release'
search_fields = ['title', 'author__name']
admin.site.register(Book, BookAdmin)
Grâce aux propriétés fields
ou exclude
, il est possible de spécifier
quels champs on souhaite voir apparaître dans les formulaires de l'interface
d'administration
La propriété fieldsets
permet d'aller plus loin et d'organiser la mise en
page du formulaire.
Il est possible d'aller encore plus loin en surchargeant la template d'un formulaire
ou même d'écrire son propre formulaire et de le déclarer dans le ModelAdmin
# admin.py
from django.contrib import admin
from django import forms
from myproject.myapp.models import Book
class BookAdminForm(forms.ModelForm):
# ...
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'release']
list_display_links = ['title']
list_filter = ['author']
date_hierarchy = 'release'
search_fields = ['title', 'author__name']
form = BookAdminForm
admin.site.register(Book, BookAdmin)
WSGI : interface entre un serveur web et une application web en Python
Application WSGI minimale :
def application(environ, start_response):
data = b'Hello, World!\n'
start_response('200 OK', [
('Content-type', 'text/plain'),
('Content-Length', str(len(data)))
])
return iter([data])
Table of contents | t |
---|---|
Exposé | ESC |
Autoscale | e |
Full screen slides | f |
Presenter view | p |
Source files | s |
Slide numbers | n |
Blank screen | b |
Notes | 2 |
Help | h |