Сервер отчетов на django

от автора

Доброго времени суток.

Так случилось, что моя работа связана с написанием отчетов.
Этому я посвятил около 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

Немного практики и картинок

Источник данных ds

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 %} 

Источник данных ds1

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
и прописав название. Все остальное не трогаем.

Таблица t1 (на 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> 

Таблица t1 (на 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> 

График test (на ds)

<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> 

График test1 (на ds)

<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 я знаком недавно.
Все конструктивные предложения прошу писать в «личку».

ссылка на оригинал статьи http://habrahabr.ru/post/177567/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *