Using Django as Ember.js back-end

Ember.js is moving a lot, but since the adoption of JSONAPI for the default "wire protocol" with back-ends it's easier to keep up.

I have had good luck using the Django Rest Framework JSON API library on top of Django Rest Framework.

This post will try to document how to get things working and forget about your back-end for a while. I'm going to follow the Django Tutorial schema, with an important addition of adding a related_name to the ForeignKey field.

from django.db import models


class Question(models.Model):  
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):  
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="choices")
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Do the usual makemigrations and migrate then also register the models in the Admin.

Install Django Rest Framework and the aforementioned JSON API adapter. Also install django-filter. In the settings.py, make sure you add rest_framework.

pip install django-rest-framework  
pip install git+https://github.com/django-json-api/django-rest-framework-json-api.git@develop --upgrade # FIXME once 2.0 is properly released it should be enough  
pip install django-filter  

Now configure the DRF like so:

REST_FRAMEWORK = {  
    #'PAGE_SIZE': 10,
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
# FIXME do permission and authentication as you see fit.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        #'rest_framework.authentication.SessionAuthentication',
    ],
    'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
    'DEFAULT_PAGINATION_CLASS':
        'rest_framework_json_api.pagination.PageNumberPagination',
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework_json_api.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ),
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework_json_api.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ),
    'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',

# this is optional but very useful
    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),

}


APPEND_SLASH=False  
JSON_API_FORMAT_KEYS = 'dasherize'  
JSON_API_FORMAT_RELATION_KEYS = 'dasherize'  
JSON_API_PLURALIZE_RELATION_TYPE = True

Now create a new rest.py file:

from .models import Question, Choice  
from rest_framework_json_api import serializers, relations  
from rest_framework import viewsets, views, response

import django_filters  
from rest_framework import filters

class QuestionSerializer(serializers.ModelSerializer):  
    choices = relations.ResourceRelatedField(read_only=True, many=True)
    class Meta:
        model = Question

class ChoiceSerializer(serializers.ModelSerializer):  
    class Meta:
        model = Choice


class QuestionViewSet(viewsets.ModelViewSet):  
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

class ChoiceViewSet(viewsets.ModelViewSet):  
    queryset = Choice.objects.all()
    serializer_class = ChoiceSerializer

# this is should plural and dasherized names
ROUTES = {  
    'questions': QuestionViewSet,
    'choices': ChoiceViewSet
}

Then in the global urls.py:

from rest_framework import routers  
router = routers.DefaultRouter(trailing_slash=False)  
from polls.rest import ROUTES  
for key, viewset in ROUTES.items():  
    router.register(key, viewset)

urlpatterns = [  
    url(r'^admin/', admin.site.urls),
    url(r'^api/v1/', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

The only Ember change you have to do is generate an application adapter with this content:

import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({  
    namespace: 'api/v1',
    isInvalid(status, headers, payload) {
        return status === 422 || status === 400;
    },
});

Now run the Django dev server, and don't forget to use ember serve --proxy http://127.0.0.1:8000 to ensure your Ember app can easily make Ajax requests there.