16 Май, 2016

Excel-Django-Angularjs (график по данным из .xls) #1

Сделал приятное тестовое задание на позицию python/django/angularjs разработчика. Делюсь граблями, костылями и велосипедами, с которыми столкнулся.

Как сказали
Как хотел сначала
Как правильно
Как реализовывал
views.py
models.py и содомия
Валидатор


Как сказали.
Сделать одностраничное веб-приложение для рисования графика по данным из файла, на бэкэнде Django, на клиенте Angular — форма загрузки и кнопка отрисовки графика.

  1. Человек загружает Excel файл, в котором столбиком записаны координаты.
  2. Файл летит на сервер.
  3. Данные из файла парсятся в базу данных.
  4. Человек нажимает кнопку рисования графика.
  5. Angular тащит из базы данных… данные.
  6. Какая-нибудь библиотека рисует график.


Как хотел сначала.
Начальный опыт: месяц Django, час официального туториала Angular, javascript не трогал. Начал делать просто как умею:

  • Django-модель OfficeFile с единственным полем FileField.
  • Django ModelForm в представлении (OfficeFileView)
  • При загрузке, прямо в представлении OfficeFileView, библиотекой pandas парсим из .xls данные в словарь, передаем его на страницу контекстом представления.
  • Как-нибудь чем-нибудь рисуем график.

Упершись в последний пункт, понял хреновость решения — инструмент получается никчемным. Даже если бы я выполнил задачу так, повторять такое в рабочем проекте нельзя, это бесполезные костыли из велосипедов:

  • Фронтэнд мёртв и заперт в пределах одной страницы. Он ничего не подгружает, и только один раз рисует график.
  • Зачем нужна модель OfficeFile, если после сохранения объекта он больше не используется?
  • Ужасная архитектура порождает ужасный UX: для каждого построения графика нужно загружать новый файл, после загрузки нового графика старый уходит в небытие.


Как правильно.
Django-модель OfficeFile с полями file, upload-date и dict_coor:

  • Django-форма принимает файл.
  • При сохранении объекта вытаскиваем из него данные в словарь.
  • Хитроумно конвертируем словарь в строку и кладем результат в dict_coor(CharField).
  • На странице надо сделать список уже загруженных файлов, график любого строится уже на клиенте.
  • Связываем бэкэнд и фронтэнд по HTTP. Здравствуй, REST!
  • Полученные данные делаем javascript списками.
  • Рисуем все, есть миллион js-библиотек.


Как реализовывал.
Немного кода из views.py. Здесь только отдача формы контекстом на страницу, обработка формы и сохранение объекта. Запомним метод save(), позже пригодится.

def onepage(request):
    c = {}
    if request.method == 'POST':
        form = OfficeFileForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect('/')
    else:
        form = OfficeFileForm()

    RequestContext(request, {'form': form})

    return render_to_response('web/base.html', c)


Разберемся с models.py. Сначала общее ревью, объяснение импортов будет по ходу.

from __future__ import unicode_literals
from django.db import models
from django.utils import timezone
from .validators import validate_file_extension #про это будет ниже
from pandas import *
import json

class OfficeFile(models.Model):
    file = models.FileField(validators=[validate_file_extension])
    upload_date = models.DateField(default=timezone.now)
    dict_coor = models.CharField(blank=True, max_length=200)

    def save(__self__):
        super(OfficeFile, __self__).save()
        xls = ExcelFile(__self__.file.path) #получаем ExcelFile
        df = xls.parse(xls.sheet_names[0]) #получаем pandas DataFrame
        dict_data = df.to_dict('list') #получаем словарь с координатами
        listx = dict_data['x']
        listy = dict_data['y']

        i = 0
        while i < len(listx):
            listx[i] = int(listx[i])
            i +=1
        i = 0
        while i < len(listy):
            listy[i] = int(listy[i])
            i += 1

        dict_data['x'] = listx
        dict_data['y'] = listy
        __self__.dict_coor = json.dumps(dict_data)
        super(OfficeFile, __self__).save()

Сама модель понятна, а вот метод save() не так очевиден. Переопределил я его для моментального парсинга данных из файла в поле dict_coor при сохранении объекта. Наш form.save() из view.py собирает данные формы и применяет save(), прописанный в модели.

Здесь мы подключаем pandas, который умеет работать с Excel файлами, и начинаем содомию. Получив в итоге парсинга словарь dict_coor мы узнаем, что все координаты имеют тип numpy.int64. С этим не будет работать ни JSON, ни сам Javascript, так что некрасивым проходом по всем элементам списков мы «перетипируем» их в родной int. JSON форматирует словарь в строку, которую мы сохраняем.

Интересно, что на локальном сервере все работало и с numpy.int64! Причем, на него ругался JSON.dumps, работающий локально и одинаково и там, и там. Мистика.

Почему в самом начале метода мы сразу сохраняем объект? Для обработки файла нам нужно, чтобы он уже лежал на своем месте в нашем хранилище. Все, что происходит после — заполнение поля dict_coor уже существующего объекта.

Почему везде написано __self__, а не self? После очередного обновления Django надо так.

Валидация.
В отдельном файле validators.py лежит валидатор для формы, пускающий только .xls и .xlsx файлы:

validators.py

В данном случае форма отправит любой файл, но Django примет только указанный в валидаторе формат.

В продолжении речь пойдет о Tasypie API, REST и Angularjs.