Experts en logiciels libres, cartographie et analyse de données, nous concevons des applications métiers innovantes.
Nos valeurs :
"%s : un•e expert•e, fort de %s ans d'expérience à votre écoute" % (name, xp)
La Plateforme de développement Web pour les perfectionnistes sous pression.
— www.django-fr.org
Simplicity should be a key goal in design and unnecessary complexity should be avoided.
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
La documentation précise certaines conventions de codage spécifiques à Django. La PEP 8 fait référence pour le reste.
L'architecture de Django s'inspire du principe MVC (Model, View, Controller) ou plutôt MTV (Model, Template, View) :
La fonction controller est gérée par l'URL dispatcher qui permet de faire correspondre des URLs sous forme d'expressions régulières à des vues.
Python parcours sys.path pour chercher les modules à importer
/usr/lib/python,
/usr/local/lib/python, ~/.local/lib/python ainsi que le répertoire courant en général$ virtualenv env # crée l'environnement
$ ./env/bin/python # lance le python de l'environnement virtuel
(env) $ source env/bin/activate # ajoute ./env/bin en tête du PATH
(env) $ python # lance le python de l'environnement virtuel
(env) $ deactivate # rétablit le path
$ python # lance le python du système
Cela permet ainsi de créer plusieurs environnement avec différentes version de python, de Django, etc.
Quelques alternatives/extensions : virtualenvwrapper, anaconda, pyenv.
Proposez vos idées pour un tutorial original !
Sur les slides, des exemples basés sur une bibliothèque seront utilisés
À défaut d'idées dans l'audience, nous créerons une application de gestion de Todo lists :
$ virtualenv venv
$ source venv/bin/activate
(venv) $ pip install django
(venv) $ django-admin startproject library
(venv) $ cd library
(venv) $ ./manage.py runserver
Django vient avec ce serveur HTTP de développement (à ne surtout pas utiliser en production pour des raisons de performances et de sécurité)
settings.py)└── library
├── library
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
library : conteneur du projet (le nom est sans importance)library/manage.py : utilitaire en ligne de commande permettant différentes actions sur le projetlibrary/library : paquet Python effectif du projetlibrary/library/settings.py : réglages et configuration du projetlibrary/library/urls.py : déclaration des URLs du projetlibrary/library/wsgi.py : point d'entrée pour déployer le projet avec WSGIIl est important de différencier la notion de projet et d'application.
Une application est une application Web qui fait quelque chose – par exemple un système de blog, une base de données publique ou une application de sondage
Un projet est un ensemble de réglages et d’applications pour un site Web particulier.
Un projet peut contenir plusieurs applications. Une application peut apparaître dans plusieurs projets.
— docs.djangoproject.com
pip)manage.py startapp crée automatiquement un patron d'app dans un nouveau
répertoireINSTALLED_APPS = [...] ) Django "impose" une organisation du code (noms et emplacements des fichiers)
views.py dans le répertoire de l'app (ou dans le package views de l'app)urls.py) from django.conf.urls import url, include
urlpatterns = [
# soit on définit des couples url-vue
url(r'^$', une_vue),
# ... ou on inclut les urls depuis une autre app
url(r'', include('books.urls')),
]
$ ./manage.py startapp books
├── books/
│ ├── __init__.py
│ ├── admin.py
| ├── apps.py
│ ├── migrations/__init__.py
│ ├── models.py
│ ├── tests.py
│ ├── views.py
models.py : déclaration des modèles de l'applicationviews.py : écriture des vues de l'applicationadmin.py : comportement de l'application dans l'interface d'administrationtests.py : Il. Faut. Tester.migrations: modifications successives du schéma de la base de donnéesLa commande devra être lancée avec le bon nom de module (todo).
# settings.py
INSTALLED_APPS = (
'django.contrib.admin',
...
'books',
)
Django propose une configuration par défaut pour une base SQLite (cf : settings.py).
Voici un exemple de configuration pour une base Postgresql :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'library_db',
'USER': 'library_user',
'PASSWORD': 'Cx12%a03oa',
'HOST': 'localhost'
}
}
$ ./manage.py migrate
Créer le projet (ex: formation)
Puis une application (ex: todo)
Enfin activer l'application
HttpRequest et renvoie un objet HttpResponseHttpRequest correspondant à la requête du clientHttpRequest en paramètreHttpResponse en retour de la fonction ou de la classeHttpRequestPermet d'accéder à de nombreux attributs tels que
Peut être lu comme un flux
cf. https://docs.djangoproject.com/fr/stable/ref/request-response/#httprequest-objects
HttpResponsePermet de régler de nombreux attributs tels que
Peut être instancié directement avec le contenu comme paramètre
response = HttpResponse("foobar")
Peut être écrit comme un flux
request.write()
Est dérivé en sous-classes (ex. HttpResponseRedirect)
cf. https://docs.djangoproject.com/fr/stable/ref/request-response/#httpresponse-objects
En somme, une vue se résume à déclarer une url :
# books/urls.py
from django.conf.urls import patterns, include, url
import books.views
urlpatterns = [
url(r'^ma-vue$', book.views.ma_vue),
]
et retourner un contenu en fonction d'une requête
# books/views.py
from django.http import HttpResponse
def ma_vue(request):
return HttpResponse("mon contenu")
Créer une vue affichant "Bienvenue dans la Todo List"
Ce sera notre page d'accueil, elle sera accessible sur http://127.0.0.1:8000/
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
release = models.DateField(blank=True, null=True)
borrowed = models.BooleanField(default=False)
def __str__(self):
return self.title
Ces 3 types de champs suffisent pour l'appli todo
Pour python2, utiliser @python2_unicode_compatible
L'ajout de la classe Meta dans un modèle permet de déclarer des options de métadonnées sur le modèle. Exemple :
class Book(models.Model):
...
class Meta:
db_table = 'book'
verbose_name = 'Book'
verbose_name_plural = 'Books'
ordering = ('-released', )
D'autres options permettent par exemple de :
Documentation : https://docs.djangoproject.com/fr/stable/ref/models/
Ici on utilisera uniquement verbose_name et ordering
Mentionner le fait que les noms de modele sont declinés de leur nom système
CharField (une ligne avec longueur max)TextField (multiligne)EmailField (vérifie la syntaxe de l'adresse)IntegerField et PositiveIntegerFieldFloatFieldDecimalField (précision fixe, non soumis aux arrondis) AutoField (IntegerField incrémenté automatiquement)BooleanField et NullBooleandFieldDateField, TimeField et DateTimeFieldDurationFieldFileField et ImageFieldFilePathFieldChaque type de champs possède ses propres propriétés. Cependant, certaines sont communes et souvent utilisées comme :
verbose_name: label du champnull : valeur NULL autorisée ou non en base de donnéesblank : valeur vide autorisée lors de la validation du champ dans un formulairedefault : valeur par défaut pour une nouvelle instanceeditable : le champ doit-il apparaître automatiquement dans les formulaireschoices permet d'expliciter la liste de valeurs possiblesprimary_key est la clé primaire (remplace id)unique ajoute une contrainte d'unicitévalidators permet d'ajouter des contraintes de validation au niveau du modèleDocumentation https://docs.djangoproject.com/fr/stable/ref/models/fields/#field-options
migrations/. Il est conseillé d'enregistrer les migrations avec le code(venv) $ ./manage.py makemigrations
(venv) $ ./manage.py migrate
# admin.py
from django.contrib import admin
from books.models import Book
admin.site.register(Book)
L'interface d'administration est le "back-office" automatique" de Django qui liste les instances et par introspection des modèles, créer les formulaire de création/modification correspondants.
Elle est personnalisable et permet de modifier :
Documentation : https://docs.djangoproject.com/fr/stable/ref/contrib/admin/
Par défaut: Créer le modèle tâche ayant notamment les champs :
Pour accèder à l'admin de Django, vous aurez besoin d'un superutilisateur :
(venv) $ ./manage.py createsuperuser
But: Le modèle doit apparaitre dans l'interface d'administration avec les bons champs
# book/views.py
from django.shortcuts import render
from books.models import Book
def book_list(request):
books = Book.objects.all()
context = {
'books': books
}
return render(request, 'books/book_list.html', context)
Ce style de vue est dit "Function-based" (par opposition à "Class-based").
{# books/templates/books/book_list.html #}
<h1>Liste des livres</h1>
{% if books %}
<ul>
{% for book in books %}
<li>{{ book }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun livre !</p>
{% endif %}
Documentation: https://docs.djangoproject.com/fr/stable/topics/templates/
Routeur basé sur des regex, avec un préfixe par application. Ce préfixe n'est pas obligatoire mais permet de classer les vues afin d'éviter les conflits, par exemple :
book/list et book/addmovie/list et movie/add# books/urls.py
from django.conf.urls import patterns, include, url
urlpatterns = [
url(r'^book_list$', books.views.book_list, name='book_list'),
]
# library/urls.py
...
urlpatterns = [
...
url(r'^books/', include('books.urls', namespace="books")),
]
Une vue basée sur une fonction Django est simplement une fonction Python qui prend en entrée une requête HTTP et retourne une réponse HTTP.
Cette réponse peut être une page HTML, un document XML, une redirection, une erreur 404, ...
Ces vues sont généralement écrites dans le fichier views.py de l'application.
# some_app/views.py
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
Une vue basée sur une classe Django est simplement une classe Python préformatée qui prend en entrée une requête HTTP et retourne une réponse HTTP.
# some_app/views.py
from django.http import HttpResponse
from django.views.generic import View
class CurrentDatetimeView(View):
def get(self, request, * args, ** kwargs):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
C'est un simple fichier texte qui peut générer n'importe quel format de texte (HTML, XML, CSV, ...).
Un template a accès à des variables qui lui auront été passées via un contexte par la vue.
Par défaut, Django fournit sa propre syntaxe de template mais il est possible de la remplacer par un autre moteur comme Jinja2.
Pour retrouver les templates d'un projet, Django se base sur le réglage TEMPLATES. Le plus souvent on stocke les templates :
<app>/templates/<app>.
Ils seront retrouvés grâce au loader activé par défaut quand la clé APP_DIRS vaut True.templates/ à la racine du projet qu'il faudra déclarer dans la clé DIRS.Dans ce mécanisme de découverte, l'ordre importe : cela permet de surcharger les templates d'autres applications.
{{ ma_variable }}
Il est possible de modifier l'affichage d'une variable en appliquant des filtres. Un filtre peut prendre (ou non) un argument. Les filtres peuvent être appliqués en cascade. Quelques exemples :
{{ name|lower }}
{{ text|linebreaksbr }}
{{ current_time|time:"H:i" }}
{{ weight|floatformat:2|default_if_none:0 }}
Django fournit nativement une liste de filtres assez intéressante et il est possible d'écrire des filtres personnalisés facilement.
Les tags sont plus complexes que les variables, ils peuvent créer du texte ou de la logique (boucle, condition, ...) dans la tempate.
{% if condition %} .. {% else %} .. {% endif %}
{% for item in list %} .. {% endfor %}
<a href="{% url 'books:book_detail' book.pk %}">Django book</a>
Django fournit aussi plusieurs tags nativement et il est possible d'écrire ses propres tags.
L'intérêt de l'héritage de template est par exemple de pouvoir créer un squelette HTML contenant tous les éléments communs du site et définir des blocs que chaque template pourra surcharger.
Dans une template parent, la balise {% block %} permet de définir les blocs surchargeables.
Dans une template enfant, la balise {% extends %} permet de préciser de quel template celui-ci doit hériter.
{# templates/base.html #}
<html>
<head>
<title>
{% block title %}
...
{% endblock %}
</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<header>Entête commune à tout le site</header>
<section>
{% block content %}
...
{% endblock %}
</section>
<footer>Pied de page commun à tout le site</footer>
</body>
</html>
{# books/templates/books/book_list.html #}
{% extends "base.html" %}
{% block title %}
Liste des livres
{% endblock %}
{% block content %}
{% if books %}
<ul>
{% for book in books %}
<li>{{ book }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun livre !</p>
{% endif %}
{% endblock %}
L'intérêt de l'inclusion de template est de pouvoir factoriser du code de template :
Cela peut être utile dans différents cas :
{# templates/base.html #}
<html>
<head>
<title>
{% block title %}
...
{% endblock %}
</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
{% include 'templates/header.html' %}
<section>
{% block content %}
...
{% endblock %}
</section>
{% include 'templates/footer.html' %}
</body>
</html>
ROOT_URLCONF dans les settings), souvent projet/urls.pyurlpatternsurls.py de chaque application.HttpRequest) puis toutes les valeurs capturées dans la regex.Le module URLconf est un fichier urls.py contenant une variable urlpatterns :
# library/urls.py
from django.conf.urls import patterns, url
urlpatterns = [
url(r'^myview$', myapp.views.my_view, name='my_view'),
...
]
À chaque vue peut être associé un nom système qui pourra servir lors de l'inversion d'une url.
Souvent, l'URLconf racine inclura les modules URLconf de chaque application :
# urls.py
from django.conf.urls import patterns, url
urlpatterns = [
url(r'^myapp/', include('myapp.urls', namespace='myapp')),
...
]
À chaque application peut être associé un namespace qui pourra servir lors de l'inversion d'une url.
url(r'^myview$', my_view, name='my_view')
La vue aura en argument seulement l'objet HttpRequest.
url(r'^myview_by_month/(?P<year>\d{4})/(?P<month>\d{2})/$',
MyViewByMonth.as_view(),
name='myview_by_month'),
La vue aura en argument l'objet HttpRequest, puis les valeurs trouvées dans l'expression régulière (ex: request, year=2014, month=12).
La résolution d'une url consiste à partir d'une URL et trouver la regex correspondant ainsi que ses paramètres.
La résolution inversée part d'un nom système de vue et de paramètres pour arriver à une URL.
Ceci permet de modifier les motifs sans avoir à retoucher toutes les fois où
l'url est appelée (par exemple dans un <a href>).
# project/urls.py
urlpatterns = [
url(r'^book/', include('book.urls', namespace='book')),
]
# book/urls.py
urlpatterns = [
url(r'^list$', book.views.list, name='list'),
url(r'^edit/(?P<pk>\d+)$', book.views.edit, name='edit'),
]
Dans un template:
<a href="{% url 'book:list' %}">Liste des livres</a>
Dans du code python :
from django.urls import reverse
return HttpResponseRedirect(reverse('book:edit', pk=12))
Créer la vue liste des tâches et détail d'une tâche
Créer auparavant quelques tâches via l'interface d'administration
Pour la liste de toutes les tâches : Task.objects.all() et pour le détail Task.objects.get(pk=<pk>)
django.formsDjango possède une bibliothèque assez complète de gestion de formulaires : django.forms.
Les concepts principaux sont les suivants:
Widget : permet de gérer et faire le rendu d'un widget HTML (ex: un champ <input>, <textarea>, ...)Field : permet de gérer l'initialisation et la validation d'un champ de formulaireForm : permet de gérer un ensemble de champs de formulaires, ainsi que l'initialisation, le rendu et la validation du formulaire globalModelForm : permet de gérer des formulaires basés sur des modèles (création / modification d'une instance du modèle)# contact/forms.py
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
cc_myself = forms.BooleanField(required=False)
__init__ : permet de personnaliser l'intialisation du formulaire (par exemple : pré-remplir le champ sender par l'email de l'utilisateur connecté)clean : permet de personnaliser la validation du formulaire (par exemple : vérifier que sender a bien été fourni si cc_myself a été coché)La bibliothèque django.forms fournit plus de 20 types de champs différents, dont voici les principaux :
CharField, SlugField, RegexField, EmailField, UrlFieldFloatField, IntegerFieldBooleanField, NullBooleandFieldChoiceField, MultipleChoiceFieldDateField, DateTimeField, TimeField, DurationFieldFileField, FilePathField, ImageFieldCertains modules annexes fournissent leurs propres champs et il est possible d'écrire des champs personnalisés.
Rappel : ces champs gérent les données, ce sont les widgets qui gèrent la manière
dont ils sont saisis. Par exemple, un CharField gérera du texte et aura par défaut
un widget TextInput, mais il est possible de spécifier un widget un widget Textarea.
from django.shortcuts import render
from django.http import HttpResponseRedirect
from myapp.forms import ContactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Traite les données dans form.cleaned_data puis redirige
# ...
return HttpResponseRedirect('/thanks/')
else:
form = ContactForm()
# Affiche le formulaire
return render(request, 'contact.html', {'form': form})
def contact(request):
form = ContactForm(request.POST or None)
if form.is_valid():
# ...
return HttpResponseRedirect('/thanks/')
return render(request, 'contact.html', {'form': form})
<form action="/contact/" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
L'utilisation du tag {% csrf_token %} est importante car elle permet de protéger le formulaire des attaques de type CSRF (Cross Site Request Forgeries).
Un formulaire peut être rendu de différentes manières :
Des packages de la communauté (cripsy_forms, floppyforms, material) permettent d'afficher le bon markup pour un framework HTML/CSS.
La classe ModelForm permet de créer automatiquement des formulaires basés sur des modèles.
Le fonctionnement est assez semblable à celui des formulaires classiques à quelques différences près :
Meta est nécessaire pour préciser sur quel modèle doit se baser le formulaire__init__ prend en argument l'instance du modèle à modifier (ou None dans le cas d'une création)save qui permet d'enregistrer l'instance éditée via le formulaireeditable=False# book/models.py
class Book(models.Model):
title = models.CharField(max_length=100)
release = models.DateField()
borrowed = models.BooleanField(default=False)
# book/forms.py
class AddBookForm(forms.ModelForm):
class Meta:
model = Book
fields = ('title', 'release')
# book/views.py
def add_book(request):
form = AddBookForm(request.POST or None)
if form.is_valid():
form.save()
return HttpResponseRedirect('/books/')
return render(request, 'add_book.html', {'form': form})
Créer les vues d'ajout et modification d'une tâche
Afficher également des liens depuis la liste de tâches
La bibliothèque django.models fournit différents champs spécifiques pour représenter les relations entre modèles.
models.ForeignKey : représente une relation de type 1-Nmodels.ManyToManyField : représente une relation de type N-Nmodels.OneToOneField : représente une relation de type 1-1Le champ ForeignKey doit être déclaré avec comme premier argument le modèle auquel il est lié par cette relation 1-N. L'argument optionnel related_name permet de nommer la relation inverse à partir de ce modèle lié.
La représentation de ce champ en base de données est une contrainte de type clé étrangère.
Un livre est associé à un auteur, un auteur peut avoir écrit plusieurs livres.
# book/models.py
class Author(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, related_name='books')
Le champ ManyToManyField doit être déclaré de la même manière que le champ ForeignKey.
La représentation de ce champ en base de données est une table contenant deux clés étrangères vers les deux tables des modèles liés.
Un livre est associé à plusieurs catégories, plusieurs livres peuvent appartenir à une même catégorie.
# book/models.py
class Category(models.Model):
label = models.CharField(max_length=50)
class Book(models.Model):
title = models.CharField(max_length=100)
categories = models.ManyToManyField(Category, related_name='books')
La déclaration du OneToOneField est similaire.
La représentation de ce champ en base de données est une clé étrangère possédant une contrainte d'unicité.
Un livre est associé à un seul code barre, un code barre correspond à un seul livre.
# book/models.py
class BarCode(models.Model):
code = models.CharField(max_length=50)
class Book(models.Model):
title = models.CharField(max_length=100)
barcode = models.OneToOneField(BarCode, related_name='book')
Mettre en place une modélisation gérant des listes de tâches partagées entre utilisateurs
Ici l'attendu est
django.db.backends.postgresqldjango.db.backends.mysqldjango.db.backends.oracledjango.db.backends.sqlite3DateRangeField, JSONField, etc)settings.py (variable DATABASES), ainsi que la configuration du nom de la base, du serveur, et de l'authentificationDocumentation https://docs.djangoproject.com/fr/stable/ref/databases/
Pour créer une instance, il suffit de l'instancier en passant en argument les noms des attributs du modèle. L'instance dispose ensuite d'une méthode save qui permet de l'enregistrer en base de données.
>>> b = Book(name='Two scoops of django',
release=date(2013, 08, 31))
>>> b.save()
La même méthode save est utilisée pour enregistrer en base de données des modifications sur l'instance.
>>> b.name ='Two scoops of django - Best practices'
>>> b.save()
Pour supprimer une instance, il suffit d'appeler la méthode delete() qui permet de supprimer directement la ligne en base de données.
>>> b = Book(name='Two scoops of django',
release=date(2013, 08, 31))
# Création en BDD
>>> b.save()
# Suppression
>>> b.delete()
Manager & QuerysetPour récupérer une ou plusieurs instances, il faut construire un Queryset via un Manager associé au modèle.
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.
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.
A partir d'un queryset il est possible d'obtenir un autre queryset plus spécialisé. Un queryset est paresseux (la requête SQL n'est faite que lorsqu'il n'est plus possible de la retarder).
>>> Book.objects.all()
Les méthodes de filtrage principalement utilisées sont filter et exclude. Il est possible de les chaîner.
>>> Book.objects.filter(
release__gte=date(2013, 01, 01)
).exclude(
borrowed=True
)
>>> Book.objects.exclude(borrowed=True).order_by('title')
__iexact pour une recherche insensible à la casse__contains pour chercher à l'intérieur__lt, __lte, __gt,__gte pour les inégalitésDocumentation : https://docs.djangoproject.com/fr/stable/ref/models/lookups/
books = Book.objects.filter(title__startswith="Le")
books = Book.objects.filter(release__year__lt=1950) \
.exclude(title__icontains="fleurs")
Pour l'opérateur OU ou des requêtes plus complexes, utiliser django.db.models.F
et django.db.models.Q, qui permettent des combinaisons avant exécution.
La méthode get permet de récupérer une instance particulière.
>>> Book.objects.get(pk=12)
La méthode ne peut retourner qu'une instance précise, il faut donc que le filtre fourni ne soit pas ambigu. Il faut veiller à filtrer sur un champ unique (ou un ensemble de champs uniques ensemble).
Book.DoesNotExist sera levée (de manière générique : <Model>.DoesNotExist).Book.MultipleObjectsReturned (<Model>.MultipleObjectsReturned).Pour les relations entre instances (ForeignKey, ManyToManyField), Django fournit un Manager spécifique nommé RelatedManager. Il permet notamment de :
ForeignKey vers une instance donnéeManyToManyFieldRetrouver les livres disponibles d'un auteur :
>>> author = Author.objects.get(pk=25)
>>> author.books.filter(borrowed=False)
Ajouter un livre à une catégorie :
>>> category = Category.objects.get(pk=5)
>>> book = Book.objects.get(pk=12)
>>> category.books.add(book)
Supprimer l'association de livres à une catégorie :
>>> category = Category.objects.get(pk=5)
>>> category.books.clear()
Mettre en place un formulaire de filtrage de tâches :
django_extensions : plusieurs extensions et outils d'administration très pratiquesdjango_debug_toolbar : une barre latérale permettant de faire du debug et du profiling page par pagedjango_hijack: permet de se connecter avec un autre utilisateur sans se déconnecterdjango_extra_views: apporte d'autres CBV pour des formulaires et vues toujours plus rapidesdjango_braces: apporte des mixins pour vos CBVfactory_boy : création de grappes de données pour les testsdjango_jenkins : intégration à Jenkinsdjango_compressor : compression des fichiers statiquesdjango_pagination : affichage de listes paginéesdjango_sorting : affichage de tableaux triablesdjango_filters : création de liste filtréesdjango_crispy_forms : affichage de forms avec Bootstrap/Foundation/Uniformdjango_breadcrumbs : création de fil d'arianedjango_xworkflows : gestion de workflowsdjango_modeltranslation : gestion de modèles multilingueseasy_thumbnails ou versatileimagefield : gestion de miniatures pour les imagesdjango_tinymce : intégration d'un widget TinyMCE| 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 |