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

ссылка на оригинал статьи http://habrahabr.ru/company/starttospeak/blog/228411/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *