23 Сен, 2016

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

В первой части разобрали модели, представления и валидатор. Теперь frontend и API.

UPD: Статья пишется спустя несколько месяцев, от этого скомконность.

TastypieAPI
Resources (api.py)
Angularjs
Принимаем данные и находим границы графика
Рисуем график
Конечный app.js
Страница HTML

Tastypie API.
Сейчас модно сообщать frontend и backend через RESTfull API. Это когда с клиента на определенный URL по HTTP отправляется конкретно структурированный запрос, а сервер так же отдает информацию. Используются GET, POST, DELETE, PUT методы.

На стороне сервера нам необходим какой-то инструмент для сериализации данных, определяющий, по каким URL какую информацию кому отдавать. Но сначала пойдем в url.py:

from tastypie.api import Api
from web.api import OfficeFileResource

v1_api = Api(api_name='v1')
v1_api.register(OfficeFileResource())

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

Теперь Django будет принимать запросы к API по http://127.0.0.1/api/v1/… Рассмотрим в директории приложения файл api.py:

from tastypie.resources import ModelResource
from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
from .models import OfficeFile

class OfficeFileResource(ModelResource):
class Meta:
queryset = OfficeFile.objects.all() //делаем запрос всех объектов модели
resource_name = 'officefile' //имя ресурса в запросе
allowed_methods = ['get', 'delete'] //методы, которые нужно использовать
authentication = Authentication() //определяем, как аутентифицировать и авторизировать юзеров
authorization = Authorization()
always_return_data = True

Запрос http://127.0.0.1/api/v1/officefile/ вернет информацию по всем объектам. Для проверки информации прямо в браузерной строке нужно прибавить к запросу «/?format=json» (http://127.0.0.1/api/v1/officefile/?format=json).

Кроме этого, мы можем конкретизировать запросы с помощью фильтров. Например, если нужно получить объекты, созданные в определенное время. По любому полю модели можно делать фильтр, но это материал следующей статьи про Memebook! (Тут будет ссылочка)

Angularjs.
Я всегда боялся Javascript. После Python, C++, да хоть BASIC, он с разбега ломает все надежды на лучшее об свой синтаксис. Но через пару часов становится ясно — тащится. Весь код ниже это смесь из нескольких начальных туториалов по AngularJS и я почти уверен, что он ужасен. Но это работает.

Сначала ревью функций, потом конечный код.

Принимаем данные.
Пишем функцию, которая отправляет http запрос GET нашему API и вносит все принятые объекты в массив files. Каждый JSON объект — объект из базы данных, а все его поля были определены нами в API. Мы будем работать только со словарем dict_coor.

$scope.files = [];
$scope.getAll =  function() {
$http.get('/api/v1/officefile/').then(function(response) {
$scope.files = response.data.objects;
});
};

На странице таблица со всеми объектами. При нажатии на какой-нибудь запускается функция clickme(f), где f — сам объект. Функция парсит наш словарь dict_coor в actualFile, где мы проходим по каждому ключу и огромным некрасивым способом вносим данные в массив array, попутно определяя предельные координаты (это необходимо для масштабирования итогового графика.

$scope.clickme = function (f) {
var actualFile = JSON.parse(f.dict_coor);
var array = [];
console.log(actualFile);

for (key in actualFile) {
if (key == 'x_value') {
var minX = actualFile.x_value[0];
var maxX = actualFile.x_value[0];

for(var i=0; i<actualFile.x_value.length; i++) {
array.push([actualFile.x_value[i], actualFile.y_value[i]]);

if(actualFile.x_value[i] > maxX){
actualFile.x_value[i] = maxX;
} else if (actualFile.x_value[i] < minX) {
actualFile.x_value[i] = minX;
}

if(actualFile.y_value[i] > maxX){
actualFile.y_value[i] = maxX;
} else if (actualFile.y_value[i] < minX) {
actualFile.y_value[i] = minX;
}
}
}

if (key == 'x') {
var minX = actualFile.x[0];
var maxX = actualFile.x[0];

for(var i=0; i<actualFile.x.length; i++) {
array.push([actualFile.x[i], actualFile.y[i]]);

if(actualFile.x[i] > maxX){
actualFile.x[i] = maxX;
} else if (actualFile.x[i] < minX) {
actualFile.x[i] = minX;
}

if(actualFile.y[i] > maxX){
actualFile.y[i] = maxX;
} else if (actualFile.y[i] < minX) {
actualFile.y[i] = minX;
}
}
}
}

Рисуем графики.

Поверхностное гугление показало, что самый простой фреймворк для отрисовки графиков на JS — Google Charts. Границы графика minX-5 и maxX+5 зависят от наших данных, а не захардкожены. График гнется, а цифра 5 от балды, чтобы точки не находились прямо на краю графика.

google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('number', 'x');
data.addColumn('number', 'y');
data.addRows(array);

var options = {
title: 'Graphic:',
hAxis: {title: 'y', minValue: minX-5, maxValue: maxX+5},
vAxis: {title: 'x', minValue: minX-5, maxValue: maxX+5},
legend: 'none'
};

var chart = new google.visualization.ScatterChart(document.getElementById('chart_div'));

chart.draw(data, options);
}

Конечный app.js.
Пойдем в /static/js/app.js:

app.js
var app = angular.module('OfficeFileApp', []);

app.controller('OfficeFileController', function($scope, $http) {
$scope.files = [];

$scope.getAll =  function() {
$http.get('/api/v1/officefile/').then(function(response) {
$scope.files = response.data.objects;
});
};
$scope.getAll();

google.charts.load('current', {'packages':['corechart']});

$scope.clickme = function (f) {
var actualFile = JSON.parse(f.dict_coor);
var array = [];
console.log(actualFile);

for (key in actualFile) {
if (key == 'x_value') {
var minX = actualFile.x_value[0];
var maxX = actualFile.x_value[0];

for(var i=0; i<actualFile.x_value.length; i++) {
array.push([actualFile.x_value[i], actualFile.y_value[i]]);

if(actualFile.x_value[i] > maxX){
actualFile.x_value[i] = maxX;
} else if (actualFile.x_value[i] < minX) {
actualFile.x_value[i] = minX;
}

if(actualFile.y_value[i] > maxX){
actualFile.y_value[i] = maxX;
} else if (actualFile.y_value[i] < minX) {
actualFile.y_value[i] = minX;
}
}
}

if (key == 'x') {
var minX = actualFile.x[0];
var maxX = actualFile.x[0];

for(var i=0; i<actualFile.x.length; i++) {
array.push([actualFile.x[i], actualFile.y[i]]);

if(actualFile.x[i] > maxX){
actualFile.x[i] = maxX;
} else if (actualFile.x[i] < minX) {
actualFile.x[i] = minX;
}

if(actualFile.y[i] > maxX){
actualFile.y[i] = maxX;
} else if (actualFile.y[i] < minX) {
actualFile.y[i] = minX;
}
}
}
}

google.charts.setOnLoadCallback(drawChart);

function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('number', 'x');
data.addColumn('number', 'y');
data.addRows(array);

var options = {
title: 'Graphic:',
hAxis: {title: 'y', minValue: minX-5, maxValue: maxX+5},
vAxis: {title: 'x', minValue: minX-5, maxValue: maxX+5},
legend: 'none'
};

var chart = new google.visualization.ScatterChart(document.getElementById('chart_div'));

chart.draw(data, options);
}
};

});

Страница HTML.
Ревью конечного HTML не имеет никакого смысла, потому что там и Bootstrap, и Materialize, и Google Charts с Angular, не говоря уже о шаблонизаторе Django. Вот конечный код, в которым видны и зависимости, и версии софта, и все красоты Material, и отображение элементов AngularJS.

А это версия, которая сейчас работает:

base.html
<!DOCTYPE html>
<html>
<head>
{% load staticfiles %}
<link rel="stylesheet" href="{% static 'css/web.css' %}">
<meta charset="UTF-8">
<title>Excel-Django-Angular</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css">
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-resource.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/restangular/1.3.1/restangular.min.js"></script>
<script type="text/javascript" src="{% static "js/app.js" %}"></script>
<script lang="javascript" src="https://cdnjs.cloudflare.com/ajax/libs/xls/0.7.5/xls.min.js"></script>

</head>
<body>

<nav >
<div class="blue lighten-2 nav-wrapper ">
<a href="/" class="brand-logo center">Excel-Django-Angular</a>
</div>
</nav>

<div class="content container" ng-app="OfficeFileApp" ng-controller="OfficeFileController">
<div class="row">
<div class="col-md-5">
<div class="card z-depth-1">
<div class="info">
<p>Upload file (.xls of .xlsx) or choose one of already downloaded.</p>
<p>First row in file should be [x] for A1 and [y] for B1.</p>
<p>Click <i class="large material-icons">trending_up</i> for draw, <i class="large material-icons">delete</i>
for remove file.</p.
<p>All files will be deleted after 2 hours.</p>

</div>
</div>

<div class="card z-depth-1">
<form method="POST" action="/" enctype="multipart/form-data" class="center-align">{% csrf_token %}
<div>
{{ form.file }}
</div>
<button type="submit" class="waves-effect blue lighten-3 waves-light btn">Upload</button>
</form>
</div>

<div class="card z-depth-1">
<div class="panel panel-default" >
{% verbatim %}
<table class="table" >
<tr>
<td><b>Filename</b></td>
<td><b>Upload Date</b></td>
</tr>
<tr ng-repeat="file in files">
<td class="center-align">{{ file.file }}</td>
<td class="center-align">{{ file.upload_date }}
<button ng-click="clickme(file)" class="right-align waves-effect blue lighten-3 waves-light btn "><i class="center-align large material-icons">trending_up</i></button>
<button ng-click="delete(file.id)" class="right-align waves-effect blue lighten-3 waves-light btn "><i class="large material-icons">delete</i></button></td>
</tr>
</table>
{% endverbatim %}
</div>
</div>
</div>

<div class="col-md-7 card z-depth-1">
<div id="chart_div" style="width: 700px; height: 500px;"></div>
</div>
</div>
</div>
</body>
</html>

Вопросы?