Доброго времени суток.
Так случилось, что моя работа связана с написанием отчетов.
Этому я посвятил около 8 лет. Отчеты — это глаза бизнес-процесса и информация,
необходимая для принятия оперативных решений.
Вначале наш отдел делал отчеты,
— Принимая задачи по outlook
— Составляя sql-запрос
— Отправляя результаты заказчику в xls
— В лучшем случае, сохраняя sql-код куда-то в папку (а иногда и не сохраняя)
Но это было скучно и неинтересно. Так появилось простейшее приложение на PHP,
в котором каждый отчет был представлен в виде php-файла с одним классом, имеющим единственный (помимо конструктора) метод show()
В таком виде, система прожила 5,5 лет, за которые мной и еще одним человеком было написано более 500 различных отчетов.
В процессе появился опыт и стало понятно, что многое (если не все) сделано не так, да и PHP уже не устраивал.
Сервер отчетов был переписан на django, где была «админка» и код приложения уже не правился.
В процессе работы снова накопилось несколько мыслей,
в итоге сервер был снова переписан.
Я решил представить сообществу текущую версию системы, возможно, она кому-то облегчит жизнь,
сняв рутину отчетности и переложит ее на машину, которая готова сотни раз формировать отчеты
тысячи людям по сотни разных входных параметров.
Это рабочий инструмент, который вы можете (если захотите) использовать в работе,
который нам использовать просто нравится.
Структура проекта
Все заинтересованные, могут сразу ознакомиться с исходным кодом на https://bitbucket.org
Концепция и основные элементы
Система состоит из набора отчетов.
— Отчеты слабо (практически никак) не связаны друг с другом, чтоб разработчик мог быть уверен, что ничего не испортит в других отчетах, отредактировав какой-то один.
— Элементы отчета жестко связанны внутри отчета и влияют друг на друга.
Отчеты не организованы в иерархию (от этого ушли), а помечаются набором тэгов.
На главной системы отчетов можно щелчками набирать нужные сочетания тэгов, которые работают, как фильт
и поиск отчетов.
Элементы отчета «ленивые» и начинают выполнятся в момент сборки на уровне компоновщика виджетов,
что дает возможность выполнять только те запросы к базе, которые необходимы,
размещая в отчете и группированные данные и детализацию.
За счет кэширования данных на уровне источника, несколько виджетов, выводящих данные из одного источника, дают только один запрос в базу данных.
<!--
Выполняются только те источники данных,
на которых основаны виджеты, которые должны быть показаны
-->
{% if get.detail == 'table' %}
{{table.some_table_detail}}
{% elif get.detail == 'chart' %}
{{charts.some_chart}}
{% else %}
{{tables.grouped_table}}
{% endif %}
Есть параметры доступа пользователю к отчету, помимо самого факта доступности.
Которые могут быть использованы для предоставлении доступа к части отчета или части данных
select * from some_table t
where 1 = 1
-- Это инъекция
{% if user_params.only_filials %}and filial_id in ({{user_params.only_filials|join:","}}){% endif %}
-- Это привязанная переменная, которая заменяется на %s
{% if user_params.only_sectors %}and sector_id = [[user_params.only_sectors.0]]{% endif %}
При необходимости одного варианта вызова менеджера в шаблонной системе, используется __call__
Если возможны несколько вариантов, используется __getitem__ для словарного объекта.
Каждый отчет может состоять из:
class EnvirementManager(object):
u'''
Работа с элементами окружения, render строк внутри переменных окружения,
разбор запросов и т.п.
'''
def __init__(self, **kwargs):
self._env = kwargs
def render(self, text, **dict):
u''' Обработка строки шаблонной системой '''
return render_string(text, self._dict_mix(dict))
def render_template_compiled(self, template, **dict):
u''' Обработать предварительно скомпилированный шаблон '''
return template.render(Context(self._dict_mix(dict)))
def render_template(self, path, **dict):
u''' Обработать '''
return render_to_string(path,
self._dict_mix(dict),
context_instance=RequestContext(self._env['request']))
def render_sql(self, sql, **dict):
u''' Обрабатывает строку шаблонной системой
Возвращает обработанную строку и массив переменных для привязки '''
sql_rendered = self.render(sql, **dict)
binds = []
for bind_token in re.findall(r'[{2}.+]{2}', sql_rendered):
env_path = bind_token[2:-2]
binds.append(attribute_by_name(self._env, env_path))
sql_rendered = sql_rendered.replace(bind_token, u'%s')
return (sql_rendered, binds)
def _dict_mix(self, dict):
if dict:
e = self._env.copy()
e.update(dict)
else:
e = self._env
return e
def get(self, name, value=None):
return self._env.get(name, value)
def __getitem__(self, name):
return self._env[name]
def __setitem__(self, name, value):
self._env[name] = value
Инициализируется именованными аргументами и используется для рендеринга всего
с учетом тех переменных, которыми инициализирован.
За счет этого, мы можем гарантировать наличие из любого места заранее известного набора переменных.
Может рендерить sql-запросы, которые являются источниками данных (но об этом чуть ниже)
class DatasetManager(object):
u''' Управление источниками данных '''
def __init__(self, request, dataset, env):
u'''
Конструктор
'''
self._request = request
self._dataset = dataset
self._env = env
self._cache = []
self._xml = None
if self._dataset.xml_settings:
self._xml = etree.fromstring(self._env.render(self._dataset.xml_settings))
def get_data(self):
u'''
Выполняет запрос и возвращает словарь с данными
'''
if not self._cache:
self._cache = [[], []]
(sql, binds) = self._env.render_sql(self._dataset.sql)
cursor = self._modify_cursor(connections['reports'].cursor())
cursor.execute(sql, binds)
# Настройки колонок запроса
xml_columns = {}
if self._xml not in (None, ''):
for xml_column in self._xml.xpath('/xml/columns/column'):
attrs = xml_column.attrib
xml_columns[force_unicode(attrs['name'])] = force_unicode(attrs['alias'])
# Колонки запроса (заменяем название колонки, если запрошено)
# это может пригодится при использовании БД, запрещаюей использование длинных имен
for field in cursor.description:
name_unicode = force_unicode(field.name)
self._cache[0].append(xml_columns[name_unicode] if name_unicode in xml_columns
else name_unicode)
self._cache[1] = cursor.fetchall()
return self._cache
def __getitem__(self, name):
u''' вызовы из шаблонной системы работают через словарные объекты '''
if name == 'sql':
return self._env.render(self._dataset.sql)
elif name == 'render':
(sql, binds) = self._env.render_sql(self._dataset.sql)
return {'sql': sql, 'binds': binds}
elif name == 'data':
(fields, data) = self.get_data()
return [dict(zip(fields, row)) for row in data]
def _modify_cursor(self, cursor):
u'''
Модификация параметров курсора (может потредоваться для разных баз баз данных),
например, для Oracle требуется
установка параметров локали, отключение number_as_string = True (по умолчанию в стандартном
backends django)
'''
return cursor
Поставляет:
— Данные в виде кортежа (field_names, rows,)
Для визуализации различными типами виджетов, выгрузки в excel
— Данные в виде списка словарей (каждая строка является словарем)
Для прямого обращения к источнику и генерации html, javascript, css кода в менеджере компоновки (о нем немного позже)
— sql-код без обработки переменных привязки
Используется для организации вложенных источников данных, например:
select key, count(1) from ({{datasets.base_dataset.sql}}) group by key
— sql-код и переменные привязки
Просто для отладки и выводе в отчет
За счет этого, можно:
— Менять на лету sql-запрос в зависимости от переменных менеджера окружения
— На основе одного запроса, можно делать другие так, чтоб у вас группировка и детализация никогда не будут отличатся,
так как основаны на одном источнике, и вы никогда не забудите доработать группировку и забыть сделать это в детализации
— Выборку из одного источника данных использовать для динамической сборки другого.
Например, создать заранее неизвестное кол-во колонок в запросе помесячно, по месяцам (или неделям), входящим в отчетный период (задается пользователем в форме, но о них тоже чуть ниже),
и все это независимо от возможностей базы данных (без pivot|unpivot oracle и xml_query oracle)
class FilterManager(object):
u''' Класс для управления фильтрами '''
def __init__(self, request, filter_obj, env):
u''' Конструктор '''
self._request = request
self._filter_obj = filter_obj
self._env = env
self._form_instance = None
self._changed_data = {}
# Если отправили эту форму, инициализировать форму POST-данными
# Проверить отправленные данные и, если они отличаются от сохраненных, запомним их в self._changed_data
if self._request.method == 'POST' and int(self._request.POST['filter_id']) == self._filter_obj.id:
self._form_instance = self._get_form_class()(self._request.POST)
if self._form_instance.is_valid():
data = self._form_instance.cleaned_data
for key, value in data.items():
if self._env['f'].get(key) != value:
self._changed_data[key] = value
def get_changed_data(self):
return self._changed_data
def get_form(self):
u''' Получить экземпляр формы фильтра '''
try:
# Если экземпляр формы не создан в конструкторе, (при изменениях)
# значит инициализируем из заранее сохраненных значений
if self._form_instance is None:
self._form_instance = self._get_form_class()(initial=self._env['f'])
html = self._env.render('{% load custom_filters %}' + self._filter_obj.html_layout, form=self._form_instance)
return self._env.render_template('reports_filter.html', form_html=html, filter=self._filter_obj)
except Exception as e:
return e
def __call__(self):
u''' Для вызова в шаблоне '''
try:
return self.get_form()
except Exception as e:
return e
def _get_form_class(self):
u''' Собрать форму фильтра и вернуть ее '''
form_attrs = {}
filter_widgets = (DateField, ChoiceField, MultipleChoiceField, BooleanField, CharField, CharField, DateRangeField)
for item in self._filter_obj.form_items.all():
kwargs = {'label': item.title, 'required': False}
if item.xml_settings:
xml = xml = etree.fromstring(self._env.render(item.xml_settings))
else:
xml = None
if item.widget_type == 0:
kwargs['widget'] = forms.DateInput(attrs={'class': 'date'})
elif item.widget_type in (1, 2):
choices = []
for option in xml.xpath('/xml/options/option'):
choices.append((option.attrib['id'], option.attrib['value']))
sql = xml.xpath('/xml/sql')
if sql:
curs = connections['reports'].cursor().execute(sql[0].text)
for row in curs.fetchall():
choices.append((row[0], row[1]))
kwargs['choices'] = choices
elif item.widget_type == 4:
kwargs['max_length'] = 50
elif item.widget_type == 5:
default = xml.xpath('/xml/value')
kwargs['widget'] = forms.HiddenInput(attrs={'value': default[0].text})
form_attrs[item.key] = filter_widgets[item.widget_type](**kwargs)
filter_form = type(str(self._filter_obj.title), (forms.Form,), form_attrs)
return filter_form
Тут все достаточно тривиально. Собираем форму и отдаем форму и измененные данные
class WidgetTableManager(object):
u''' Управляет работой с табличным виджетом '''
def __init__(self, request, widget_obj, dataset, env):
u''' Конструктор '''
self._request = request
self._widget_obj = widget_obj
self._dataset = dataset
self._env = env
self._xml = None
if widget_obj.xml_settings:
self._xml = etree.fromstring(self._env.render(widget_obj.xml_settings).replace('[[', '{{').replace(']]', '}}'))
def get_html(self):
u''' Вернуть html-код таблицы '''
(fields, data) = self._dataset.get_data()
field_settings = {}
table_settings = {}
if self._xml is not None:
table_settings_node = xml_node(self._xml, '/xml/table')
if table_settings_node is not None:
table_settings = table_settings_node.attrib
for xml in self._xml.xpath('/xml/fields/field'):
xml_attributes = dict(xml.attrib)
field_name = xml_attributes['name']
if 'lnk' in xml_attributes:
xml_attributes['tpl_lnk'] = Template(force_unicode(xml_attributes['lnk']))
if 'cell_attributes' in xml_attributes:
xml_attributes['tpl_cell_attributes'] = Template(force_unicode(xml_attributes['cell_attributes']))
field_settings[field_name] = xml_attributes
# Выводимые на экран колонки
fields_visible = []
for index, field_name in enumerate(fields):
settings = field_settings.get(field_name, {})
if 'display' in settings and settings['display'] == '0':
continue
fields_visible.append((index, field_name, settings))
# Вычислить параметры и привязать к данным
rows = []
for row in data:
row_dict = dict(zip(fields, row))
row_settings = {}
# Настройки уровня строки
if 'field_row_style' in table_settings:
row_settings['row_style'] = row_dict[table_settings['field_row_style']]
if 'field_row_attributes' in table_settings:
row_settings['row_attributes'] = row_dict[table_settings['field_row_attributes']]
# Перебрать строки и собрать настройки уровня строки
fields_set = []
for index, field_name, settings in fields_visible:
field = {'name': field_name, 'value': row[index]}
# Если есть ссылка и она должна быть показана
if 'tpl_lnk' in settings and ('lnk_enable_field' not in settings or row_dict[settings['lnk_enable_field']] not in (0, '0', '', None)):
field['lnk'] = self._env.render_template_compiled(settings['tpl_lnk'], row=row_dict)
# Аттрибуты ячейки
if 'tpl_cell_attributes' in settings:
field['cell_attributes'] = settings['tpl_cell_attributes'].render(Context(row_dict))
field['settings'] = settings
fields_set.append(field)
rows.append({'settings': row_settings, 'fields': fields_set})
return render_to_string('reports_widget_table.html', {'fields': fields_visible, 'rows': rows, 'widget_obj': self._widget_obj})
def __call__(self):
u''' При вызове, возвращает таблицу '''
try:
return self.get_html()
except Exception as e:
return u'Ошибка в виджете %s: "%s"' % (self._widget_obj.title, e)
Берет данные из источника данных и выводит в табличном виде. Поддерживает
— форматирование
— Генерацию ссылок (которые могут использоваться для детализации ячейки в другой таблице, график или excel)
— Генерацию произвольных аттрибутов строк (для работы javascript, скрытия или показа итоговых строк и т.п.)
class WidgetChartManager(object):
u''' Менеджер графика '''
def __init__(self, request, chart_obj, dataset, env):
u''' Конструктор '''
self._request = request
self._chart_obj = chart_obj
self._dataset = dataset
self._env = env
self._xml = etree.fromstring(self._env.render(self._chart_obj.xml_settings))
print 1
def __call__(self):
u''' При вызове из шаблона '''
print 2
try:
return self.get_chart()
except Exception as e:
print unicode(e)
return unicode(e)
def get_chart(self):
u''' html со скриптом сборки графика '''
(fields, data) = self._dataset.get_data()
return self._env.render_template('reports_widget_chart.html',
settings=xml_to_dict(xml_node(self._xml, '/xml')),
data=json.dumps([dict(zip(fields, row)) for row in data], cls=JSONEncoder),
chart_obj=self._chart_obj,
)
Тут тоже просто. Берет данные из источника, XML-настройки переводит в dict и рендерит шаблон,
собирая javascript-код графика (используется amcharts)
тэги XML-узлов преобразуются в название парамерта, текст в значение параметра,
то есть можно использовать практически все параметры библиотеки amcharts,
просто поместив нужный тэг у нужную секцию
И, как завершение теоретической части, привожу код класса, который всем этим управляет,
размещая виджеты, или возвращая xls или произвольный документ (html с расширением .doc или .xls)
class WidgetTableManager(object):
u''' Управляет работой с табличным виджетом '''
def __init__(self, request, widget_obj, dataset, env):
u''' Конструктор '''
self._request = request
self._widget_obj = widget_obj
self._dataset = dataset
self._env = env
self._xml = None
if widget_obj.xml_settings:
self._xml = etree.fromstring(self._env.render(widget_obj.xml_settings).replace('[[', '{{').replace(']]', '}}'))
def get_html(self):
u''' Вернуть html-код таблицы '''
(fields, data) = self._dataset.get_data()
field_settings = {}
table_settings = {}
if self._xml is not None:
table_settings_node = xml_node(self._xml, '/xml/table')
if table_settings_node is not None:
table_settings = table_settings_node.attrib
for xml in self._xml.xpath('/xml/fields/field'):
xml_attributes = dict(xml.attrib)
field_name = xml_attributes['name']
if 'lnk' in xml_attributes:
xml_attributes['tpl_lnk'] = Template(force_unicode(xml_attributes['lnk']))
if 'cell_attributes' in xml_attributes:
xml_attributes['tpl_cell_attributes'] = Template(force_unicode(xml_attributes['cell_attributes']))
field_settings[field_name] = xml_attributes
# Выводимые на экран колонки
fields_visible = []
for index, field_name in enumerate(fields):
settings = field_settings.get(field_name, {})
if 'display' in settings and settings['display'] == '0':
continue
fields_visible.append((index, field_name, settings))
# Вычислить параметры и привязать к данным
rows = []
for row in data:
row_dict = dict(zip(fields, row))
row_settings = {}
# Настройки уровня строки
if 'field_row_style' in table_settings:
row_settings['row_style'] = row_dict[table_settings['field_row_style']]
if 'field_row_attributes' in table_settings:
row_settings['row_attributes'] = row_dict[table_settings['field_row_attributes']]
# Перебрать строки и собрать настройки уровня строки
fields_set = []
for index, field_name, settings in fields_visible:
field = {'name': field_name, 'value': row[index]}
# Если есть ссылка и она должна быть показана
if 'tpl_lnk' in settings and ('lnk_enable_field' not in settings or row_dict[settings['lnk_enable_field']] not in (0, '0', '', None)):
field['lnk'] = self._env.render_template_compiled(settings['tpl_lnk'], row=row_dict)
# Аттрибуты ячейки
if 'tpl_cell_attributes' in settings:
field['cell_attributes'] = settings['tpl_cell_attributes'].render(Context(row_dict))
field['settings'] = settings
fields_set.append(field)
rows.append({'settings': row_settings, 'fields': fields_set})
return render_to_string('reports_widget_table.html', {'fields': fields_visible, 'rows': rows, 'widget_obj': self._widget_obj})
def __call__(self):
u''' При вызове, возвращает таблицу '''
try:
return self.get_html()
except Exception as e:
return u'Ошибка в виджете %s: "%s"' % (self._widget_obj.title, e)
Берет данные из источника данных и выводит в табличном виде. Поддерживает
— форматирование
— Генерацию ссылок (которые могут использоваться для детализации ячейки в другой таблице, график или excel)
— Генерацию произвольных аттрибутов строк (для работы javascript, скрытия или показа итоговых строк и т.п.)
class ReportManager(object):
u''' Управляет отчетами '''
def __init__(self, request, report):
self._report = report
self._request = request
self._user = request.user
self._forms = {}
self._env = EnvirementManager(request=request, user_params=self._get_user_params(), forms={}, f={})
self._datasets = {}
self._widgets_table = {}
self._widgets_chart = {}
self._load_stored_filter_values()
# Сборка фильтров
for filter_obj in self._report.forms.only('title', 'html_layout'):
filter_manager = FilterManager(self._request, filter_obj, self._env)
self._save_stored_filter_values(filter_manager.get_changed_data())
self._forms[filter_obj.title] = filter_manager
self._env['forms'] = self._forms
# Собираем источники данных
for ds in self._report.datasets.only('sql', 'title', 'xml_settings'):
self._datasets[ds.title] = DatasetManager(request, ds, self._env)
self._env['datasets'] = self._datasets
# Собираем виджет-таблицы
for widget_obj in self._report.widgets_table.only('title', 'dataset', 'table_header', 'xml_settings'):
self._widgets_table[widget_obj.title] = WidgetTableManager(self._request,
widget_obj,
self._datasets[widget_obj.dataset.title],
self._env)
self._env['tables'] = self._widgets_table
# Виджеты - графики
for chart_obj in self._report.widgets_chart.only('title', 'dataset', 'xml_settings'):
self._widgets_chart[chart_obj.title] = WidgetChartManager(self._request,
chart_obj,
self._datasets[chart_obj.dataset.title],
self._env)
self._env['charts'] = self._widgets_chart
def get_request(self):
u''' Результат отчета '''
response_type = self._request.REQUEST.get('response_type', 'html')
if response_type == 'xls':
return self._get_request_xls(self._request.REQUEST['xls'])
elif response_type == 'template':
return self._get_request_template(self._request.REQUEST['template'])
else:
return self._get_request_html()
def _get_request_html(self):
u''' Вернуть результат в виде html для вывода на экран '''
context = {'favorite_reports': self._user.reports_favorited.all()}
context['report_body'] = self._env.render(self._report.html_layout)
context['breadcrumbs'] = (('Отчеты', reverse('reports_home')), (self._report.title, None))
context['filter_presets'] = self._report.filter_presets.filter(user=self._user)
context['report'] = self._report
return render(self._request, 'reports_report.html', context)
def _get_request_template(self):
u''' Вернуть результат в виде html для вывода на экран '''
# TODO
raise NotImplementedError(u'Не реализовано')
def _get_request_xls(self, dataset_title):
u""" Вернуть результат выгрузки в xls """
dataset = self._datasets[dataset_title]
(columns, data) = dataset.get_data()
w = Workbook(optimized_write=True)
sheet = w.create_sheet(0)
sheet.append(columns)
rows_in_sheet = 0
for row in data:
if rows_in_sheet > 1000000:
sheet = w.create_sheet()
sheet.append(columns)
rows_in_sheet = 0
sheet.append(row)
rows_in_sheet += 1
try:
tmpFileName = os.tempnam()
w.save(tmpFileName)
fh = open(tmpFileName, 'rb')
resp = HttpResponse(fh.read(), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
finally:
if fh:
fh.close()
os.unlink(tmpFileName)
resp['Content-Disposition'] = 'attachment; filename="Выгрузка.xlsx"'
return resp
def _get_request_document_template(self, template_title):
u""" Вернуть ответ, сгенерировав документ по шаблону """
pass
def _save_stored_filter_values(self, values):
u"""
Записать значения фильтров в базу и в _env
Если результат не html, то в базу ничего не сохраняем,
потому что не происходит перезагрузки страницы и не нужно восстанавливать значения фильтров
"""
for key, value in values.items():
self._env['f'][key] = value
if values.get('response_type', 'html') == 'html':
(store_item, is_created) = models.WidgetFormStorage.objects.get_or_create(user=self._user, report=self._report, key=key)
store_item.value = pickle.dumps(value)
store_item.save()
def _load_stored_filter_values(self):
u""" Загрузить значения форм, считав их из базы данных """
for item in self._report.form_storage.all():
self._env['f'][item.key] = pickle.loads(item.value)
def _get_user_params(self):
u""" Вернуть параметры пользователя """
params = {}
try:
param_string = models.UserAccess.objects.get(report=self._report, user=self._user).params
if param_string:
for pair in param_string.split(';'):
(key, values_str) = pair.split('=')
values = values_str.split(',')
params[key] = values
except Exception, e:
pass
return params
Имеет только один публичный метод, который возвращает httpResponse (html, вложение или xlsx)
Вот, в общем-то и все.
Интерфейс администратора описывать не стал.
css от bootstrap
Немного практики и картинок
select key, value, value * 0.5 as value1 from (
select 1 as key, 2000 as value union all
select 2 as key, 4000 as value union all
select 3 as key, 6000 as value union all
select 4 as key, 3000 as value union all
select 5 as key, 2000 as value union all
select 6 as key, 1000 as value
) t
{% if f.doble_rows %}cross join (select 1 union all select 2) t1 {% endif %}
select
t.key as key,
t.value as value,
case when key = 1 then 'background-color: #dff0d8;' end as row_style_field,
case when key = 2 then 'class="error"' end as row_attribute_field
{% if f.add_column %}
{% for row in datasets.ds.data %}
, {{row.key}} as Поле{{row.key}}
{% endfor %}
{% endif %}
from ({{datasets.ds.sql}}) t
{% if request.GET.key %} where key = [[request.GET.key]] {% endif %}
{% if f.limit %} limit [[f.limit]] {% endif %}
Табличный виджет t:
Создадим, выбрав из списка источник ds1
и прописав название. Все остальное не трогаем.
<xml>
<table field_row_style='row_style_field' field_row_attributes='row_attribute_field'/>
<fields>
<field name='Ключ' classes='text-right'/>
<field name='value' classes='text-right' lnk='?response_type=xls&xls=ds&key=[[row.Ключ]]&from=value'/>
{% for row in datasets.ds.data %}
<field name='Поле{{row.key}}' classes='text-right' lnk='?response_type=xls&xls=ds1&key=[[row.Ключ]]&from=Поле{{row.key}}'/>
{% endfor %}
<field name='Поле1' display='0'/>
<field name='row_style_field' display='0'/>
<field name='row_attribute_field' display='0'/>
</fields>
</xml>
<xml>
<table field_row_style='row_style_field' field_row_attributes='row_attribute_field'/>
<fields>
<field name='Ключ' classes='text-right'/>
<field name='value' classes='text-right' lnk='?response_type=xls&xls=ds&key=[[row.Ключ]]&from=value'/>
{% for row in datasets.ds.data %}
<field name='Поле{{row.key}}' classes='text-right' lnk='?response_type=xls&xls=ds1&key=[[row.Ключ]]&from=Поле{{row.key}}'/>
{% endfor %}
<field name='Поле1' display='0'/>
<field name='row_style_field' display='0'/>
<field name='row_attribute_field' display='0'/>
</fields>
</xml>
<xml>
<chart>
<categoryField>key</categoryField>
<marginTop>32</marginTop>
</chart>
<categoryAxis>
<labelsEnabled>true</labelsEnabled>
<gridCount>50</gridCount>
<equalSpacing>true</equalSpacing>
</categoryAxis>
<valueAxis>
<valueAxisLeft>
<stackType>regular</stackType>
<gridAlpha>0.07</gridAlpha>
</valueAxisLeft>
</valueAxis>
<cursor>
<bulletsEnabled>true</bulletsEnabled>
</cursor>
<graphs>
<graph>
<type>column</type>
<title>Отказы</title>
<valueField>value</valueField>
<balloonText>[[category]] дней: [[value]] шт.</balloonText>
<lineAlpha>0</lineAlpha>
<fillAlphas>0.6</fillAlphas>
</graph>
<graph1>
<type>column</type>
<title>Не отказы</title>
<valueField>value1</valueField>
<balloonText>[[category]] дней: [[value]] шт.</balloonText>
<lineAlpha>0</lineAlpha>
<fillAlphas>0.6</fillAlphas>
</graph1>
</graphs>
</xml>
<xml>
<chart>
<categoryField>key</categoryField>
<marginTop>32</marginTop>
</chart>
<categoryAxis>
<labelsEnabled>true</labelsEnabled>
<gridCount>50</gridCount>
<equalSpacing>true</equalSpacing>
</categoryAxis>
<valueAxis>
<valueAxisLeft>
<gridAlpha>0.07</gridAlpha>
</valueAxisLeft>
</valueAxis>
<cursor>
<bulletsEnabled>true</bulletsEnabled>
</cursor>
<graphs>
<graph>
<type>column</type>
<title>Отказы</title>
<valueField>value</valueField>
<balloonText>[[category]] дней: [[value]] шт.</balloonText>
<lineAlpha>0</lineAlpha>
<fillAlphas>0.6</fillAlphas>
</graph>
<graph1>
<type>column</type>
<title>Не отказы</title>
<valueField>value1</valueField>
<balloonText>[[category]] дней: [[value]] шт.</balloonText>
<lineAlpha>0</lineAlpha>
<fillAlphas>0.6</fillAlphas>
</graph1>
<graph2>
<type>smoothedLine</type>
<title>Не отказы</title>
<valueField>value1</valueField>
</graph2>
</graphs>
</xml>
<h2>Работа с источниками данных</h2>
<div class='well well-small'>{{forms.f}}</div>
<h3>После пре-процессора django template</h3>
<pre>
sql:{{datasets.ds1.render.sql}}
параметры:{{datasets.ds1.render.binds}}
</pre>
<h3>После выполнения</h3>
{{tables.t}}
<a class='btn btn-success' href='?response_type=xls&xls=ds1'>В Excel</a>
<h2>Визуализация данных</h2>
{{tables.t1}}
<div style='height:500px;width:500px;'>{{charts.test}}</div>
<div style='height:500px;width:500px;'>{{charts.test1}}</div>
Наш отчет:
Хочу сказать спасибо за помощь Павлу, Александру, Евгению.
Спасибо за внимание. Если хотите, можете взять это из репозитория и использовать по своему усмотрению.
bitbucket.org/dibrovsd/py_docflow/
P.S. в репозитории есть еще практически законченное приложение документооборота,
но о нем немного позже, если вам это будет интересно.
P.P.S.
Наверняка найдется множество не оптимальных решений, с python и django я знаком недавно.
Все конструктивные предложения прошу писать в «личку».
Автор: dibrovsd