Django tips & tricks
Приветствую! В этом посте — небольшие советы по работе с Django, которые могут пригодиться начинающим разработчикам. Как я хотел бы знать это в начале моего пути освоения Django!…Рассматривать эти советы следует с долей критицизма. Буду рад, если вы найдёте неточности / лучшее решение, или предложите свои «фишки» для django, которых нет в документации.
Итак, начнём издалека, а уж потом перейдём к деталям.
Виртуальное окружениеЕсли вы не используете virtualenv для вашего django-приложения — то обязательно попробуйте.Если вы уже используете virtualenv, то ответьте, нужен ли вам --no-site-packages. Этот флаг по умолчанию включён и используется при создании виртуального окружения. При включённом флаге программы «внутри» окружения не увидят программы «снаружи». Если вы поставите вашим пакетным менеджером какой-нибудь пакет глобально, например, python2-django, то «внутри» окружения всё равно придётся делать pip install django.
Зачем могут понадобиться глобально установленные пакеты? Я столкнулся с этим, когда настраивал поисковый движок на xapian. Xapian идёт в поставке xapian-core (написан на C++) и xapian-bindings (обвязка для разных ЯП, в том числе python). Логично их обновлять одновременно — если изменился движок, то и обвязки надо обновить. Поэтому ставить xapian-core глобально пакетным менеджером, а обвязки через pip не устравивает (к тому же, их нет в pip). Выхода 2: Создать помойку внутри virtualenv: ./configure --prefix=/path/to/virtualenv && make && make install Сделать глобальные пакеты видимыми извне и обновлять их пакетным менеджером дистрибутива, что я и выбрал Вообще, когда модуль написан на чистом питоне, проблем не возникает — устанавливаем через pip в virtualenv. Если модуль — это смесь, скажем, c++ и питона — начинается магия. Видимость / невидимость глобальных программ из virtualenv устанавливается отсутствуем / наличием файла [virtualenv]/lib/python*.*/no-global-site-packages.txt. Вот так просто.Кстати, рекомендую всем статью про «изолированность» virtualenv: Why I hate virtualenv and pip (сайт тормозит, смог открыть только через web.archive.org). В ней рассматривается, насколько virtualenv действительно изолирован от «внешней» среды — если кратко, то это лишь частичная изоляция.
ipython Pip install ipython заменит стандартный питоновский шелл на продвинутый, с раскрашиванием, автодополнением, интроспекцией, удобным многострочным вводом, копипейстом и т.д. Django автоматически подцепляет ipython, если он установлен.Кстати, все перечисленные достоинства можно использовать не только в ./manage.py shell, но и в дебаге, вызывая отладку с помощью import ipdb; ipdb.set_trace ().Структура проекта Django по умолчанию при создании проекта или приложения создаёт необходимые каталоги. Но и самим нужно думать.Как проект назовёшь, так и будешь импортировать Называйте ваш проект project (django-admin.py startproject project) — ну или другим, но одинаковым именем для всех проектов. Раньше я называл проекты соответственно домену, но при повторном использовании приложений в других проектах приходилось менять пути импорта — то from supersite import utils, то from newsite import utils. Это путает и отвлекает. Если расширить этот совет — зафиксируйте (унифицируйте) для себя структуру каталогов всех ваших проектов и строго её придерживайтесь.Живой пример:
--site.ru |--static |--media |--project (папка с проектом) |--manage.py |--project (папка с основным приложением) | |--settings.py | |--urls.py | |-- … |--app1 |--app2 |--… Куда сохранять html-шаблоны Никогда, никогда не кидайте шаблоны (.html) в папку templates вашего приложения. Всегда создавайте дополнительный каталог с названием, совпадающим с именем приложения.Вот это плохо, т.к. создаёт коллизию шаблонов, например, при {% include 'main.html' %}: /gallery/templates/main.html /reviews/templates/main.html Вот это — хорошо, можно использовать {% include 'reviews/main.html' %}: /gallery/templates/gallery/main.html /reviews/templates/reviews/main.html {% include %} К слову, если вы используете {% include 'some_template.html' %}, то велика вероятность, что что-то не так. Почему? Пример: def view (request): return render ( request, 'master.html', {'var': 'Some text'} } Value of variable var: {{ var }}. {% include 'slave.html' %}
Again, value of variable var: {{ var }}. 1) KISS едет лесом. С одной стороны, код страницы разбит на несколько — master.html и подключаемый slave.html, и это удобно для разделения больших html-страниц на части. Но в данном случае переменная var передаётся в шаблон slave.html неявно — var передатся в master.html, а slave.html просто «цепляет» контекст master’а. Таким образом, мы видим, что шаблон внутри {% include %} зависит от контекста основного шаблона. Мы вынуждены следить за контекстом родительского шаблона, иначе в дочерний может попасть что-нибудь не то.2) По моим наблюдениям, {% include %} дорогой в плане рендеринга. Лучше его избегать.
Что делать? Если очень хочется одни шаблоны включать в другие — используйте inclusion tags (о них читать ниже). Но проще — просто пишите всё в одном файле:
Value of variable var: {{ var }}. Again, value of variable var: {{ var }}. settings.py Вы же не имеете два разных settings.py на тестовом и деплой серверах, да? Создайте дополнительные local_settings.py и deployment_settings.py, куда скиньте всё, что относится только к соответствующему серверу.Вот, например, что логично задавать в local_settings.py DEBUG = True
DOMAIN = '127.0.0.1:8000' ALLOWED_HOSTS = ['127.0.0.1', DOMAIN]
SERVER_EMAIL = 'mail@test.ru' EMAIL_HOST = 'localhost' EMAIL_PORT = 1025 EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_USE_TLS = False EMAIL_SUBJECT_PREFIX = '[' + DOMAIN + '] '
DEFAULT_FROM_EMAIL = 'mail@test.ru'
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'test', 'USER': 'test', 'PASSWORD': 'test', 'HOST': 'localhost', 'PORT': '', 'ATOMIC_REQUESTS': True, } }
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', } } В settings.py пишем в начале:
# Load local settings if available try: from local_settings import * except ImportError: from deployment_settings import * Соответственно, на деплое удаляем local_settings.py. Чтобы он не мешался, его можно добавить в .gitignore.Корень проекта Задайте корень проекта в settings.py — это облегчит жизнь потом: from os import path BASE = path.dirname (path.dirname (path.dirname (path.abspath (__file__)))) MEDIA_ROOT = BASE + '/media/' STATIC_ROOT = BASE + '/static/' Контекстные процессоры (context_processors.py), {% include %} и inclusion tags Используйте контекстные процессоры только если вам нужно добавить переменные в контекст каждой страницы сайта — ведь контекстные процессоры будут вызываться для любой страницы, даже если вы не воспользуйтесь их результатами. Лично я использую их для передачи номера телефона в контекст шаблона — этот номер реально на каждой странице выводится, и не единожды. Ещё пример — меню сайта. Я прописал заголовки и ссылки в контекстном процессоре, и если мне нужно будет добавить новый раздел в меню — я просто добавлю его в контекстный процессор, и он автоматически добавится везде на сайте.Есть одна ошибка — использование контекстных процессоров для виджетов. Например, у вас на сайте есть колонка новостей, которая выводится всегда, т.е. на каждой страничке. Казалось бы, создать news/context_processors.py, и в контекст добавлять переменную news с новостями, а в шаблоне {% include 'news/news_widget.html' %}, или даже {% load news_widget %} {% news_widget news %}…
Это работает, но это замусоривает контекст и, кроме того, кто знает, всегда ли у вас будет эта колонка. Выход есть — используйте inclusion tag. Вы просто пишете в шаблоне {% news %}, а уже этот templatetag ищет новости и вставляет колонку новостей. И работает он только тогда, когда вы его реально запускаете — т.е. пишете {% news %} в шаблоне.
Батарейки django-debug-toolbar-template-timings Все его знают и, наверно, используют. Но есть django-debug-toolbar-template-timings — плагин к debug toolbar, который замеряет время рендеринга шаблонов. А учитывая, что шаблоны django довольно «дорогие» (рендерятся долго), то для ускорения сайта этот плагин — то что доктор прописал.adv_cache_tag django-adv-cache-tag позволяет очень гибко управлять кешированием в шаблонах — версионность, сжатие, частичное кэширование. Просто оцените: {% load adv_cache %} {% cache 0 object_cache_name object.pk obj.date_last_updated %} {{ obj }} {% nocache %} {{ now }} {% endnocache %} {{ obj.date_last_updated }} {% endcache %} django-mail-templated Шаблоны email писем — это то, чего не хватает django. django-mail-templateddjango-ipware django-ipware определит ip пользователя за вас, и сделает это лучше.Вы же знаете, откуда брать ip пользователя? 'HTTP_X_FORWARDED_FOR', # client, proxy1, proxy2 'HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_VIA', 'REMOTE_ADDR', Beautiful Soup Не пишите свой парсер html. Не парсите html сами. Всё уже есть.Templatetags, которые могут пригодиться add_class Если вы создаёте форму и хотите для каждого input-а задать стиль, класс или placeholder, то django заставит вас нарушить принципы и прописать все стили прямо в forms.py: class SomeForm (ModelForm): class Meta: model = SomeModel fields = ('field1', 'field2') widgets = { 'field1': Textarea (attrs={'rows': '2', 'class': 'field1_class'}), } Меня каждый раз коробит при виде html текста не в .html файлах. Это нарушает MVT архитектуру. Поэтому я создал для себя фильтр: {% load add_class %} {{ form.field1|add_class:'field1_class' }} Данный фильтр добавляет класс к тегам, но можно переписать и добавлять любое свойство.Код add_class.py from django import template from django.utils.safestring import mark_safe from bs4 import BeautifulSoup
register = template.Library () @register.filter def add_class (html, css_class): soup = BeautifulSoup (unicode (html), 'html.parser')
for tag in soup.children: if tag.name!= 'script': if 'class' in tag: tag['class'].append (css_class) else: tag['class'] = [css_class]
return mark_safe (soup.renderContents ()) is_current_page Иногда нужно что-то выводить в шаблоне, если открыта определённая страница. Например, подсветить кнопку «магазин» в меню, если пользователь сейчас в разделе магазина. Предлагаю следующий вариант: from django import template from django.core.urlresolvers import resolve from project.utils import parse_args
register = template.Library () @register.filter def is_current_page (request, param): return resolve (request.path).view_name == param Это фильтр, а не тэг, и причина тут одна: можно строить совершенно дичайшие конструкции с {% if %}. Например, если текущая страница — карточка товара, и при этом пользователь авторизован: {% if request|is_current_page:'shop/product' and user.is_authenticated %} Есть и альтернативная, более точная, реализация, в которой используются аргументы (args или kwargs) для определения точной страницы (т.е. не просто «страница какого-либо товара», а «страница товара с id=36»): {% if request|is_current_page:'shop/product, id=36' %} @register.filter def is_current_page (request, param): params = param.split (',') name = params[0] args, kwargs = parse_args (params[1:]) # Do not mix args and kwargs in reverse () — it is forbidden! if args: return request.path == reverse (name, args=args) elif kwargs: return request.path == reverse (name, kwargs=kwargs) else: return request.path == reverse (name)
Модели Пустые Модели могут быть пустыми. Вот так: class Phrase (models.Model): pass
class PhraseRu (models.Model): phrase = models.ForeignKey (Phrase, verbose_name='фраза', related_name='ru')
class PhraseEn (models.Model): phrase = models.ForeignKey (Phrase, verbose_name='фраза', related_name='en') В данном случае Phrase является связующим звеном между PhraseEn и PhraseRu, хотя сама в себе ничего не содержит. Полезно, когда две модели равнозначны, и их необходимо связать в единое целое.Generic relation mixin Объекты GenericRelation всегда возвращаются QuerySet’ом, даже есть мы точно знаем, что объект один: class Token (models.Model): content_type = models.ForeignKey (ContentType) object_id = models.PositiveIntegerField () content_object = generic.GenericForeignKey ()
class Registration (models.Model): tokens = generic.GenericRelation (Token) Если нужно получить доступ к токену, мы пишем registration.tokens.first (). Но мы-то знаем, что токен один, и хотим писать просто registration.token и получить сразу заветный токен. Это возможно при помощи mixin: class Token (models.Model): content_type = models.ForeignKey (ContentType) object_id = models.PositiveIntegerField () content_object = generic.GenericForeignKey ()
class TokenMixin (object): @property def token (self): content_type = ContentType.objects.get_for_model (self.__class__) try: return Token.objects.get (content_type__pk=content_type.pk, object_id=self.id) except Token.DoesNotExist: return None
class Registration (models.Model, TokenMixin): tokens = generic.GenericRelation (Token) Теперь registration.token работает!
get_absolute_url Старайтесь не писать {% url 'shop/product' id=product.id %}.Лучше для каждой модели задайте метод get_absolute_url (), и используйте {{ object.get_absolute_url }}. Заодно и ссылка «смотреть на сайте» появится в админке.
pre_save В pre_save можно узнать, изменится ли модель после сохранения или нет. Цена — запрос к БД для получения старой записи из базы. @receiver (pre_save, sender=SomeModel) def process_signal (sender, instance, **kwargs): old_model = get_object_or_None (SomeModel, pk=instance.pk) if not old_model: # Created old_value = None … else: old_value = old_model.field new_value = instance.field
if new_value!= old_value: # field changed! Формы Этот паттерн уже был на хабре, но он слишком хорош, чтобы не упомянуть его. form = SomeForm (request.POST or None) if form.is_valid (): # … actions … return HttpResponseRedirect (…) return render ( request, {'form': form} ) На этом всё. Спасибо за внимание.