Django widgets and a couple more tricks



    Everyone knows that Django is a great development framework with tons of powerful batteries. For me personally, when I first met django, everything seemed extremely convenient - everything was for the convenience of the developer, I thought. But those who are forced to work with him for a long time know that not everything is as fabulous as it seems to a beginner. As time went on, projects became larger, more difficult, writing views became uncomfortable, and understanding the relationship of models became harder and harder. But work is work, the project was large and complex, and, above all, it was necessary to have a page management system like in cms, and, it seems, there is a wonderful django cms, to which all you need to write plugins. But it turned out that you can make the whole process somewhat more convenient by adding a couple of features and some code.

    In this short article, you will not see ready-made recipes; the purpose of the article is to share your ideas. The code examples serve only to help explain the key elements, without significant improvements this will not be enough to repeat the functionality. But if the topic turns out to be interesting, then it will be possible to continue in the next article. Or even put it in open source.

    Models


    Suppose we have 2 models with common fields: title, description and tags. If we just need to display in the feed the latest materials from both models sorted by creation date, then the easiest way is to combine them into one model. And so that in the admin panel they do not merge into one entity, we can use the Generic Foreign Key .
    For the admin panel, configure inline editing of Info and immediately add GFKManager - a snippet for optimizing queries:

    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
    


    Keep in mind that you may get an error when deleting objects of models A and B, if you use generic.GenericRelation. Unfortunately I can not find the source:
    # -*- 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"])
    


    Now you can easily execute the query:
    Info.objects.filter(content_type__in=(CT.models.A, CT.models.B))
    


    for convenience, I use a ContentType map:
    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()
    


    If we need to organize a search (sphinx) then we can connect django-sphinx to Info. Now with one request we can get a feed, search, filter by tags, etc. The disadvantage of this approach is that all the fields by which it is necessary to filter requests should be stored in Info, and in the models themselves only those fields by which the filter is not needed, for example, pictures.

    Django CMS, plugins and widgets


    With the help of CMS we can add new pages, edit and delete old ones, add widgets to the page, create sidebars and so on. But sometimes, or rather, quite often there is a need to permanently add a plug-in to a template so that it is visible on all pages. django widgets is a solution to our problems, with the help of the include_widget tag we can add everything we need and where we need it. Even more often, you need to get some data into the plugin with ajax. We use 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%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 []
    

    Having passed the widget and template name parameters in the request, we can get the rendered context. Here I use the variable PLUGIN_TEMPLATE_MAP so that I can only pass the name of the template.

    It remains to connect widgets and plugins. Here is a rather large piece, but the most important.
    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())
    


    Some more tasty batteries




    Well, quite commonplace things:


    Conclusion


    I tried to explain two key points that can be simplified in working with django, I wanted to explain more, but the article is too lengthy. Other interesting points are the processing and generation of dynamic URLs, as well as two main widgets - the ribbon widget and the entity widget, but this is the next time. So, with this concept, I
    • I create new models and add them to the feed in a couple of minutes (when there are about 50 such feeds on the project, it matters);
    • I never write views, I customize widgets, occasionally write new ones;
    • I do not create new templates for url, django cms does it for me;
    • I'm not worried about ajax, I just pass the parameters and get the result;
    • made life easier, on three projects among which one is very large;
    • I spend a lot more time on js than on django, but that's another story.


    Thanks for attention!

    Also popular now: