Debian, Dojo, Django, Python

Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Показаны сообщения с ярлыком Framework. Показать все сообщения
Показаны сообщения с ярлыком Framework. Показать все сообщения

Несколько слов о Dojo Framework

Немного о Dojo Framework

Введение

Dojo Dramework - проект, ведущий свою историю с 2004 года. Причиной, побудившей авторов создать его, стала политика фирмы, где они тогда работали: Sencha хотела денег, и в общем-то, цели своей достигла, на их странице можно полюбоваться совершенно нескромными ценами на ExtJS, они же хотели сделать библиотеку бесплатной

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

Обзор возможностей

"Из коробки" Dojo содержит практически весь тот функционал, что мы можем найти в jQuery. Правда, вместо $() там используется dojo.query(), но суть не в этом - это лишь малая часть того, что Dojo на самом деле умеет.

Асинхронная подгрузка модулей

Последние 2 года я читаю о том, что в Angular 2 будет реализована асинхронная подгрузка модулей. В Dojo эта фича была реализована в 2009, но не позиционировалась как "серебрянная пуля", в отличие от. Скажу даже больше, это самый что ни на есть базовый функционал. Всё приложение не просто можно, а даже нужно разбивать на маленькие модули. Люди, знакомые с require.js, оценят написанный ниже код:

require([
      "dojo",
      "dojo/domReady!"
], function(
    dojo
){
    dojo.query("div.centered").style({
      color: "orange",
      textWeight: "bold"
    });
});

Классическое наследование

Стоян Стефанов в своей книге "JavaScript. Шаблоны" выразил мысль о том, что для подготовленного программиста наследование через прототип, используемое в JS, является более мощным, чем классическое (через классы). Не могу с ним согласиться, поскольку сама концепция является довольно спорной.

Существует множество способов обойти прототипное наследование JavaScript, и один из них предлагает Dojo. И не просто предлагает, а так же реализует концепцию множественного наследования, когда результирующий класс получает все свойства и методы своих родителей. Для объявления класса в Dojo имеется метод declare(). С его помощью можно очень легко создать новый "класс" (не в том смысле, в каком мы понимаем его говоря о языках с чистым классическим наследованием, например, C++ или Python, а лишь его эмуляцию).

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

Dijit и Dojox

Dijit

Dijit представляет собой готовую библиотеку виджетов, входящую в официальную поставку Dojo. В комплекте идёт несколько тем и базовые контролы, необходимые для создания Rich Interface Applications - кнопки, меню, диалоговые окна (опять эмуляция, ясное дело), поля выбора, деревья, списки, панели... Лучше посмотрите сами вот здесь.

Можно создавать свои собственные компоненты (виджеты) на основе имеющихся. Есть даже базовые классы - _BaseWidget, _TemplatedMixin и другие. Да, здесь нет директив из мира Angular, и контроллеров тоже. И сервисов нет, о, Боже, я в аду! Если нам нужно использовать какой-то виджет, мы пишем в свойстве элемента DOM, например, data-dojo-type="dijit/form/Button", и получаем кнопку. Соответственно, нет здесь и проблем с приоритетом директив, и возни с transclude, контролллерами и прочими столь милыми сердцу фанатов Angular надстройками над языком, которые ко второй версии весело отомрут, будучи заменены Web-компонентами.

Dojox

Как я понял, это набор не входящих в официальную поставку виджетов. Авторы могут поддерживать, а могут и не поддерживать их. Исправление ошибок? Новые фичи? Какой-то Road Map? Всё на совести автора, никто ничего не обещает. Однако, библиотека и набор возможностей впечатляют.

Причины непопулярноcти Dojo

Причин множество, я остановлюсь на тех, которые кажутся мне наиболее явными и значительными. Отсортированы в порядке всплытия в памяти.

Скудная документация

Документация к Dojo не просто бедная, а очень бедная. Некоторые статьи были написаны ещё в нулевых, и с тех пор ни разу серьёзно не перерабатывались. Литература? Самое свежее, что я видел - книга 2009 года. От жизни отстала очень сильно.

Высокий порог вхождения

Angular не может похвастать доброжелательностью к новичкам, однако, даже там, сев вечером с кружкой чая ближе к ночи уже можно сделать что-то более-менее работающее. В случае с Dojo ситуация совершенно иная. Фреймворк требует долгого, настойчивого изучения. Используемые в нём подходы серьёзно отличаются от тех, к которым привыкли пользователи Angular и jQuery.

Слабый пиар

В отличие от Google, пихающего свой Angular буквально везде, IBM - один из основных спонсоров проекта - практически никак его не продвигает. Попробуйте сами найти на YouTube какой-нибудь dojoConf или вебинар по новым возможностям. Может быть, у вас получится найти how-to или разбор сложного примера? Дайте мне ссылку! В основном всё, что я находил, описывается следующим образом: "Вот смотрите, числа обладают свойствами коммутативности. 2+2=4. Зная это, не трудно доказать теорему Ферма". Другими словами, пропущен средний уровень, порой даже складывается впечатление, что спецов, которые могут написать высококачественную статью про Dojo и имеют на это время, свободное от загребания бабла, попросту не существует.

Итоги

Так стоит ли тратить время на этот фреймворк?

Сейчас я не готов ответить на этот вопрос однозначно. Свой новый проект я начал писать на нём, и пока дело не сильно продвинулось. Однако, как мне кажется, Dojo именно тот проект, на который стоит равняться. Многие идеи, которые первыми появились именно в нём, впоследствии были заимствованы другими фреймворками. Исходный код и сама структура проекта (а так же виджетов Dijit) являются хорошим образцом продуманного, профессионального подхода. Стиль, модульность, комментарии - всё это выполнено блестяще.

Django Rest Framework - обновление поля типа ImageField

Убил сегодня полдня на решение этой проблемы. Чтобы не забыть, сразу же публикую всё здесь.

Исходные данные

Дано:

  • Модель, имеющая поле типа ImageField
  • Django REST Framework
  • ngFileUpload на фронте

Задача: сделать возможным загрузку изображений в указанное поле на основе Class-Based View в DRF.

Решение

Фронт-энд:

Вёрстка

<img ng-src="{$ item.logo200x200 $}" ng-model="logo" ngf-select ngf-change="uploadLogo(files)" accept="image/*" />

Да, всего одна строка. Вы можете поместить указанное изображение в любой подходящий контейнер, например, панель из Twitter Bootstrap.

Что делает этот код:

Параметр Описание
ng-src="{$ item.logo200x200 $}" Связываем свойство модели и источник для нашего изображения. Делается через директиву Angular ng-src, как того советует официальная документация. На скобки в виде '{$' и '$}' не обращайте внимания. Т.к. на сервере используется стандартный шаблонизатор Django, приходится для Angular использовать другие скобки.
ng-model="logo" Для выбора файлов будет использоваться отдельная модель - logo
ngf-select Указываем, что данное изображение (можно использовать вообще-то что угодно) является полем ввода для плагина ngFileUpload
ngf-change="uploadLogo(files)" При изменении значения поля выполняем указанную функцию. Загрузка без нажатия кнопки "Загрузить", в общем, достаточно лишь выбрать файл.
accept="image/*" Разрешаем выбирать любые изображения. Фильтр для окна выбора файла.

После того, как будет произведён клик по указанному изображению, откроется обычное окно открытия файла. Когда же файл будет выбран, запустится функция загрузки изображения:

LogoController.js

$scope.uploadLogo = function() {
    if ($scope.logo.length < 1) {
        return;
    }
    Upload.upload({
        url: logoUrl, // /api/item/3/logo/
        file: $scope.logo,
        method: 'PATCH'
    }).success(function(data) {
        $scope.item.logo = data.logo;
    });
};

Я описал лишь одну функцию контроллера. Надеюсь, догадаться, что нужно инжектировать $scope и Upload, не сложно.

Обратите внимание, для загрузки логотипа используется метод PATCH, а файл логотипа помещяется в объект file - потом именно его будем обрабатывать на сервере.

Бэк-энд

Нам понадобятся модель, отдельный сериализатор для логотипов и отдельное представление. Так же размеры всех логотипов следует нормализовать - не более 200px по большей стороне. Для этого можно написать отдельную функцию - resize_logo(), принимающую как аргумент экземпляр нашей модели.

core.helpers.py

from PIL import Image

MAX_THUMBNAIL_SIZE = 200

def resize_logo(instance):
    """
    Resize model logo to needed sizes.
    """
    width = instance.logo.width
    height = instance.logo.height

    filename = instance.logo.path

    max_size = max(width, height)

    if max_size > MAX_THUMBNAIL_SIZE:  # Да, надо изменять размер
        image = Image.open(filename)
        image = image.resize(
            (round(width / max_size * MAX_THUMBNAIL_SIZE),
             round(height / max_size * MAX_THUMBNAIL_SIZE)),
            Image.ANTIALIAS
        )
        image.save(filename)

Пришло время описать саму модель, переопределив её метод save() таким образом, чтобы при сохранении размеры изображения для логотипа нормализовались, как нам нужно:

core.items.models.py

from os import path

from django.db import models

from core.helpers import resize_logo

class ItemModel(models.Model):

    name = models.CharField(
        "Название",
        max_length=255,
        help_text='Максимум 255 знаков',
        null=False,
        blank=False
    )
    logo = models.ImageField(
        "Логотип",
        upload_to=path.join('item', 'logo'), # Отдельный каталог для аватаров
        null=True,
        blank=True,
    )

    def save(self, *args, **kwargs):
        # Сначала модель нужно сохранить, иначе изменять/обновлять будет нечего
        super(ItemModel, self).save(*args, **kwargs)

        # Приводит размеры лого к одному виду - 200px по наибольшей стороне
        if self.logo:
            resize_logo(self)

    class Meta:
        app_label = 'core'
        db_table = 'item'
        verbose_name = 'элемент'
        verbose_name_plural = 'элементы'

Теперь можно описать части, относящиеся к API - сериализатор, представление и часть конфигурации URL.

api.items.serializers.py

from rest_framework import serializers

from core.items.models import ItemModel

# Тут должны быть описаны остальные сериализаторы, сейчас же опускаю для краткости


class ItemLogoSerializer(serializers.ModelSerializer):

    class Meta:
        model = ItemModel

Как видно, сериализатор крайне прост. Опишем наше представление.

api.items.api.py

from rest_framework import permissions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from core.items.models import ItemModel

from .serializers import ItemLogoSerializer


class ItemLogoAPIView(APIView):

    permission_classes = [
        permissions.IsAdminUser,
    ]

    serializer_class = ItemLogoSerializer

    # Обновление модели - методом PATCH, как я уже писал выше
    def patch(self, *args, **kwargs):

        # Находим нужную модель (по-хорошему надо обернуть в try ... except, но
        # сейчас я этого делать не буду, чтобы не загромождать код)
        instance = ItemModel.objects.get(pk=kwargs.get('pk'))

        # Получаем из запроса наш файл (как указали выше, в JS)
        instance.logo = self.request.FILES['file']

        # Сохраняем запись (тут должна быть проверка значений встроенными в DRF
        # методами, но сейчас я этого делать не буду)
        instance.save()

        # Возвращаем ответ - нашу сериализованную модель и статус 200
        return Response(
            ItemLogoSerializer(instance).data,
            status=status.HTTP_200_OK
        )
Обязательно проверяйте, что именно приходит от клиента, иначе будут проблемы. Так же добавьте нужные права в permission_classes.

Теперь - самое простое - конфигурация URL:

api.items.urls.py

from django.conf.urls import url

# Тут должен быть импорт остальных сериализаторов
from .api import ItemLogoAPIView

urlpatterns = [
    # А здесь должны быть остальные URL (создание/получение/обнавление)
    url(r'^(?P\d+)/logo/$', ServiceLogoAPIView.as_view()),
]

Ну что ж, всё выглядит не таким уж сложным. Пришло время закрыть вопросы на Toster'е и StackOverflow.

DRF, часть 1 - Проектирование структуры API

Введение

Автор не является экспертом с мировым именем в данном вопросе и всего лишь публикует здесь свои мысли, которые кажутся ему правильными на момент написания. Для более полной информации о работе с Django REST Framework обращайтесь к официальной документации.

Раз уж у меня не получается написать полноценную огромную статью про Django REST Framework, следует публиковать хотя бы небольшие заметки. Начать следует с прописных истин.

  1. 1. Дуб - дерево.
  2. 2. Олень - животное.
  3. 3. Смерь - неизбежна.
  4. 4. api - отдельное приложение в нашем проекте
  5. 5. Следует придерживаться общепринятых правил именования частей API
  6. 6. API должен быть версионным
  7. 7. API должен быть простым

С первыми тремя пунктами всё ясно, поэтому перейду сразу к четвёртому и нижеследующим.

Структура каталогов и версионность

Сначала я пытался запихнуть API в разные части основного проекта, создавал в каталоге с приложениями файлы api.py, serializers.py и permissions.py.

О том, насколько это плохая идея, я узнал почти сразу же, когда начал путаться с тем, что и где лежит. Стоило только переименовать один из каталогов, как сразу же всё начинало сыпаться. В итоге я пришёл к тому, что API - не просто отдельное приложение, содержащее только файлы __init__.py и urls.py (со ссылками на нужные файлы в приложениях Django), а полноценный модуль со множеством вложенных модулей. В общем, почувствуйте разницу:

Было
/api/
    __init__.py
    urls.py
/news/
    __init__.py
    admin.py
    api.py
    models.py
    serializers.py
    tests.py
    urls.py
    views.py
/comments/
    __init__.py
    admin.py
    api.py
    models.py
    serializers.py
    tests.py
    urls.py
    views.py
Стало:
/api/
    /v1/
        /news/
            /comments/
                __init__.pt
                api.py
                permissions.py
                serializers.py
                urls.py
            __init__.py
            api.py
            permissions.py
            serializers.py
            urls.py
    __init__.py
    urls.py
/core/
    /comment/
        __init__.py
        api.py
        permissions.py
        serializers.py
        urls.py    
    /news/
        /comments/
             __init__.py
            admin.py
            models.py
            tests.py
            views.py
         __init__.py
        admin.py
        models.py
        tests.py
        views.py

Надеюсь, структура понятна. Во-первых, всё, что связано с API, переехало в одноимённое приложение. Во-вторых, API теперь поддерживает версионность. Для этого нужно всего ничего - создать соответствующие каталоги и правильно описать файл urls.py в самом начале. Я сделал так:

api/urls.py
from django.conf.urls import include
from django.conf.urls import url

urlpatterns = [
    url(r'', include('api.v2.urls')),
    url(r'^v1/', include('api.v1.urls')),
    url(r'^v2/', include('api.v2.urls')),
]

Естественно, где-то в главном файле urls.py есть строка, в которой конфигурация URL для API присоединяется к общей конфигурации URL через всё тот же include().

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

Именование API

Сначала хочу сказать, как делать НЕ НАДО:

Вешать всё на один URI и в зависимости от содержимого полученных пакетов выполнять то или иное действие либо возвращать нужный набор данных.
Пример:
http://example.org/api/

Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.

Использовать глаголы в частях URL или для выполнения определённых действий.
Пример
GET /api/news/get/?id=1     - получить новость с id=1
GET /api/news/get/all/      - получить все новости
POST /api/news/add/         - добавить новость
POST /api/news/update/?id=1 - обновить новость с id=1
POST /api/news/delete/?id=1 - удалить новость с id=1
Ещё примерчик
GET  /api/news/?id=1               Получение записи с id=1
POST /api/news/?id=1?action=update Обновление записи с id=1
POST /api/news/?id=1?action=delete Удаление записи с id=1

Это API? Конечно же, нет! Суть REST-сервисов в том, что требуемое действие определяется HTTP-заголовком!

Пример
URL - /api/news/:id

GET    /api/news/ - получить список всех новостей
POST   /api/news/ - создать новость
GET    /api/news/12/ - вернёт новость с id=12
PATCH  /api/news/12/ - обновить новость с id=12
DELETE /api/news/12/ - удалить новость с id=12

Разница, как говорится, налицо.

Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).

Названия сущностей во множественном числе. Каждой сущности - отдельный URL.
Пример

/api/articles/:id/ - статьи
/api/friends/:id/  - друзья
/api/news/:id/     - новости
/api/users/:id/    - пользователи
/api/videos/:id/   - видео
Пример:
GET    /api/acticles/:id/comments/ - получить комментарии к статье
GET    /api/news/:id/comments/     - получить комментарии к новости
POST   /api/articles/:id/comments/ - добавить комментарий к статье
POST   /api/news/:id/comments/     - добавить комментарий к новости
DELETE /api/comments/articles/:id/ - удалить комментарий к статье
DELETE /api/comments/news/:id/     - удалить комментарий к новости

Сериализаторы и всё остальное.

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

Каждой версии API - свои права, сериализаторы и представления. Не следует делать предположений, что в течение продолжительного времени список полей моделей будет постоянным, а права на доступ к частям API не изменятся.

Простота API - почти недостижимый идеал. Чтобы сделать его простым, придётся проделать очень сложную работу. Не удивляйтесь. Кнопка включения на корпусе компьютера - очень простой интерфейс, однако, по одному нажатию на неё выполняется огромное количество самых разнообразных и сложных действий, на глубокое понимание которых некоторые люди тратят большую часть своей жизни. Таким же должен быть и ваш API - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.

Итоги

  • Разработчики хотят работать с сущностями, а не с URL. Сделайте интуитивно понятными все URL вашего API.
  • Разработчики ленивы и не хотят изучать 34 параметра, влияющие на результат вызова одного единственного метода (из 56, имеющихся в наличии). Старайтесь избегать реализации поведения API через параметры. limit, offset, filter и sorting - необходимое зло, от них никуда не деться.
  • Действие, выполняемое методом API, должно зависеть от HTTP-заголовка. GET для чтения, POST для создания, PUT/PATCH для обновления и DELETE для удаления записей.
  • Пишите документацию к вашему API. Даже самая лучшая структура URL для API неспособна описать различные тонкости и нюансы его работы.

Получение аргументов URL в сериализаторах Django REST Framework

Небольшая заметка о том, как в ClassBasedView Django REST Framework получить значение параметра из URL.

Допустим, наши URL сконфигурированы таким образом:

urls.py
from django.conf.urls import url

from .api import ArticleListCreateView
from .api import ArticleDetailView
from .api import ArticleCommentListCreateView

urlpatterns = [
    url(r'^article/$', ArticleListCreateView.as_view()),
    url(r'^article/((\d+))/$', ArticleDetailView.as_view()),
    url(r'^article/((\d+))/comment/$', ArticleCommentListCreateView.as_view()),
]

Делать какую-либо работу для вида ArticleDetailView не приходится - достаточно создать класс, унаследованный от RetrieveUpdateDestroyAPIView.

Получить доступ к параметру pk в виде не проблема, достаточно переопределить метод get_queryset(), например, так:

article.comment.api.py
from rest_framework.generics import RetrieveUpdateDestroyAPIView

from article.models import Article
from .models import ArticleComment


class ArticleCommentListCreateView(RetrieveUpdateDestroyAPIView):

    model = ArticleComment

    # Возвращаем только комментарии к указанной статье
    def get_queryset(self):
        return ArticleComment.objects.filter(article=self.kwargs['pk])

    def get_serializer_class(self):
        if self.request.method == 'GET':
            return ArticleCommentReadSerializer
        return ArticleCommentWriteSerializer

А вот получить в сериализаторе значение параметра pk можно не вполне очевидным способом:

serializers.py
from django.shortcuts import get_object_or_404
from rest_framework import serializers

from .models import ArticleComment
from article.models import Article

# ArticleReadSerializer не описан из-за своей простоты

class ArticleCommentWriteSerializer(serializers.ModelSerializer):

    article = serializers.PrimaryKeyRelatedField(
        queryset=Article.objects.all(),
        default=None
    )

    # Заменяем валидатор поля article таким образом, чтобы всегда
    # подставлялось значение из URL
    def validate_article(self, value):
        # Кто бы мог подумать???
        article_id = self.context['view'].kwargs['pk']
        return get_object_or_404(Article, pk=article_id)

    class Meta:
        model = ArticleComment

Анонс статьи по Django REST Framework и Angular

В сети крайне мало информации по работе с Django REST Framework, а на русском можно сказать что нет вообще. Учитывая сложность вопросов на Toster и устарелость статей на Хабре, решил написать довольно объёмную статью по работе с данным фреймворком. Сам не являюсь профессионалом в нём и WEB-разработке вообще, но поделиться накопленными знаниями будет не лишне.

Примерное оглавление

  • Зачем нужен DRF?
  • Установка, подключение к проекту.
  • Сериализаторы
  • Виды-функции и виды-классы
  • Типовые задачи
  • Подключение Angular Resource
  • Несколько советов по API
  • Исходный код моего модуля Angular для работы с API с объяснением написанного

Пишите в комментариях, какие вопросы дополнительно следует включить в статью, постараюсь рассмотреть их все.