Так случилось, что моя работа связана с написанием отчетов.
Этому я посвятил около 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 я знаком недавно.
Все конструктивные предложения прошу писать в «личку».
ссылка на оригинал статьи http://habrahabr.ru/post/177567/
Добавить комментарий