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, хотел объяснить больше, но статья получается слишком объемной. Другие интересные моменты это обработка и формирование динамических урл, а также два основных виджета — виджет ленты и виджет сущности, но это в следующий раз. Итак, при помощи данного концепта я
- создаю новые модели и добавляю их в ленту за пару минут (когда таких лент на проекте около 50 это имеет значение);
- никогда не пишу вьюшки, я настраиваю виджеты, изредка пишу новые;
- не создаю новые шаблоны для url, за меня это делает django cms;
- не парюсь с ajax, я просто передаю параметры, и получаю результат;
- облегчил себе жизнь, на трех проектах среди которых один очень большой;
- трачу намного больше времени на js чем на django, но это уже совсем другая история.
Спасибо за внимание!