Les vues Django

Les vues (ou view en angalis) sont la deuxième notion du concept MVC (Modèle Vue Controleur).

A quoi sert une vue?

Les vues créent un contexte, intègre un template (ou gabarit) puis retournent un objet de type HttpResponse.

Comment fonctionne une vue?

Tout d'abord votre serveur web reçoit une requête avec comme contenu: une adresse, un entête HTTP, divers données fournit par GET, POST, etc. Ensuite le serveur web demande à Django ce qu'il doit faire avec cette rêquete. L'adresse est comparée dans une sorte de table de routage (url.py) puis Django exécute la fonction associée au pattern de l'adresse. Cette fonction est donc une vue, puisqu'elle reçoit un contexte et renvoit un objet HttpResponse.

Passons à la pratique

Reprenons l'exemple du chapitre sur les applications Django:

eboutique/url.py

#!/usr/bin/python
#-*- coding: utf-8 -*-

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'eboutique.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
    url(r'^$', 'backoffice.views.home', name='home'),
)

backoffice/views.py

#!/usr/bin/python
#-*- coding: utf-8 -*-

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def home(request):
    return HttpResponse("Bonjour monde!")

résultat:

Première chose que l'on remarque c'est que la vue home dans notre fichier view.py n'est en réalité qu'une simple fonction qui récupère un objet request et qui retourne un objet HttpReponse

L'objet request c'est quoi?

En anglais request signifie "demande / sollicitation", ce sont donc des informations envoyées par un client et celui-ci attend une réponse en fonction des informations qu'il a envoyé.

Qui a-t-il dans un objet request?

Pour cela, rien de plus simple, ajouter la fonction dir dans votre vue:

backoffice/views.py

def home(request):
    print dir(request)
    return HttpResponse("Bonjour monde!")

résultat:

['COOKIES', 'FILES', 'GET', 'META', 'POST', 'REQUEST', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cookies', '_encoding', '_get_cookies', '_get_files', '_get_get', '_get_post', '_get_request', '_initialize_handlers', '_is_secure', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_parse_content_type', '_post_parse_error', '_read_started', '_set_cookies', '_set_get', '_set_post', '_stream', '_upload_handlers', 'body', 'build_absolute_uri', 'csrf_processing_done', 'encoding', 'environ', 'get_full_path', 'get_host', 'get_signed_cookie', 'is_ajax', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'read', 'readline', 'readlines', 'resolver_match', 'session', 'upload_handlers', 'user', 'xreadlines']

Pour voir les données associées à l'objet request:

backoffice/views.py

def home(request):
    print request.__dict__
    return HttpResponse("Bonjour monde!")

Je ne peux pas copier coller le résultat -il y en a trop- je vous invite donc à effectuer cette manipulation sur votre machine. Mais vous verrez qu'il existe des données sur tout comme l'adresse IP du client, le navigateur utilisé, les données de session, le port utilisé, le language, le csrftoken, les données GET et POST, etc.

Récupérer GET et POST

Bon passons aux choses sérieuses. Comment lire les données GET et POST envoyées par le client?

backoffice/views.py

def home(request):
    string = request.GET['name']
    return HttpResponse("Bonjour %s!" % string)

Réponse http

Une réponse où aucune erreur n'est à signaler possède le code 200, vous pouvez le vérifier sur votre navigateur en lisant l'entête de la réponse:

Il est possible de retourner d'autres types de réponse, comme la fameuse 404 indiquant que le document est introuvable:

backoffice/views.py

from django.http import HttpResponse, HttpResponseNotFound

def home(request):
    return HttpResponseNotFound("Erreur fichier introuvable")

Il existe d'autres réponses HTTP:

les réponses HTTP possibles

from django.http import *
------------------------------------
HttpResponse                  → 200
HttpResponseRedirect          → 302
HttpResponsePermanentRedirect → 301
HttpResponseNotModified       → 304
HttpResponseBadRequest        → 400
HttpResponseNotFound          → 404
HttpResponseForbidden         → 403
HttpResponseNotAllowed        → 405
HttpResponseGone              → 410
HttpResponseServerError       → 500
 

Http404 exception

Si vous voulez relever une erreur 404 standard, vous pouvez lever une exception:

backoffice/views.py

from django.http import HttpResponse, Http404

def home(request):
    
    if request.GET and request.GET["test"]:
        raise Http404
    return HttpResponse("Bonjour Monde!")

L'erreur suivante devrait apparaître si vous ajouter une variable GET test:

Récupérer l'id dans l'url

Comme nous l'avons vu dans les chapitres précédents, l'url passe par une sorte de table de routage.

L'url peut contenir un id; comment récupérer cette information?

eboutique/urls.py

#!/usr/bin/python
#-*- coding: utf-8 -*-

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'eboutique.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
    url(r'^$', 'backoffice.views.home', name='home'),
    url(r'^commentaire/(?P<pk>\d+)/$', 'backoffice.views.commentaire'),
)

Et ajoutons notre nouvelles vue:

backoffice/views.py

def commentaire(request, pk):
    
    return HttpResponse("id: {0}".format(pk, ) )

Résultat:

Nous avons utilisé le paramètre pk dans le fichier urls.py, mais vous pouvez choisir n'importe quel nom du moment qu'il est renseigné dans la vue.

Retourner un JSON

Si vous travaillez en Ajax il vous sera peu être utile de savoir retourner un JSON:

from django.http import JsonResponse

def ma_vue(request):
    return return JsonResponse({'foo': 'bar'})  

Templates

Comment exploiter les templates?

backoffice/views

from django.template.response import TemplateResponse

def home(request):
    return TemplateResponse(request, 'home.html', {'name': 'olivier'})

Dans cet exemple, nous utilisons la fonction TemplateResponse qui envoit l'objet request, le nom du template et un dictionnaire (contexte). Vous pouvez mettre dans ce dictionnaire les données que vous voulez.

Et voici notre template:

backoffice/templates/home.html

Bonjour {{name}}

Les doubles crochets indiquent que name est une variable.

Les vues génériques

Très rapidement vous vous rendrez compte que vos vues se ressemblent toutes plus ou moins. Un peu comme dans la contrib Admin, finalement on voudra lister des modèles, puis pouvoir en créer, modifier et enfin supprimer.

Le concept de Django c'est de faciliter au maximum la conception d'un projet. Inutile de répéter du code, on se veut DRY et factoriser le code est aussi intéressant sur la performance du développeur que sur la maintenance.

TemplateView, la vue générique de base

Commençons par la vue générique de base: TemplateView. Cette vue se contente de renvoyer un fichier type static, pas de modèles, pas de requêtes en base de données.

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *

urlpatterns = patterns('',
    url(r'^$', TemplateView.as_view(template_name="home.html")),
)

backoffice/templates/home.html

Bonjour Monde!

Dans cet exemple, le template home.html sera utilisé comme nous l'avons indiqué. Nul besoin d'instancier quoi que ce soit, la classe TemplateView ne nécessite que l'appel de la méthode as_view.

Les attributs de la classe TemplateView

content_type = None
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace']
response_class = <class 'django.template.response.TemplateResponse'>
template_name = None

Les méthodes de la classe TemplateView

_allowed_methods(self)
as_view(cls, **initkwargs)
dispatch(self, request, *args, **kwargs)
get(self, request, *args, **kwargs)
get_context_data(self, **kwargs)
get_template_names(self)
http_method_not_allowed(self, request, *args, **kwargs)
options(self, request, *args, **kwargs)
render_to_response(self, context, **response_kwargs)

Vous pouvez dériver Templateview pour mettre votre vue-classe dans la fichier views.py

eboutique/views.py

from django.views.generic import TemplateView

class IndexView(TemplateView):
    template_name = "templates/index.html"
    
    def post(self, request, **kwargs):
        print "Action lors d'un post"
        return render(request, self.template_name)

eboutique/urls.py

from django.conf.urls import include, url

urlpatterns = [
    url(r'^$', IndexView.as_view()),
]

N'oubliez pas de renseigner tous les chemins vers vos templates dans le fichier de configuration de votre projet:

eboutique/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ '', 'test'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

ListeView

Cette vue a vocation à travailler sur une liste d'objets. Elle est l'une des vues génériques les plus utilisées

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *

urlpatterns = patterns('',
    url(r'^$', ListView.as_view(model=Product)),
)

Par défaut le template chargé sera dans notre cas nommé product_list.html

backoffice/product_liste.html

Bonjour Monde!

Vous pouvez bien évidemment choisir l'emplacement et le nom de votre template:

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *

urlpatterns = patterns('',
    url(r'^$', ListView.as_view(model=Product, template_name="templates/product_list.html")),
)

Vous pouvez surclasser la vue générique pour ajouter des options plus facilement:

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *

class ProductsView(ListView):
    model = Product
    template_name = "template/product_list.html"

urlpatterns = patterns('',
    url(r'^$', ProductsView.as_view()),
)

Il nous manque plus que l'objet du contexte, c'est à dire nos produits:

class ProductsView(ListView):
    model = Product
    template_name = "template/product_list.html"
    context_object_name = "products"

et notre template:

{% for product in products %}
    {{product.id}}
{% endfor %}

Il est possible de modifier la queryset qui liste les produits:

class ProductsView(ListView):
    model = Product
    template_name = "template/product_list.html"
    context_object_name = "products"
    queryset = Product.objects.filter(id__gt=400)

ou en passant par la méthode get_queryset:

class ProductsView(ListView):
    model = Product
    template_name = "template/product_list.html"
    context_object_name = "products"
    def get_queryset(self):
        return Product.objects.filter(id__gt=450)

Si vous voulez passer d'autres données, vous pouvez passer par la méthode :

class ProductsView(ListView):
    model = Product
    template_name = "template/product_list.html"
    context_object_name = "products"
    
    def get_context_data(self, **kwargs):
        context = super(ProductsView, self).get_context_data(**kwargs)
        context['name'] = "olivier"
        return context

Et dans notre template:

Bonjour {{name}}

Les attributs de la classe ListeView

allow_empty = True 
content_type = None
context_object_name = None 
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace']
model = None
page_kwarg = 'page'
paginate_by = None 
paginate_orphans = 0
paginator_class = <class 'django.core.paginator.Paginator'>
queryset = None
response_class = <class 'django.template.response.TemplateResponse'>
template_name = None
template_name_suffix = '_list'

Les méthodes de la classe ListeView:

dispatch(request, *args, **kwargs)                 → retourne un HttpResponse
http_method_not_allowed(request, *args, **kwargs)  → est exécuté si la méthode est interdite
get_template_names(self)                           → retourne une liste de nom de templates
get_queryset(self)                                 → retourne la queryset
get_context_object_name()                          → retourne l'objet en question
get_context_data(self, **kwargs)                   → retourne le contexte
render_to_response(context, **response_kwargs)     → créer le contenu
_allowed_methods(self)
as_view(cls, **initkwargs)
get(self, request, *args, **kwargs)
get_allow_empty(self)
get_paginate_by(self, queryset)
get_paginate_orphans(self)
get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs)
options(self, request, *args, **kwargs)
paginate_queryset(self, queryset, page_size)

DetailView

Tout est identique à ListView sauf qu'il s'agit que d'un seul item:

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *

class ProductView(DetailView):
    model = Product
    context_object_name = "product"
    
urlpatterns = patterns('',
    url(r'^product/(?P<pk>\d+)/$', ProductView.as_view()),
)

et dans votre template:

{{product.id}} - {{product.name}}

Les arguments de la classe DetailView

content_type = None 
context_object_name = None  
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace'] 
model = None    
pk_url_kwarg = 'pk' 
queryset = None 
response_class = <class 'django.template.response.TemplateResponse'>
slug_field = 'slug' 
slug_url_kwarg = 'slug' 
template_name = None    
template_name_field = None  
template_name_suffix = '_detail'    

Les méthodes de la classe DetailView

_allowed_methods(self)
as_view(cls, **initkwargs)
dispatch(self, request, *args, **kwargs)
get(self, request, *args, **kwargs)
get_allow_empty(self)
get_context_data(self, **kwargs)
get_context_object_name(self, object_list)
get_paginate_by(self, queryset)
get_paginate_orphans(self)
get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs)
get_queryset(self)
get_template_names(self)
http_method_not_allowed(self, request, *args, **kwargs)
options(self, request, *args, **kwargs)
paginate_queryset(self, queryset, page_size)
render_to_response(self, context, **response_kwargs)

CreateView

CreateView est une vue générique qui vous aide à créer une page pour enregistrer un item.

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *
    
urlpatterns = patterns('',
    url(r'^product/new/$', CreateView.as_view(model=Product)),
)

Votre template:

<form action="" method="POST">
{% csrf_token %}
{{form.as_ul}}
<input type="submit" value="Créer"/>
</form>

Résultat:

Les attributs de la classe CreateView:

content_type = None
context_object_name = None
fields = None
form_class = None
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace']
initial = {}
model = None
pk_url_kwarg = 'pk'
prefix = None
queryset = None
response_class = <class 'django.template.response.TemplateResponse'>
slug_field = 'slug'
slug_url_kwarg = 'slug'
success_url = None
template_name = None
template_name_field = None
template_name_suffix = '_form'

Les méthodes de la classe CreateView:

_allowed_methods(self)
as_view(cls, **initkwargs)
dispatch(self, request, *args, **kwargs)
form_invalid(self, form)
form_valid(self, form)
get(self, request, *args, **kwargs)
get_context_data(self, **kwargs)
get_context_object_name(self, obj)
get_form(self, form_class) 
get_form_class(self)
get_form_kwargs(self)
get_initial(self)
get_object(self, queryset=None)
get_prefix(self)
get_queryset(self)
get_slug_field(self)
get_success_url(self)
get_template_names(self)
http_method_not_allowed(self, request, *args, **kwargs)
options(self, request, *args, **kwargs)
post(self, request, *args, **kwargs)
put(self, *args, **kwargs)
render_to_response(self, context, **response_kwargs)

UpdateView

Cette vue vous permet de modifier un item:

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *
    
urlpatterns = patterns('',
    url(r'^product/upd/(?P<pk>\d+)/$', UpdateView.as_view(model=Product)),
)

On reprend le même template que pour CreateView:

<form action="" method="POST">
{% csrf_token %}
{{form.as_ul}}
<input type="submit" value="Créer"/>

Résultat:

Les attributs de la classe UpdateView:

content_type = None
context_object_name = None
fields = None
form_class = None
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace']
initial = {}
model = None
pk_url_kwarg = 'pk'
prefix = None
queryset = None
response_class = <class 'django.template.response.TemplateResponse'>
slug_field = 'slug'
slug_url_kwarg = 'slug'
success_url = None
template_name = None
template_name_field = None
template_name_suffix = '_form'

Les méthodes de la classe UpdateView:

_allowed_methods(self)
as_view(cls, **initkwargs)
dispatch(self, request, *args, **kwargs)
form_invalid(self, form)
form_valid(self, form)
get(self, request, *args, **kwargs)
get_context_data(self, **kwargs)
get_context_object_name(self, obj)
get_form(self, form_class)
get_form_class(self)
get_form_kwargs(self)
get_initial(self)
get_object(self, queryset=None)
get_prefix(self)
get_queryset(self)
get_slug_field(self)
get_success_url(self)
get_template_names(self)
http_method_not_allowed(self, request, *args, **kwargs)
options(self, request, *args, **kwargs)
post(self, request, *args, **kwargs)
put(self, *args, **kwargs)
render_to_response(self, context, **response_kwargs)

DeleteView

Et enfin la vue qui propose de supprimer un item:

eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *
    
urlpatterns = patterns('',
    url(r'^product/delete/(?P<pk>\d+)/$', DeleteView.as_view(model=Product, success_url = "/products/")),
)

Voici notre template:

<form action="" method="post">{% csrf_token %}
    <p>Supprimer "{{ object }}"?</p>
    <input type="submit" value="Confirmer" />
</form>

Et le résultat:

Les attributs de la classe DeleteView:

content_type = None
context_object_name = None
http_method_names = [u'get', u'post', u'put', u'patch', u'delete', u'head', u'options', u'trace']
model = None
pk_url_kwarg = 'pk'
queryset = None
response_class = <class 'django.template.response.TemplateResponse'>
slug_field = 'slug'
slug_url_kwarg = 'slug'
success_url = None
template_name = None
template_name_field = None
template_name_suffix = '_confirm_delete'

Les méthodes de la classe DeleteView:

_allowed_methods(self)
as_view(cls, **initkwargs)
delete(self, request, *args, **kwargs)
dispatch(self, request, *args, **kwargs)
get(self, request, *args, **kwargs)
get_context_data(self, **kwargs)
get_context_object_name(self, obj)
get_object(self, queryset=None)
get_queryset(self)
get_slug_field(self)
get_success_url(self)
get_template_names(self):
http_method_not_allowed(self, request, *args, **kwargs)
options(self, request, *args, **kwargs)
post(self, request, *args, **kwargs)
render_to_response(self, context, **response_kwargs)