python

Django widgets и еще пара трюков

  • четверг, 3 июля 2014 г. в 03:10:40
http://habrahabr.ru/company/starttospeak/blog/228411/



Все знают что Django это замечательный фреймворк для разработки, с кучей мощных батареек. Лично для меня, при первом знакомстве с django все казалось крайней удобным — все для удобства разработчика, думалось мне. Но те кто с ним вынужден работать в течении долгого времени, знают, что не все так сказочно, как кажется новичку. Шло время проекты становились больше, сложнее, писать вьюшки стало неудобным, а разбираться во взаимоотношении моделей становилось сложнее и сложнее. Но работа есть работа, проект был большой и сложный, и, ко всему прочему необходимо было иметь систему управления страниц как в cms, и, вроде бы, есть замечательный django cms, к которому всего и надо что написать плагинов. Но оказалось, что можно сделать весь процесс несколько более удобным, добавив пару фич и немного кода.

В этой небольшой статье, вы не увидите готовых рецептов, цель статьи — поделиться своими задумками. Примеры кода служат лишь для того, чтобы помочь в объяснении ключевых элементов, без существенных доработок этого будет недостаточно для повторения функционала. Но если тема окажется интересной, то можно будет продолжить в следующей статье. Или даже выложить в опенсорс.

Модели


Предположим у нас есть 2 модели с общими полями: заголовок, описание и теги. Если нам надо просто вывести в ленту последние материалы из обоих моделей отсортированные по дате создания, то самый простой способ — это объединить их в одну модель. А для того, чтобы в админке они не сливались в одну сущность, мы можем использовать Generic Foreign Key.
Для админки настроим inline редактирование Info и сразу добавим GFKManager — сниппет для оптимизации запросов:

from django.db import models
from core.container.manager import GFKManager

class Info(models.Model):
    objects = GFKManager()

    title = models.CharField(
        max_length=256, blank=True, null=True
    )
    header = models.TextField(
        max_length=500, blank=True, null=True
    )
    tags = models.ManyToManyField(
        'self', symmetrical=False, blank=True, null=True
    )
    def content_type_name(self):
        return self.content_type.model_class()._meta.verbose_name

class Model(models.Model):
    info = CustomGenericRelation(
        'Info',
        related_name="%(class)s_info"
    )

class A(Model):
    field = models.CharField(
        max_length=256, blank=True, null=True
    )

class B(Model):
    pass


Имейте ввиду что вы можете получить ошибку при удалении объектов моделей A и B, если использовать generic.GenericRelation. К сожалению не могу найти первоисточник:
# -*- coding: utf-8 -*-
from django.contrib.contenttypes import generic
from django.db.models.related import RelatedObject
from south.modelsinspector import add_introspection_rules


class CustomGenericRelation(generic.GenericRelation):
    def contribute_to_related_class(self, cls, related):
        super(CustomGenericRelation, self).contribute_to_related_class(cls, related)
        if self.rel.related_name and not hasattr(self.model, self.rel.related_name):
            rel_obj = RelatedObject(cls, self.model, self.rel.related_name)
            setattr(cls, self.rel.related_name, rel_obj)


add_introspection_rules([
    (
        [CustomGenericRelation],
        [],
        {},
    ),
], ["^core\.ext\.fields\.generic\.CustomGenericRelation"])


теперь можно легко выполнить запрос:
Info.objects.filter(content_type__in=(CT.models.A, CT.models.B))


для удобства я использую карту ContentType:
rom django.contrib.contenttypes.models import ContentType
from django.db import models
from models import Model

class Inner(object):
    def __get__(self, name):
        return getattr(self.name)


class ContentTypeMap(object):
    __raw__ = {}

    def __get__(self, obj, addr):
        path = addr.pop(0)
        if not hasattr(obj, path):
            setattr(obj, path, type(path, (object,), {'parent': obj}))
        attr = getattr(obj, path)
        return self.__get__(attr, addr) if addr else attr

    def __init__(self):
        for model in filter(lambda X: issubclass(X, Model), models.get_models()):
            content_type = ContentType.objects.get_for_model(model)
            obj = self.__get__(self, model.__module__.split('.'))
            self.__raw__[content_type.model] = content_type.id
            setattr(obj, '%s' % model.__name__, content_type)
        for obj in map(lambda X: self.__get__(self, X.__module__.split('.')),
            filter(lambda X: issubclass(X, Model), models.get_models())):
            setattr(obj.parent, obj.__name__, obj())


CT = ContentTypeMap()


Если нам надо организовать поиск (sphinx) то мы можем подключить django-sphinx к Info. Теперь одним запросом мы можем получить ленту, поиск, выборку по тегам и тд. Минус такого подхода в том, что все поля по которым необходимо фильтровать запросы должны хранится в Info, а в сами модели только те поля по которым фильтр не нужен, например картинки.

Django CMS, плагины и виджеты


При помощи CMS мы можем добавлять новые страницы, редактировать и удалять старые, добавлять на страницу виджеты, формировать сайдбары и так далее. Но иногда, а точнее, довольно часто есть необходимость перманентно добавить плагин в шаблон, так чтобы он был виден на всех страницах. django widgets — решение наших проблем, при помощи тега include_widget мы сможем добавить все, что нам нужно, и куда нужно. Еще более часто необходимо получать ajax'ом какие то данные в плагин. Воспользуемся tastypie.

from django.conf.urls.defaults import *
from django.http import HttpResponseForbidden
from django_widgets.loading import registry
from sekizai.context import SekizaiContext
from tastypie.resources import Resource
from tastypie.utils import trailing_slash
from tastypie.serializers import Serializer
from core.widgets.cms_plugins import PLUGIN_TEMPLATE_MAP
from core.ext.decorator import api_require_request_parameters


class HtmlSreializer(Serializer):
    def to_html(self, data, options=None):
        return data


class WidgetResource(Resource):
    class Meta:
        resource_name = 'widget'
        include_resource_uri = False
        serializer = HtmlSreializer(formats=['html'])

    def prepend_urls(self):
        return [
            url(r"^(?P<resource_name>%s)/render%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('render'), name="api_render")
        ]

    @api_require_request_parameters(['template'])
    def render(self, request, **kwargs):
        data = dict(request.GET)
        template = data.pop('template')[0]
        if 'widget' in data:
            widget = registry.get(data.pop('widget')[0])
        else:
            if template not in PLUGIN_TEMPLATE_MAP:
                return  HttpResponseForbidden()
            widget = PLUGIN_TEMPLATE_MAP[template]

        data = dict(map(lambda (K, V): (K.rstrip('[]'), V) if K.endswith('[]') else (K.rstrip('[]'), V[0]), data.items()))
        return self.create_response(
            request,
            widget.render(SekizaiContext({'request': request}), template, data, relative_template_path=False)
        )

    def obj_get_list(self, bundle, **kwargs):
        return []

Передав в запросе параметры названия виджета и шаблона, мы можем получить отрендереный контекст. Тут я использую переменную PLUGIN_TEMPLATE_MAP так, чтобы иметь возможность передавать только название шаблона.

Остается связать виджеты и плагины. Тут довольно большой кусок, но самый важный.
import os
import json
from django import forms
from django.conf import settings
from django_widgets.loading import registry
from cms.models import CMSPlugin
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from core.widgets.widgets import ItemWidget


PLUGIN_MAP = {}
PLUGIN_CT_MAP = {}
PLUGIN_TEMPLATE_MAP = {}


class PluginWrapper(CMSPluginBase):
    admin_preview = False

class FormWrapper(forms.ModelForm):
    widget = None
    templates_available = ()

    def __init__(self, *args, **kwargs):
        super(FormWrapper, self).__init__(*args, **kwargs)
        if not self.fields['template'].initial:
            # TODO
            self.fields['template'].initial = self.widget.default_template
            self.fields['template'].help_text = 'at PROJECT_ROOT/templates/%s' % self.widget.get_template_folder()

            if self.templates_available:
                self.fields['template'].widget = forms.Select()
                self.fields['template'].widget.choices = self.templates_available


        self.__extra_fields__ = set(self.fields.keys()) - set(self._meta.model._meta.get_all_field_names())

        data = json.loads(self.instance.data or '{}') if self.instance else {}
        for key, value in data.items():
            self.fields[key].initial = value

    def clean(self):
        cleaned_data = super(FormWrapper, self).clean()
        cleaned_data['data'] = json.dumps(dict(
            map(
                lambda K: (K, cleaned_data[K]),
                filter(
                    lambda K: K in cleaned_data,
                    self.__extra_fields__
                )
            )
        ))

        return cleaned_data

    class Meta:
        model = CMSPlugin
        widgets = {
            'data': forms.HiddenInput()
        }


def get_templates_available(widget):
    template_folder = widget.get_template_folder()
    real_folder = os.path.join(settings.TEMPLATE_DIRS[0], *template_folder.split('/'))
    result = ()

    if os.path.exists(real_folder):
        for path, dirs, files in os.walk(real_folder):
            if path == real_folder:
                choices = filter(lambda filename: filename.endswith('html'), files)
                result = zip(choices, choices)
            rel_folder =  '%(template_folder)s%(inner_path)s' % {
                'template_folder': template_folder,
                'inner_path': path.replace(real_folder, '')
            }
            for filename in files:
                PLUGIN_TEMPLATE_MAP['/'.join((rel_folder, filename))] = widget
    return result


def register_plugin(widget, plugin):
    plugin_pool.register_plugin(plugin)
    PLUGIN_MAP[widget.__class__] = plugin

    if issubclass(widget.__class__, ItemWidget):
        for content_type in widget.__class__.content_types:
            if content_type not in PLUGIN_CT_MAP:
                PLUGIN_CT_MAP[content_type] = []
            PLUGIN_CT_MAP[content_type].append(plugin)

def get_plugin_form(widget, widget_name):
    return type('FormFor%s' % widget_name, (FormWrapper,), dict(map(
        lambda (key, options): (key, (options.pop('field') if 'field' in options else forms.CharField)(initial=getattr(widget, key, None), **options)),
        getattr(widget, 'kwargs', {}).items()
    ) + [('widget', widget), ('templates_available', get_templates_available(widget))]))

def register_plugins(widgets):
    for widget_name, widget in widgets:
        if getattr(widget, 'registered', False):
            continue
        name = 'PluginFor%s' % widget_name
        plugin = type(
            name, (PluginWrapper,),
            {
                'name': getattr(widget, 'name', widget_name),
                'widget': widget,
                'form': get_plugin_form(widget, widget_name)
            }
        )
        register_plugin(widget, plugin)

register_plugins(registry.widgets.items())


Еще немного вкусных батареек


  • django-sekizai — зависимость django cms, но, разумеется, можно использовать и без него
  • django-localeurl — удобные штуки для интернационального сайта
  • django-modeltranslation — как вариант, но есть не менее вкусные альтернативы
  • django-redis-cache — кеш в редисе, туда же можно засунуть и сессии, особенно полезно если вы годами не чистите сессии из MySQL
  • django-admin-bootstrapped — более современная админка, (надо поставить bootstrap-modeltranslation если используете modeltranslation )
  • django-sorl-cropping — для работы с thumbnail


Ну и совсем банальные вещи:


Заключение


Я постарался объяснить два ключевых момента, которые можно упростить в работе с django, хотел объяснить больше, но статья получается слишком объемной. Другие интересные моменты это обработка и формирование динамических урл, а также два основных виджета — виджет ленты и виджет сущности, но это в следующий раз. Итак, при помощи данного концепта я
  • создаю новые модели и добавляю их в ленту за пару минут (когда таких лент на проекте около 50 это имеет значение);
  • никогда не пишу вьюшки, я настраиваю виджеты, изредка пишу новые;
  • не создаю новые шаблоны для url, за меня это делает django cms;
  • не парюсь с ajax, я просто передаю параметры, и получаю результат;
  • облегчил себе жизнь, на трех проектах среди которых один очень большой;
  • трачу намного больше времени на js чем на django, но это уже совсем другая история.


Спасибо за внимание!