Django REST Framework DRF API

Django REST Framework est une boite à outils puissante et flexible qui vous facilite la création d'application web API.

Je dois avouer que ce projet a complétement changé ma manière de travailler sur des projets Django. Tout est centralisé et combiné avec les outils AngularJS / Grunt / CoffeeScript, vous augmentez considérablement votre productivité.

Nous verrons comment utiliser ce projet dans un tutoriel de création d'une eboutique.

Installation de Django REST Framework

Comme il s'agit d'une librarie python, nous pouvons passer par pip pour l'installation:

pip install djangorestframework
pip install markdown
pip install django-filter  

Création de notre app avec Django

Nous pouvons ensuite créer notre projet (qu'importe l'emplacement)

django-admin startproject eboutique
django-admin startapp erp

Adoptons notre connection à la base de données

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', 
        'NAME': 'eboutique',
        'USER': 'user',
        'PASSWORD': 'MOTDEPASSE',
        'HOST': '127.0.0.1',                     
        'PORT': '',
    }
}

Et renseignons nos applciations

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'erp'
)

Puis créeons nos modèles

class Product(models.Model):

    date_add = models.DateTimeField(auto_now_add=True)
    name     = models.CharField(max_length=255)
    code     = models.CharField(max_length=100, null=True)
    price    = models.FloatField()
    supplier = models.ForeignKey('Supplier', null=True)
    image    = models.ImageField(upload_to='product')

    def __unicode__(self):
        return "{0}".format(self.code, )

class ProductItem(models.Model):

    product = models.ForeignKey('Product', related_name="product_item")
    code    = models.CharField(max_length=255)
    ean13   = models.CharField(max_length=255)

    def __unicode__(self):
        return "{0}".format(self.code, )

class Supplier(models.Model):

    name = models.CharField(max_length=255)
 
    def __unicode__(self):
        return "{0}".format(self.name, )

Création du projet Django Rest Framework

Maintenant nous allons créer des fichiers que nous implémenterons directement dans le module eboutique. Vous pouvez évidemment créer un module consacré à l'API pour mieux organiser votre projet.

# eboutique/views.py

from rest_framework import viewsets
from erp.models import *
from eboutique.serializers import *

class ProductViewSet(viewsets.ModelViewSet):

    queryset = Product.objects.all()
    serializer_class = ProductSerializer


class SupplierViewSet(viewsets.ModelViewSet):

    queryset = Supplier.objects.all()
    serializer_class = SupplierSerializer
# eboutique/serializers.py

from rest_framework import serializers
from erp.models import *

class ProductSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Product
        fields = ('date_add', 'name', 'code', 'price')


class SupplierSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Supplier
        fields = ('url', 'name')
# eboutique/urls.py

from django.conf.urls import patterns, include, url
from django.contrib import admin
from rest_framework import routers
from eboutique.views import *

router = routers.DefaultRouter()
router.register(r'product', ProductViewSet)
router.register(r'supplier', SupplierViewSet)


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'^api/', include(router.urls)),
  #   url(r'^api/', include('rest_framework.urls', namespace='rest_framework'))
)

Créeons maintenant le schéma de notre projet:

python manage.py migrate

Et lançons le serveur web

python manage.py runserver 8888

Vous devriez obtenir le résultat suivant via l'url http://localhost:8888/api

Django rest Framework

Si vous cliquez sur le lien permettant d'éditer les produits, vous pourrez ajouter un item:

Django rest Framework

Il peut être intéressant dans votre environnement de dev de créer un petit script qui initialise tout votre projet à votre guise. Un exemple de script est disponible ici: initialiser Projet Django . Vous pouvez l'ajouter à Grunt pour facilier son exécution.

Authentification et permission

Alors oui c'est magique en deux trois mouvements nous avons crée une application web API REST. Mais ce n'est pas encore parfait: il manque encore l'aspect sécurité c'est à dire la permissions d'acceder à certaines ressources (ou non).

Pour cela nous allons ajouter la configuation REST_FRAMEWORK dans notre fichier settings.py:

# eboutique/settings.py

REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
    'PAGE_SIZE': 100,

    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',

}

Si nous voulons maintenant accéder à la liste des produits: http://127.0.0.1:8888/api/product/, nous recevons le message suivant:

{"detail":"Informations d'authentification non fournies."}

On nous refuse l'accès parce que je ne suis pas identifié!

Pour vous créer un système de login, je vous conseille de lire ce chapitre: Login Django

Personnaliser les vues

Dans l'exemple ci-dessous, nous verrons comment modifier la queryset et l'optimiser avec prefetch_related. Nous verrons également comment modifier l'objet de sortie et y ajouter au dernier moment des manipulations de données.

class ProductViewSet(viewsets.ModelViewSet):

  queryset = Product.objects.all()
  serializer_class = ProductSerializer
  filter_class = ProductFilter


  def get_serializer_class(self):
    if self.request.method == 'GET':
        return ReadProductSerializer
    else:
        return self.serializer_class


  def list(self, request, *args, **kwargs):

    queryset = self.filter_queryset(self.get_queryset())

    if 'search' in request.QUERY_PARAMS:
      search = request.QUERY_PARAMS['search']
      q = Q()
      for term in search.split(' '):
          if term  "":
              q = q & Q(name__icontains=term)
      q = q | Q(name__icontains=search)
      # On filtre avec l'objet Q
      queryset = queryset.filter(q)

      if search == "":
          queryset = queryset.none()

    queryset = queryset.prefetch_related("product_item")
    queryset = queryset.prefetch_related("product_item__product_item_stock")
    queryset = queryset.prefetch_related("product_item__product_item_stock__warehouse")
    queryset = queryset.prefetch_related("product_item__attribute")

    page = self.paginate_queryset(queryset)
    if page is not None:
      serializer = self.get_serializer(page, many=True)
      return self.get_paginated_response(serializer.data)

    serializer = self.get_serializer(queryset, many=True)
    return Response(serializer.data)


  def get_paginated_response(self, data):
    
    for product in data:
      product['product_item']["test"] = 1

    assert self.paginator is not None
    return self.paginator.get_paginated_response(data)

Personnaliser les serializers

Vous pouvez ajouter l'option depth pour afficher les détails d'un item directement dans l'objet parent sérializé

class ReadProductSerializer(serializers.ModelSerializer):

  product_item = ReadProductItemForProductSerializer(many=True, required=False)

  class Meta:
    model = Product
    fields = ('name', 'code', 'final_price', 'product_item', 'date_add')
    depth = 2

Vous pouvez également intérgir lors de l'envoi d'une requète CRUD telle que POST ou UPDATE

# TicketItem
class TicketItemSerializer(serializers.ModelSerializer):

  class Meta:
    model = TicketItem
    fields = ('id', 'name', 'quantity', 'remise', 'price_unit', 'product_item')
    extra_kwargs = {"id": {"required": False, "read_only": False}}



# Ticket
class TicketSerializer(ReadTicketSerializer):

  ticket_item = TicketItemSerializer(many=True, required=False)

  class Meta:
    model = Ticket
    fields = ('id', 'date_add', 'customer', 'price_total', 'ticket_item')

  def create(self, validated_data):

    model = getattr(self.Meta, 'model')
    ticket_item_data = validated_data.pop('ticket_item')
    instance = model.objects.create(**validated_data)

    for item in ticket_item_data:
        TicketItem.objects.create(ticket=instance, **item)
    return instance

  def update(self, instance, validated_data):

    ticket_item_data = validated_data.pop('ticket_item')
    ids = [dict(item)['id'] for item in ticket_item_data]
    for i in instance.ticket_item.all():
      if i.id not in ids:
        i.delete()

    for item in ticket_item_data:
      TicketItem.objects.filter(id=dict(item)['id']).update(**item)

    return instance

AngualarJS et DRF

Nous avons vu précedemment comment retourner des données très facilement au niveau du développement backend. Pour les développeur frontend c'est encore plus simple avec le module resource de AngularJS.

Intégrons le module ngResource à notre projet:

app = angular.module 'app', ['ngRoute', 'ngResource', 'ngSanitize']

Configurons notre API:

app.service 'Entity', ($resource) ->

  @product = () ->
    return $resource '/api/products/:id/' , {id: '@id'}, 
    {
      query: { method: 'GET', isArray: false },
      search: { method: 'GET', isArray: false  },
      save: { method: 'POST', headers: { 'Content-Type': 'application/json' }, },
      show: { method: 'GET' },
      update: { method: 'PUT', params: {id: '@id'} },
      delete: { method: 'DELETE', params: {id: '@id'} }
    }

  @productitems = () ->
    return $resource '/api/productitems/:id/' , {id: '@id'}, 
    {
      query: { method: 'GET', isArray: false },
      search: { method: 'GET', isArray: false  },
      save: { method: 'POST', headers: { 'Content-Type': 'application/json' }, },
      show: { method: 'GET' },
      update: { method: 'PUT', params: {id: '@id'} },
      delete: { method: 'DELETE', params: {id: '@id'} }
    }

  true

Notre controlleur AngularJS

class DashboardController

  @$inject: ['$scope', 'Entity']

  constructor: (@scope, Entity) ->
    @Entity = Entity
    @scope.products = []
    
    @scope.add_product = @add_product
    @scope.upd_product = @upd_product
    @scope.del_product = @del_product

    @get_products()
    true


  add_product : () =>
    item = new (@Entity.product())()
    item.name = "Iphone 6"
    item.price = 699.99
    item.$save().then (r) =>
      console.log @scope.products
      @scope.products.push r
    true

  upd_product : ($index) =>
    product = @scope.products[$index]
    item = @Entity.product().get({'id': product.id}, (item) => 
        item.name = product.name
        item.$update()
        true
      )
    true

  get_products: () =>
    @Entity.product().query({search: "test"}).$promise.then (r) =>
      @scope.products = r.results
      true
    true

  del_product: ($index) =>
    product = @scope.products[$index]
    @Entity.product().delete({'id': product.id}).$promise.then (r) =>
      @scope.products.splice($index, 1)
      true
    true

app.controller 'DashboardController', DashboardController  

Et enfin notre template:

<ul>
  <li ng-repeat="product in products">
      <input ng-model="product.name" ng-change="upd_product($index)" /> 
      <button ng-click="del_product($index)">X</button>
  </li>
</ul>
<button ng-click="add_product()">Ajouter un produit</button>