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



    Все знают что 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, но это уже совсем другая история.


    Спасибо за внимание!
    Starttospeak.com
    Компания
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 3

      0
      А что скажите по поводу проекта dajax? На мой взгляд для работы с ajax самое то.
        0
        К сожалению не знал об этом проекте, возьму на заметку, спасибо.
          0
          Только вот что автор говорит теперь о своем проекте —

          Should I use django-dajax or django-dajaxice?

          In a word, No. I created these projects 4 years ago as a cool tool in order to solve one specific problems I had at that time.

          These days using these projects is a bad idea.

          Perhaps I'm more pragmatic now, perhaps my vision of how my django projects should be coupled to libraries like these has change, or perhaps these days I really treasure the purity and simplicity of a vanilla django development.

          If you want to use this project, you are probably wrong. You should stop couplig your interface with your backend or… in the long term it will explode in your face.

          Forget about adding more unnecessary complexity. Keep things simple.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое