![](http://habrastorage.org/getpro/habr/post_images/3bf/3a2/567/3bf3a256702850a7fb4b0217f4b5dbf9.jpg)
Все знают что 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/
Добавить комментарий