Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Когда встроенного MVC не хватает

Anna | 16.06.2014 | нет комментариев
Одним из основных превосходств фреймворков является их предопределённая зодчество. Открываешь неизвестный план и сразу знаешь, где и как искать код связи с БД, либо HTML, либо схему url. Помимо того, она разрешает разработчику не задумываться над конструкцией хранения кода и при этом быть уверенным, что план будет выглядеть больше менее адекватно. Но хочу рассказать о случае, когда реализация MVC в Django, а именно разделение логики по файлам models, forms, views, templates оказалась неудобной и какую на её основе возвели альтернативу.

Встала у нас задача сделать движок для статистической отчетности на Django. Мы сделали селекторы для приобретения данных из Oracle и виджеты для отображения этих данных в виде таблиц либо графиков (с поддержкой HighChart). Но это всё чисто технологические решения, без специальной магии. Если появятся интересующиеся, расскажу в отдельном посте. А теперь хотелось бы обратить внимание на больше странную часть плана. На предоставление составителям отчетов комфортного метода эти отчеты составлять.

Тут есть несколько моментов:

  1. С одной стороны, составители отчетов находятся с нами в одном отделе, то есть внутренности плана им показывать в тезисе дозволено. С иной стороны, они отлично обладают SQL, чуть-чуть HTML и вовсе никак Python’ом, и уж тем больше не Django.
    Значит, необходимо по вероятности избавить их от нагрузки на мозг в виде освоения архитектуры фреймворка. Помимо того, необходимо разместить их созидание в песочницу, Дабы никакие ошибки не влияли на работоспособность системы в целом.
  2. На всякой странице должно размещаться несколько отчетов в довольно произвольном виде. Страниц дюже много и они обыкновенно никак не связаны между собой (ну разве что источниками в БД)
    Если распихать логику одного отчета по различным файлам, то получим громадные файлы, по которым необходимо искать отчёт по ломтикам.

    А нужно бы иметь вероятность открыть «кое-что» и увидеть перед собой всю логику построения отчета от и до.

  3. Необходима вероятность оперативной правки отчета без перезагрузки Django.
  4. Желанно обеспечить вероятность совместной работы и отслеживания изменений в отчетах.

Был вариант беречь настройки отчётов в базе. Но отслеживать метаморфозы значительно легче в системе управления версиями, чем в БД. К тому же, предварительно было ясно, что движок будет прогрессировать, а менять схему данных, вероятно, самое болезненное для всякий системы.
Значит, файлы. Которые движок будет читать и что-то на их основе исполнять. Формат предполагался различный. И 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])

    @property
    def __name__(self):
        return self.__class__.__name__

    # Выполнение 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'),

Вот такой мини-фреймворк поверх большого. Верю, он успешно показывает, что если рамки системы мешают решить вопрос простым образом, неизменно дозволено слегка их раздвинуть, воспользовавшись больше сильными вероятностями языка.

UPD: По предложению magic4x, в DynamicPage добавлено качество __name__, усиливающее мимикрию под функцию.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB