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