Встала у нас задача сделать движок для статистической отчетности на Django. Мы создали селекторы для получения данных из Oracle и виджеты для отображения этих данных в виде таблиц или графиков (с помощью HighChart). Но это всё чисто технологические решения, без особой магии. Если появятся интересующиеся, расскажу в отдельном посте. А сейчас хотелось бы обратить внимание на более необычную часть проекта. На предоставление составителям отчетов удобного способа эти отчеты составлять.
Здесь есть несколько моментов:
- С одной стороны, составители отчетов находятся с нами в одном отделе, то есть внутренности проекта им показывать в принципе можно. С другой стороны, они хорошо владеют SQL, чуть-чуть HTML и совсем никак Python’ом, и уж тем более не Django.
Значит, нужно по возможности избавить их от нагрузки на мозг в виде освоения архитектуры фреймворка. Кроме того, нужно поместить их творчество в песочницу, чтобы никакие ошибки не влияли на работоспособность системы в целом. - На каждой странице должно размещаться несколько отчетов в достаточно произвольном виде. Страниц очень много и они обычно никак не связаны между собой (ну разве что источниками в БД)
Если распихать логику одного отчета по разным файлам, то получим огромные файлы, по которым нужно искать отчёт по кусочкам.
А надо бы иметь возможность открыть «нечто» и увидеть перед собой всю логику построения отчета от и до.
- Нужна возможность оперативной правки отчета без перезагрузки Django.
- Желательно обеспечить возможность совместной работы и отслеживания изменений в отчетах.
Был вариант хранить настройки отчётов в базе. Но отслеживать изменения гораздо легче в системе управления версиями, чем в БД. К тому же, заранее было понятно, что движок будет развиваться, а менять схему данных, пожалуй, самое болезненное для любой системы.
Значит, файлы. Которые движок будет читать и что-то на их основе выполнять. Формат предполагался разный. И JSON, и ini, и выдумать какой-то свой. XML был отметён сразу, как трудночитаемый. Но в один из вечеров меня осенило – а чем сам Python плох? Настройка выглядит ничем не сложней, даже для человека незнакомого с языком совсем (разве что, две первые строки покажутся ему магическими):
# -*- coding: utf-8 -*- from statistics import OracleSelect, Chart, Table select_traf = OracleSelect('user/password@DB', """select DAY, NSS_TRAF, BSS_TRAF from DAY_TRAFFIC where DAY >= trunc(sysdate,'dd')-32""") chart_traf = Chart(selector=select_traf, x_column='DAY', y_columns=[('NSS_TRAF', u'NSS траффик'), ('BSS_TRAF', u'BSS траффик')]) table_traf = Table(selector=select_traf, columns=['DAY', 'NSS_TRAF', 'BSS_TRAF']) template = """ {{ chart_traf }} {{ table_traf }} """
На самом деле, для виджетов Chart и Table опций гораздо больше, но я не вижу смысла в демонстрационном коде перечислять их все.
Проще говоря, настроечный файл может представлять собой скрипт, который выполняется каждый раз при обращении к странице. Для Django такое поведение не свойственно, но мы заставим её это делать.
Надо сказать, что я впоследствии много раз поблагодарил себя за это решение. Не только потому что было легче добавлять новые фичи, но и потому что в особых случаях проблему стало возможно решить простым питонским хаком прямо в настроечном файле. Например, выполнять разные запросы в зависимости от условий, или генерить несколько однотипных графиков. Если бы конфиг был стандартизован как статичный файл, неизвестно, как бы такие вопросы решались. Но подозреваю, что весьма непросто. На каждый такой случай приходилось бы допиливать движок.
Чтение(выполнение) настроечного файла
Вот как в самом простом виде выглядит «интерпретатор» настроечных файлов.
import os from django.template import RequestContext, Template from django.http import HttpResponse, Http404 from settings import PROJECT_ROOT # корневая папка проекта, вычисляется из переменной __file__ в файле settings.py def dynamic_page(request, report_path): ctx_dict = {} execfile(os.path.join(PROJECT_ROOT, 'reports', report_path + '.py'), ctx_dict) templ_header = '{% extends "base.html" %}{% block content %}' templ_footer = '{% endblock %}' template = Template(templ_header + ctx_dict['template'] + templ_footer) context = RequestContext(request) context.autoescape = False context.update(ctx_dict) return HttpResponse(template.render(context))
Выполняем с помощью execfile настроечный файл. Все переменные созданные в скрипте будут находиться в словаре ctx_dict. Берём содержимое переменной template и составляем полноценный шаблон, в который передаём стандартный RequestContext и свежесозданный контекст из того же скрипта.
В urls.py добавляем
(r'^reports/(?P<report_path>.+)$', 'statistics.views.dynamic_page'),
Передача контекста в отчет и из отчета
Передача произвольного словаря в качестве пространства имён для исполняемого скрипта открывает интересные возможности.
Например, нам понадобилось в настроечном файле обращаться к get-параметрам запроса. Для этого нужно просто изменить ctx_dict перед тем как передать его в execfile
def dynamic_page(request, report_path): ctx_dict = {'get': request.GET.get} ...
Теперь в настроечном файле безо всяких импортов будет доступна функция get, которая достаёт значение нужного параметра из текущего request’а. Собственно, импорты тут бы и не помогли, поскольку request каждый раз новый.
В то же время, понадобилась и пост-обработка полученных из настроечного файла данных. Например, появилась необходимость присвоить каждому графику html-id в соответствии с его именем. Это нужно для того, чтобы в javascript напечаталось то же имя, что и в питоне (для взаимодействия графиков друг с другом). Конечно, можно это решить ещё одним параметром в Chart, но не очень кошерно постоянно писать что-то в стиле
chart_name = Chart(select, x_col, config, ..., html_id='chart_name')
Лучше уж не напрягать пользователей движка его внутренностями, а назначать нужные id автоматически, уже после формирования ctx_dict в execfile.
... execfile(os.path.join(PROJECT_ROOT, 'reports', report_path + '.py'), ctx_dict) for (name, obj) in ctx_dict.items(): if isinstance(obj, (Chart, Table)): obj.html_id = name ...
Есть ещё один интересный момент с ctx_dict. Так как все его значения попадают в контекст шаблона, они переписывают одноименные, переданные из RequestContext. Например, если какой-то контекстный процессор вычисляет значение ‘TITLE’ для помещения его в заголовок страницы, то вы можете в своём настроечном файле вычислить своё и оно будет выводиться вместо существующего
bs = get('bs') if bs is not None: TITLE = u'Трафик на БС %s' % bs
Понятно, что здесь есть и опасность ненамеренной перезаписи. Но это решается соглашением об именах (например, в контекстных процессорах использовать только верхний регистр, а в настроечных файлах только нижний).
Масштабирование до других url и базовых шаблонов
В конце концов дошло до того, что на Портале понадобилось размещать несколько разделов со статистикой. Разумеется, они были немного по разному оформлены и требовали немного разной логики, ну и нам самим было удобно хранить группы отчетов по отдельности.
Значит dynamic_page должен стать из простой вьюхи генератором вьюх. Что и было сделано.
import os from django.template import RequestContext, Template from django.http import HttpResponse, Http404 from settings import PROJECT_ROOT from functools import partial def get_param(request, key=None, default=None, as_list=False): if key: if as_list: return request.GET.getlist(key) else: return request.GET.get(key, default) else: return request.GET.lists() class DynamicPage(object): # Создание view def __init__(self, subpath, # Путь, от корня проекта, в котором нужно искать настроечные файлы parent_template = "base.html", load_tags = (), # список библиотек шаблонных тэгов block_name = 'content', pre_calc = lambda request, context: None, # заполнение контекста перед выполнением execfile post_calc = lambda request, context: None): # обработка контекста после выполнения execfile self.templ_header = ('{% extends "' + parent_template + '" %}' + DynamicPage.loading_tags(load_tags) + DynamicPage.block_top(block_name)) self.templ_footer = DynamicPage.block_foot(block_name) self.subpath = subpath self.pre_calc = pre_calc self.post_calc = post_calc @staticmethod def block_top(block_name): if block_name: return "{% block " + block_name + " %}" else: return '' @staticmethod def block_foot(block_name): if block_name: return "{% endblock %}" else: return '' @staticmethod def loading_tags(tags): return ''.join(['{% load ' + tag + ' %}' for tag in tags]) # Выполнение view def __call__(self, request, pagepath): ctx_dict = self.get_context(request, pagepath) if 'response' in ctx_dict and isinstance(ctx_dict['response'], HttpResponse): return ctx_dict['response'] # возможность возвращать напрямую response вместо обработки шаблона # Актуально для всякого рода экспортов, основывающихся на тех же вычислениях, что и html-страница else: template = Template(self.templ_header + ctx_dict['template'] + self.templ_footer) context = RequestContext(request) context.autoescape = False context.update(ctx_dict) return HttpResponse(template.render(context)) def get_context(self, request, pagepath): fullpath = os.path.join(PROJECT_ROOT, self.subpath, pagepath + '.py') if not os.path.exists(fullpath): raise Http404 ctx_dict = {'get': partial(get_param, request), 'request': request} self.pre_calc(request, ctx_dict) execfile(fullpath, ctx_dict) self.post_calc(request, ctx_dict) return ctx_dict
Это позволило создавать оболочки для разных разделов отчетности. Ими занимались программисты. Принципы же изготовления отчётов при этом не менялись.
Например, в одном случае понадобились упомянутые выше игры с html_id.
def add_html_id(request, context): for (name, obj) in context.items(): if isinstance(obj, (Chart, Table)): obj.html_id = name show_report = DynamicPage('stat_tech/pages', parent_template='stat_tech/base.html', load_tags=['adminmedia', 'jquery', 'chapters'], post_calc=add_html_id)
В другом, заполнять из настроечного файла не один блок шаблона, а два.
show_weekly = DynamicPage('stat_weekly/pages', parent_template = 'stat_weekly/base.html', load_tags = ['chapters', ' employees'], block_name=None)
В этом случае, блоки указываются в самом файле с отчётом
template = """ {% block chart %} {{ costs_monthly }} {{ costs_weekly }} {% endblock %} {% block responsible %} {% employee vasily_pupkin %}, {% employee ivan_ivanov %} {% endblock %} """
В третьем, узнавать подразделение, в котором работает текущий пользователь, чтобы на его основе определять, как и какие запросы выполнить, а так же, в каком виде показывать подменю.
def add_division(request, context): div = Division.get_by_user(request.user) context['DIVISION'] = div context['SUBMENU'] = calc_goal_submenu(request.path, div) show_goal = DynamicPage('stat_goals/pages', load_tags = ['chapters'], block_name='report', parent_template = 'stat_goals/base.html', pre_calc = add_division)
В urls все эти обертки добавляются как обычные вьюшки
(r'^stat/(?P<pagepath>.+)$', 'stat_tech.views.show_report'), (r'^weeklyreport/(?P<pagepath>.+)$', 'stat_weekly.views.show_weekly'), (r'^goals/(?P<pagepath>.+)$', 'stat_goals.views.show_goal'),
Вот такой мини-фреймворк поверх большого. Надеюсь, он удачно демонстрирует, что если рамки системы мешают решить вопрос простым образом, всегда можно слегка их раздвинуть, воспользовавшись более мощными возможностями языка.
ссылка на оригинал статьи http://habrahabr.ru/post/185180/
Добавить комментарий