Toly blog

Python and Django notes

Проект на Django Rest Framework и AngularJS

Перевод статьи Кевина Стоуна Getting Started with Django Rest Framework and AngularJS

RESTful API становится стандартным компонентом любого современного веб-приложения. Django Rest Framework является мощным фреймворком для разработки REST API на основе вашего Django проекта. AngularJS - современный JavaScript фреймворк для создания сложных клиентских веб-приложений. Он фокусируется на сильном разделении функциональных частей (MVC) и использовании зависимостей для поощрения создания поддерживаемых (и тестируемых) модулей, которые будучи интегрированными предоставляли богатую функциональность на стороне клиента.

В этом посте я покажу создание проекта для примера, который преоставляет REST API, используемый фреймворком AngularJS на клиенте, что бы продемонстрировать как совместно использовать бекенд и фронтенд для упрощения создания сложных приложений.

Давайте сделаем тестовый Django проект

Для примера, давайте создадим простое приложение для обмена фотографиями (что-то типа примитивного Instagram) и выдачи ленты для отдельного пользователя, что бы смотреть фотографии размещенные на сайте.

Все примеры кода для этого проекта доступны в репозитории на github. Что бы установить тестовый проект локально, ознакомьтесь с инструкцией по установке, включенной в репозиторий. Там же описана установка AngularJS (и других javascript библиотек) через bower и grunt.

Наконец, есть тестовые данные, доступные в виде фикстур, для демонстрации API. У нас есть несколько пользователей ([‘Bob’, ‘Sally’, ‘Joe’, ‘Rachel’]), два поста ([‘This is a great post’, ‘Another thing I wanted to share’]) и также несколько тестовых фотографий. Прилагаемый Makefile строит для вас тестовые данные.

Несколько замечаний о примерах кода:

  • Я пропущу подробности конфигурирования, сборки и запуска приложения. В инструкции, размещенной в репозитории, описаны многие из этих деталей. Если что - сообщайте о любых приблемах на github.
  • Я написал код клиентской части на Coffee-Script, так как считаю его более понятным и более эффективным (и немного питоничным). В репозитории находится Grun-файл, который собирает все Coffee-Script файлы объединяет их в один файл - script.js для использования на клиенте.

Уровень данных проекта (модели)

Наша модель данных такая же простая, как и в водном уроке для Django. У вас есть три модели: User (пользователь), Post (пост) и Photo (фотография). Пользователь может быть автором множества постов (а также иметь много фолловеров), пост может содержать много фотографий (что-то вроде альбома или галлереи) вместе с названием и необязательным описанием.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models

from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    followers = models.ManyToManyField('self', related_name='followees', symmetrical=False)


class Post(models.Model):
    author = models.ForeignKey(User, related_name='posts')
    title = models.CharField(max_length=255)
    body = models.TextField(blank=True, null=True)


class Photo(models.Model):
    post = models.ForeignKey(Post, related_name='photos')
    image = models.ImageField(upload_to="%Y/%m/%d")

API на Django Rest Framework

Django Rest Framework предоставляет готовую архитектуру для разработки как простых RESTful API, так и более сложных конструкций. Его ключевая особенность, это четкое разделение на сериализаторы, которые описывают соответствие между моделью и ее форматом представления (будь то JSON, XML или любой другой формат), и на отдельный набор универсальных представлениях на основе классов (Class-Based-Views), которые могут быть по необходимости расширены. Вы так же можете определить свою ссылочную структуру, вместо использования дефолтной. Это то, что отличает Django Rest Framework от других фреймворков, таких как Tastypie и Piston, которые автоматизируют формировнаие API на основе моделей, но это происходит за счет снижения гибкости и применимости к различным нестандартным требованиям (особенно, если речь идет о доступах и вложенных ресурсах).

Сериализаторы моделей

Сериализаторы в Django Rest Framework предназначены для преобразования экземпляров django-модели в API представление. Это дает нам возможность конвертировать любые типы данных, или предоставлять дополнительную информацию о данной модели. Например, для пользователя, мы только откроем некоторые поля, скрывая некоторые аттрибуты, такие как пароль и адрес електронной почты. Для фото, мы сконвертируем ImageField так, что бы возвращалась ссылка на изображение (а не путь во внутреннем медиа каталоге).

Для сериализатора PostSerializer, мы выбрали вставку данных автора прямо в пост (до этого в поле author была ссылка на данные автора). Это делает информацию доступнее для клиентской части, и не требует дополнительных запросов к API для дублирующихся авторов каждого поста. Альтернативный вариант сс сылкой на автора предоставлен закомментированной строкой для сравнения. Сильной стороной сериализаторов является то, что их можно расширить для создания дополнительных версий, которые используют ссылки на данные автора вместо вложенных данных (например, для вывода списка постов одного пользователя).

Что бы привязать данные автора к PostSerializer, нам придется сделать так, что бы эти данные поддерживались API представлением. Поэтому мы сделаем поле автора необязательным (required=False) в сериализаторе поста и добавим его в исключения валидации.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from rest_framework import serializers

from .models import User, Post, Photo


class UserSerializer(serializers.ModelSerializer):
    posts = serializers.HyperlinkedIdentityField('posts', view_name='userpost-list', lookup_field='username')

    class Meta:
        model = User
        fields = ('id', 'username', 'first_name', 'last_name', 'posts', )


class PostSerializer(serializers.ModelSerializer):
    author = UserSerializer(required=False)
    photos = serializers.HyperlinkedIdentityField('photos', view_name='postphoto-list')
    # author = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='username')

    def get_validation_exclusions(self):
        # Need to exclude `author` since we'll add that later based off the request
        exclusions = super(PostSerializer, self).get_validation_exclusions()
        return exclusions + ['author']

    class Meta:
        model = Post


class PhotoSerializer(serializers.ModelSerializer):
    image = serializers.Field('image.url')

    class Meta:
        model = Photo

Хорошо, загрузив тестовые данные, давайте попробуем поработать с этими сериализаторами. Возможно вы увидите предупреджающие сообщения DeprecationWarning из-за использования HyperlinkedIdentityField без предоставления объекта запроса для построения ссылки. В настоящих представлениях объект запроса присутствует, так что вы можете спокойно игнорировать эти сообщения.

1
2
3
4
5
6
7
8
9
10
11
12
>>> from example.api.models import User
>>> user = User.objects.get(username='bob')
>>> from example.api.serializers import *
>>> serializer = UserSerializer(user)
>>> serializer.data
{'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}
>>> post = user.posts.all()[0]
>>> PostSerializer(post).data
{'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}, 'photos': '/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}
>>> serializer = PostSerializer(user.posts.all(), many=True)
>>> serializer.data
[{'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}, 'photos': '/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}]

Ссылочная структура API

Для нашего API, мы хотим использовать относительно плоскую структуру, что бы попытаться определить типичные сущности для данного API, но так же и обеспечить некоторые вспомогательные вложенные данные для фильтрации (например, посты определенного пользователя или фотографии определенного поста). Обратите внимание, что мы используем первичные ключи (primary key) моделей в качестве идентификатора, но для пользователей мы используем его имя, так как он так же является уникальным (позже мы увидим это в предствалениях).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.conf.urls import patterns, url, include

from .api import UserList, UserDetail
from .api import PostList, PostDetail, UserPostList
from .api import PhotoList, PhotoDetail, PostPhotoList

user_urls = patterns('',
    url(r'^/(?P<username>[0-9a-zA-Z_-]+)/posts$', UserPostList.as_view(), name='userpost-list'),
    url(r'^/(?P<username>[0-9a-zA-Z_-]+)$', UserDetail.as_view(), name='user-detail'),
    url(r'^$', UserList.as_view(), name='user-list')
)

post_urls = patterns('',
    url(r'^/(?P<pk>\d+)/photos$', PostPhotoList.as_view(), name='postphoto-list'),
    url(r'^/(?P<pk>\d+)$', PostDetail.as_view(), name='post-detail'),
    url(r'^$', PostList.as_view(), name='post-list')
)

photo_urls = patterns('',
    url(r'^/(?P<pk>\d+)$', PhotoDetail.as_view(), name='photo-detail'),
    url(r'^$', PhotoList.as_view(), name='photo-list')
)

urlpatterns = patterns('',
    url(r'^users', include(user_urls)),
    url(r'^posts', include(post_urls)),
    url(r'^photos', include(photo_urls)),
)

Представления API

Сильной стороной Django Rest Framework является набор базовых представлений, которые позволяют легко решать распространенные задачи связанные с CRUD практически без изменений. Для простейших представлений, вы предоставляете model и serializer_class, и расширяете одним из встроенных представлений (таким как ListAPIView или RetrieveAPIView).

В нашем случае мы имее пару настроек. Во-первых, для пользователей в качестве поля поиска мы будем использовать поле username, вместо pk. Поэтому мы устанавливаем поле lookup_field.

Мы так же хотели создать вложенные данные представлений для постов пользователей и фотографий поста. Для этого просто переопределим метод представления get_queryset так, что бы он фильтровал результаты по вложенным параметрам (username и pk, соответственно).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from rest_framework import generics, permissions


from .serializers import UserSerializer, PostSerializer, PhotoSerializer
from .models import User, Post, Photo


class UserList(generics.ListCreateAPIView):
    model = User
    serializer_class = UserSerializer
    permission_classes = [
        permissions.AllowAny
    ]

class UserDetail(generics.RetrieveAPIView):
    model = User
    serializer_class = UserSerializer
    lookup_field = 'username'

class PostList(generics.ListCreateAPIView):
    model = Post
    serializer_class = PostSerializer
    permission_classes = [
        permissions.AllowAny
    ]

class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    model = Post
    serializer_class = PostSerializer
    permission_classes = [
        permissions.AllowAny
    ]

class UserPostList(generics.ListAPIView):
    model = Post
    serializer_class = PostSerializer

    def get_queryset(self):
        queryset = super(UserPostList, self).get_queryset()
        return queryset.filter(author__username=self.kwargs.get('username'))

class PhotoList(generics.ListCreateAPIView):
    model = Photo
    serializer_class = PhotoSerializer
    permission_classes = [
        permissions.AllowAny
    ]

class PhotoDetail(generics.RetrieveUpdateDestroyAPIView):
    model = Photo
    serializer_class = PhotoSerializer
    permission_classes = [
        permissions.AllowAny
    ]

class PostPhotoList(generics.ListAPIView):
    model = Photo
    serializer_class = PhotoSerializer

    def get_queryset(self):
        queryset = super(PostPhotoList, self).get_queryset()
        return queryset.filter(post__pk=self.kwargs.get('pk'))

Лирическое отступление: встроенный браузер API

Одним из преимуществ Django Rest Framework является то, что он поставляется со встроенным браузером API для тестирования вашего API. Очень похожий на Django админку, он может помочь в начале разработки.

Просто загрузив одну из ваших API сущностей в браузер, Django Rest Framework предоставит вам удобный клиентсткий интерфейс для взаимодействия с API.

Добавление прав доступа и принадлежности в API

Как написано выше, наши представления API позволяют любому создавать что-либо на нашем сайте. Одним из преимуществ использования Django Rest Framework является наличие типичных представлений, делающих простым управление доступом в представлении без влияния на основную модель и сериализаторы. Что бы включить контроль над тем, кому разрешено редактировать наши API ресурсы, мы можем создать несколько классов, которые обеспечивают управление авторизацией для ограничения доступа. Они должны возвращать логическое значение для каждого запроса. Это дает нам доступ к полному запросу, в том числе куки, аутентифицированного пользователя и многое другое.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from rest_framework import permissions


class SafeMethodsOnlyPermission(permissions.BasePermission):
    """Only can access non-destructive methods (like GET and HEAD)"""
    def has_permission(self, request, view):
        return self.has_object_permission(request, view)

    def has_object_permission(self, request, view, obj=None):
        return request.method in permissions.SAFE_METHODS


class PostAuthorCanEditPermission(SafeMethodsOnlyPermission):
    """Allow everyone to list or view, but only the other can modify existing instances"""
    def has_object_permission(self, request, view, obj=None):
        if obj is None:
            # Either a list or a create, so no author
            can_edit = True
        else:
            can_edit = request.user == obj.author
        return can_edit or super(PostAuthorCanEditPermission, self).has_object_permission(request, view, obj)

Кроме использования элементарной авторизации, мы также хотим, что бы предварительно заполнялись значения при сохранении в зависимости от того, кто делает запрос. Когда кто-то создает новый пост, мы хотим, что бы он автоматически назначался автором этого поста. Что бы устранить дублирование между PostList и PostDetail мы можем создать миксин, который будет содержать общую конфигурацию.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PostMixin(object):
    model = Post
    serializer_class = PostSerializer
    permission_classes = [
        PostAuthorCanEditPermission
    ]

    def pre_save(self, obj):
        """Force author to the current user on save"""
        obj.author = self.request.user
        return super(PostMixin, self).pre_save(obj)


class PostList(PostMixin, generics.ListCreateAPIView):
    pass


class PostDetail(PostMixin, generics.RetrieveUpdateDestroyAPIView):
    pass

Использование API через AngularJS

С появлением более интерактивных веб-приложений, RESTful API могут быть использованы в сложных клиентских интерфейсах, что бы получать и взаимодействовать с моделью данных приложения. AngularJS может здорово помочь в этом, из-за его сильного разделения элементов управления. Модульная архитектура AngularJS является настраиваемой. Ваше приложение состоит из модулей, которые определяют сервисы, директивы и контроллеры, и таким образом функционал оказывается разложенным по полочкам.

Преимуществом AngularJS является то, что он предоставляет возможность реактивного программирования используя свой язык JavaScript-подобных выражений. Мы можем просто определить шаблон, который ссылается на переменные и наша страница будет автоматически обновлена при изменении этих переменных.

Для простейшего примера, мы просто выведем список постов в нашем приложении. Ниже приведен минимальный шаблон для AngularJS. Во-первых, на самом верхнем теге (body) мы укажем, какое приложение Angular используется для запуска этой страницы (example.app.basic), которое мы определим как корневой модуль. Во-вторых, нам нужен специальный контроллер, который будет управлять нашим шаблоном (AppController). Контроллер, в терминологии AbgularJS, больше соответствует связке модели и контроллера в традиционном MVC (c объектом $scope, содержащим состояние модели). Контроллеры определяют области видимости, которые содержат экземпляры модели, и они могут быть вложенными, чтобы ограничить область видимости по мере спускания по дереву DOM. И, наконец, мы используем директиву Angular (ng-repeat), которая является управляющей структурой для перебора наших моделей постов, хранящихся в $state. Внутри блока этой итерации, мы определяем несколько тегов и используем Angular-выражения (похожие на шаблонные теги Django), чтобы вывести имя пользователя автора, заголовок и содержание поста.

Используйте тег verbatim для включения Angular-выражений, что бы Django не пытался их отрендерить.

Я заключил некоторые части шаблона в джанговсий тег {% block %}, чтобы потом наследовать этот шаблон для каждого из примеров.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% load staticfiles %}

<html>
<head>
<link rel="stylesheet" type="text/css" href="{% static "bootstrap/dist/css/bootstrap.css" %}">
</head>
<body ng-app="{% block ng_app %}example.app.static{% endblock %}">
<div class="content" ng-controller="{% block ng_controller %}AppController{% endblock %}">{% block content %}
{% verbatim %}
    <div class="panel" ng-repeat="post in posts">
        <div class="panel-heading clearfix">
            <h3 class="panel-title">{{ post.title }}</h3>
            <author class="pull-right">{{ post.author.username }}</author>
        </div>
        <p class="well">{{ post.body }}</p>
    </div>
{% endverbatim %}
{% endblock %}</div>
<script src="{% static "underscore/underscore.js" %}"></script>
<script src="{% static "angular/angular.js" %}"></script>
<script src="{% static "angular-resource/angular-resource.js" %}"></script>
<script src="{% static "js/script.js" %}"></script>
</body>
</html>

Теперь, давайте дополним шаблон простым контроллером, который будет возвращать список постов. Сейчас мы жестко пропишем посты, а потом будем получать их через AJAX.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app = angular.module 'example.app.static', []

app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->
    $scope.posts = [
        author:
            username: 'Joe'
        title: 'Sample Post #1'
        body: 'This is the first sample post'
    ,
        author:
            username: 'Karen'
        title: 'Sample Post #2'
        body: 'This is another sample post'
    ]
]

Если вы установили все зависимости и запустили grunt, что бы скомпилить CoffeScript в JavaScript, то вы должны увидеть наш шаблон с прописанными в коде данными вроде этого:

Используем XHR для получения постов из API

Теперь, давайте продолжим и обновим контроллер, чтобы получать список постов из нашего API. Сервис $http в AngularJS похож на $.ajax из jQuery или иную реализацию XHR. Обратите внимание, что с AngularJS мы просто обновили нашу модель ($scope.posts) результатом AJAX-запроса и наше представление автоматически обновляется по завершении AJAX-запроса. Нет необходимости изменять DOM. Такой реактивный подход позволяет нам разрабатывать сложные пользовательские интерфейсы с использованием взаимозависимостей данных нашей модели и компонентами пользовательского интерфейса, которые реагируют соответствующим образом без необходимости прямого управления этими связями, что позволяет сделать представления и модель данных слабосвязанными между собой.

1
2
3
4
5
6
7
8
app = angular.module 'example.app.basic', []

app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->
    $scope.posts = []
    $http.get('/api/posts').then (result) ->
        angular.forEach result.data, (item) ->
            $scope.posts.push item
]

С сервисом $http, получающим список постов из нашего API, наша тестовая страница покажет список постов прямо с сервера.

Используем Angular-Resource для API

Хотя $http и позволяет с помощью XHR-запросов получать данные API для нашего приложения, это вынуждает нас дублировать код с учетом деталей нашего API, в том числе формирование URL’ов, осуществление запросов и другие вещи, которые можно реализовать на более высоком уровне абстракции. Используйте Angular-Resource, который предоставляет механизм для определения вашего API через сервисы Angular, управляющие большей частью низкоуровневых процессов, тем самым упрощая взаимодействие с API.

Для работы с Angular-Resource (ngResource), вы просто определяете связь между сущностями вашего API и параметрами в шаблоне URL-адреса (так же как и джанговсие urlpatterns). К сожелению, не так уж просто связать Django и определения ngResource, поэтому прицип DRY здесь неприменим.

При определении ваших ресурсов (используя $resource), вы просто предоставляете шаблон URL-адреса, список фигурируемых параметров по умолчанию и, при необходимости, некоторые HTTP методы. В нашем случае, нам нужен ресурс для модели User, который бы связывал имя пользователя (:username) в качестве параметра в URL-запросе и поле username из экземпляра ресурса. Сущности Post и Photo используют поле id экземпляра, т.к. оно является первичным ключом (идентификатором).

1
2
3
4
5
6
7
8
9
10
11
12
13
app = angular.module 'example.api', ['ngResource']

app.factory 'User', ['$resource', ($resource) ->
    $resource '/api/users/:username', username: '@username'
]

app.factory 'Post', ['$resource', ($resource) ->
    $resource '/api/posts/:id', id: '@id'
]

app.factory 'Photo', ['$resource', ($resource) ->
    $resource '/api/photos/:id', id: '@id'
]

Теперь, когда мы определили наш модуль с API, мы можем использовать его в качестве зависимости в нашем модуле контроллера и применить его как сервис, который наш контроллер сможет использовать для доступа к API. Добавим example.api как зависимый модуль и список любых ресурсов API в качестве зависимостей в нашем поределении контроллера. По умолчанию, ваши ресурсы имеют множество основных CRUD методов, включая query() (для получения набора/списка объектов), get() (для получения отдельных объектов), save(), delete() и другие.

1
2
3
4
5
app = angular.module 'example.app.resource', ['example.api']

app.controller 'AppController', ['$scope', 'Post', ($scope, Post) ->
    $scope.posts = Post.query()
]

В результате получим такой же список постов, как и в примере выше.

Добавляем фотографии в список постов

Теперь у нас есть список постов, показываемый с помощью вызовов ngResource API, но мы выполняем только получение данных. В реальных приложениях, ваши данные редко хранятся лишь в одной сущности API и может потребоваться несколько скоординированных запросов для построения актуальных данных модели. Давайте улучшим наше приложения, чтобы получать также фоторгафии каждого поста и выводить их.

Сначала давайте добавим два дополнительных ресурсы для вложенных вызовов API:

1
2
3
4
5
6
7
app.factory 'UserPost', ['$resource', ($resource) ->
    $resource '/api/users/:username/posts/:id'
]

app.factory 'PostPhoto', ['$resource', ($resource) ->
    $resource '/api/posts/:post_id/photos/:id'
]

Это создаст два дополнительных сервиса (UserPost и UserPhoto) которые мы можем использовать для получения ресурсов связанных с определенным пользователем и определенным постом соответственно. Так как это вложенные ресурсы, нам нужно что бы они загружались после основного ресурса (другой вариант заключается в использовании механизма $watch в Angular в ответ на изменения и выполнять дополнительные запросы к API). Для этого мы будем использовать сервис $q в Angular, который обеспечивает обещано-отложенную реализацию, позволяющую создавать цепочки вызовов. Начиная с версии 1.1 ngResource предоставляет аттрибут $promise, который вы можете применить для создания цепочки вызовов. Мы используем этот интерфейс, что бы после запроса к API следовало получение фотографий для полученного поста.

У вас есть несколько вариантов, как работать с вложенными ресурсами. Для этого случая, мы просто создадим еще один контейнер в $scope для фотографий и используем post_id в качестве идентификатора. Выражения в Angular и в его языке шаблонов игорируют отсутствующие ключи, поэтому мы просто проитерируем photos[post.id] чтобы получить фотографии в шаблоне. Обратите внимание, что нам не нужно что-либо делать, чтобы обновить представление или шаблон. Ангуляровский процесс $digest самостоятельно обноруживает обновления.

1
2
3
4
5
6
7
8
9
10
app = angular.module 'example.app.photos', ['example.api']

app.controller 'AppController', ['$scope', 'Post', 'PostPhoto', ($scope, Post, PostPhoto) ->
    $scope.photos = {}
    $scope.posts = Post.query()
    $scope.posts.$promise.then (results) ->
        # Load the photos
        angular.forEach results, (post) ->
            $scope.photos[post.id] = PostPhoto.query(post_id: post.id)
]

Также обновим наш шаблон чтобы итерировать каждую модель поста для отображения фотографий каждого поста. Обратите внимание, как AngularJS повторно обновляет представление по мере того, как данные загружаются из API. В этом случае, мы итерируем фотографии объекта, ссылающиеся на идентификаторы постов, которые в свою очередь итерируются директивой ng-repeat. Мы также используем директиву ng-src вместо аттрибута src, так как это не позволяет браузеру загружать картинку до того как выражение Angular было прекомпилировано (тогда бы вы увидели ошибку 404 в ваших логах о невозможности показа ‘/media/{{ photo.image }}’).

1
2
3
4
5
6
7
8
9
10
<div class="panel" ng-repeat="post in posts">
    <div class="panel-heading clearfix">
        <h3 class="pull-left panel-title">{{ post.title }}</h3>
        <author class="pull-right">{{ post.author.username }}</author>
    </div>
    <p class="well">{{ post.body }}</p>
    <span class="photo" ng-repeat="photo in photos[post.id]">
        <img class="img-thumbnail" ng-src="{{ photo.image }}">
    </div>
</div>

И в итоге, обновив страницу http://localhost:8000/photos, получим отрендеренные фотографии:

Лирическое отступление: AngularJS и защита от CSRF

Django Rest Framework расширяет джанговскую защиту от CSRF (Cross Site Request Forgery) используя класс SessionAuthentication (как например в нашем случае: используется таже сессия браузера, что и для веб-приложения). Это способствует тому, что вредоносные сценарии не смогут использовать наших пользователей для выполнения различных запросов к нашему API, заставляя сценарии при каждом запросе отправлять генерируемый сервером токен. Модульная архитектура AngularJS и внедрение зависимостей позволяет сделать это простым конфигурированием запросов к API с помощью включения CSRF-токена в заголовок запроса (по желанию, можно также использовать куки).

В нашем джанговском шаблоне, просто добавьте тег <script> для конфигурирования сервиса $http и настройте джанговское приложение, что бы шаблонная переменная {{ csrf_token }} передавалась в заголовок ответа для всех вызовов API.

Будьте уверены, что этот скрипт загружается после определений модуля.

Вы можете передавать CSRF токен между Angular и Django через куки или другим способом. Этот явный механизм с использованием заголовков просто обеспечивает генерацию CSRF токена при каждом запросе.

1
2
3
4
5
6
7
<script>
// Add the CSRF Token
var app = angular.module('example.app'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for additional configuration
app.config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';
}]);
</script>

Создание и модификация ресурсов API с использованием AngularJS

Теперь давайте сделаем редактор в нашем представлении ленты для публикации новых постов (такой как обновление статуса в Фейсбуке). Хотя большинство руководств по Angular просто добавляют функциональность к уже существующему контроллеру, я хочу показать как сохранить ваши контроллеры компактными и модульными, поэтому для этого редактора постов мы сделаем отдельный контроллер и покажем, как контроллеры используют вложенные области видимости для расширения функциональности. Это также дает возможность расширить существующий можуль example.app.photos, реализующий основной контроллер AppController для ленты постов.

Сперва, нам нужно расшарить наш базовый шаблон, и добавить html шаблон для редактора. Мы также добавим CSRF токен, согласно приведенным выше инструкциям, так как для создания новых постов будут использоваться потенциально уязвимые методы.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends 'base.html' %}

{% block ng_app %}example.app.editor{% endblock %}

{% block content %}
{% verbatim %}
<div ng-controller="EditController">
    <h5>Create a New Post</h5>
    <form class="form-inline">
        <div class="form-group block-level">
            <input type="text" class="form-control" ng-model="newPost.title" placeholder="Title">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" ng-model="newPost.body" placeholder="Body">
        </div>
        <div class="form-group">
            <button class="btn btn-default" ng-click="save()">Add Post</button>
        </div>
    </form>
</div>
{% endverbatim %}
{{ block.super }}
{% endblock %}

{% block js %}
{{ block.super }}
<script>
// Add the CSRF Token
var app = angular.module('example.app.editor'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for additional configuration
app.config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';
}]);
</script>
{% endblock %}

Теперь, у нас есть редактор - давайте сделаем контроллер для его подключения. Заметьте, что теперь у нас в зависимостях два модуля: базовый модуль нашей ленты постов и модуль API, содержащий все определения ресурсов API ($resource).

1
2
3
4
5
6
7
8
9
10
11
12
app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']

app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->

    $scope.newPost = new Post()
    $scope.save = ->
        $scope.newPost.$save().then (result) ->
            $scope.posts.push result
        .then ->
            # Reset our editor to a new blank post
            $scope.newPost = new Post()
]

Ранее, в представлениях API, мы добавляли некоторые ограничения прав доступа, чтобы предотвратить изменения пользователями чужих постов. До сих пор это не имело значения, какое отношение имеет пользователь к редактируемому посту. Но теперь, когда мы хотим создать пользователей, мы должны гарантировать, что обрабатываем запрос от пользователя, который имеет право на этот запрос (иначе, запрос на создание нового поста должен быть отклонен). В этом учебном примере, мы использовали хак аутентификации Django, который автоматически авторизует вас как пользователя root. Разумеется, в продакшене или ином недоверенном окружении так делать нельзя. Это сделано для упрощения демонстрации тестового приложения, игнорируя регистрацию и авторизацию пользователя.

Обработка ошибок

Если вы тестировали получившееся приложение, пробовали ли вы создать пост без заголовка? Мы определили обязательные поля в модели Django, и Django Rest Framework будет проверять их наличие при создании нового поста. Если вы попытаетесь создать пост без заголовка (например, с помощью браузерного API или наше новой формы), вы получите 400-ю ошибку, с описанием причин ошибочности запроса. Давайте использовать этот ответ для информировнаия пользователя.

1
2
3
4
5
{
    "title": [
        "This field is required."
    ]
}

Для информирования пользователя, давайте изменим вызов API. Так как мы используем Promises, мы можем просто добавить коллбек-функцию, вызываемую при ошибке, чтобы получить информацию в ответе и уведомить пользователя, передав ответ в $scope, откуда шаблон сможет вывести ошибку для показа пользователю.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']

app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->

    $scope.newPost = new Post()
    $scope.save = ->
        $scope.newPost.$save().then (result) ->
            $scope.posts.push result
        .then ->
            # Reset our editor to a new blank post
            $scope.newPost = new Post()
        .then ->
            # Clear any errors
            $scope.errors = null
        , (rejection) ->
            $scope.errors = rejection.data
]

Также обновим шаблон, для вывода информации об ошибках:

1
<p ng-repeat="(name, errs) in errors" class="alert alert-danger"><strong>{{ name }}</strong>: {{ errs.join(', ') }}</p>

Подобным образом довольно просто к элементам пользовательского интерфейса добавить получение обратной связи о прогрессе (индиваторы загрузки, прогресс-бары) используя цепочечные вызовы. В AngularJS есть достаточно полная обработка ошибок через цепочки вызовов, делающая обработку ошибок простой.

В этом демонстрационном примере, мы просто перечислили ошибки и вывели их в специально оформленных элементах интерфейса. Так как ошибки имеют привязку к имени аттрибута, вы легко можете показать список ошибок возле того элемента формы, которому они соответсвуют.

Удаление своих постов

Чтобы редактор был полным, нам нужен способ удалять любые посты, но только те, которые добавлены текущим пользователем. У нас уже есть API, которое предотвращает попытки удаления/изменения ресурсов пользователями, которые не владеют ими. Если вы только начали изучать AngularJS, то вам сложно понять, как с помощью его модульной архитектуры обеспечить наличие начальных данных в контроллере. В нашем случае, зная текущего пользователя, можно определить какие посты доступны ему для удаления.

Ключ к пониманию этого заключается в попытке разложить функционал контроллеров на несколько служб, выполняющих определенную часть логики. Контроллер (тоже, что и представление в Django) должны только обеспечить слаженную работу различных компонентов. В Django, вы пытаетесь разместить как можно боьше бизнес-логики в модели (паттерн “толстые модели”). В AngularJS, вы похожим образом распределяете бизнес-логику в компонуемых службах.

Чтобы добавить возможность удалять посты, давайте сначала добавим модуль, расширяющий наш редактор, и добавим дополнительный контроллер для обрабтки удаления. В зависимостях нового контроллера будет служба AuthUser, которая через джанговский шаблон будет предоставлять текущего пользователя. В этом случае, служба будет содержать один аттрибут username, в котором будет имя текущего пользователя (или пустая строка, если пользователя нет). Мы добавили две функции в область видимости нового контроллера: canDelete, чтобы определить может ли текущий пользователь удалить пост, и delete, чтобы удалять пост. Обе функции принимают аргумент post, который передается туда через шаблон.

Опять же, мы использовали цепочечный вызов службы $resource, и только успешного подтверждения со стороны сервера об удалении поста, мы обновляем список постов. Так же как и было указано выше, есть возможность обработки ошибок выполнения запроса и обеспечения обратной связи с пользователем, но в нашем простом примере мы это пропустим.

1
2
3
4
5
6
7
8
9
10
11
12
13
app = angular.module 'example.app.manage', ['example.api', 'example.app.editor']

app.controller 'DeleteController', ['$scope', 'AuthUser', ($scope, AuthUser) ->
    $scope.canDelete = (post) ->
        return post.author.username == AuthUser.username

    $scope.delete = (post) ->
        post.$delete()
        .then ->
            # Remove it from the list on success
            idx = $scope.posts.indexOf(post)
            $scope.posts.splice(idx, 1)
]

После определения контроллера, давайте обновим наш шаблон поста и добавим кнопку удаления (при условии, что функция canDelete возвращает true).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% extends 'editor.html' %}

{% block ng_app %}example.app.manage{% endblock %}

{% block post_header %}

<button type="button" class="close" ng-controller="DeleteController" ng-click="delete(post)" ng-show="canDelete(post)">&times;</button>
{{ block.super }}
{% endblock %}

{% block js %}
{{ block.super }}
<script>
// Configure the current user
var app = angular.module('example.app.manage'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for 

app.factory('AuthUser', function() {
    return {
        username: "{{ user.username|default:''|escapejs }}"
    }
});
</script>
{% endblock %}

И когда вы загрузите http://localhost:8000/manage, возле постов с автором root будет кнопка ‘X’, а возле поста с автором bob - не будет. При нажатии на кнопку ‘X’ наш API удалит пост.

Итак, у нас есть простая лента, в которой пользователи могут размещать посты.

Заключение

Хорошо, давайте подумаем. Небольшим количеством кода (примерно 100 строк в интерфейсной части, и 200 строк в серверной), используя Django Rest Framework (ну и Django) и AngularJS, мы смогли быстро написать пример приложения для простой публикации постов. Django Rest Framework позволяет легко экспортировать данные наших джанговских моделей через RESTful API с возможностью настройки выводных данных в зависимости от наших нужд. AngularJS облегчает получение данных и взаимодейтсвие с API, причем делает это модульно и структурированно, что помогает нам добавлять новый функционал для наших приложений без спагетти-кода.

Весь код, упоминавшийся в этой статье, доступен на гитхабе. Я советую скачать этот репозиторий и установить рабочую копию проекта для экспериментов. Если заметите какие-либо ошибки - поставьте таск в гитхабе (или даже пулл-реквест). Если есть какие-либо вопросы - оставьте комментарий (или стукнитесь в твиттер @kevinastone). Я планируюю дополнить эту статью дополнительными случаями, с которыми мне приходилось сталкиваться, а именно:

  • пагинация
  • API переключатели (типа подписаться/отписаться)
  • более сложные права доступа
  • расширенная валидация
  • тестирование

Comments