Приветствую!
В этом посте — небольшие советы по работе с Django, которые могут пригодиться начинающим разработчикам. Как я хотел бы знать это в начале моего пути освоения Django!..
Рассматривать эти советы следует с долей критицизма. Буду рад, если вы найдёте неточности / лучшее решение, или предложите свои «фишки» для django, которых нет в документации.
Итак, начнём издалека, а уж потом перейдём к деталям.
Если вы не используете virtualenv для вашего django-приложения — то обязательно попробуйте.
Если вы уже используете virtualenv, то ответьте, нужен ли вам --no-site-packages. Этот флаг по умолчанию включён и используется при создании виртуального окружения. При включённом флаге программы «внутри» окружения не увидят программы «снаружи». Если вы поставите вашим пакетным менеджером какой-нибудь пакет глобально, например, python2-django, то «внутри» окружения всё равно придётся делать pip install django.
Видимость / невидимость глобальных программ из virtualenv устанавливается отсутствуем / наличием файла [virtualenv]/lib/python*.*/no-global-site-packages.txt. Вот так просто.
Кстати, рекомендую всем статью про «изолированность» virtualenv: Why I hate virtualenv and pip (сайт тормозит, смог открыть только через web.archive.org). В ней рассматривается, насколько virtualenv действительно изолирован от «внешней» среды — если кратко, то это лишь частичная изоляция.
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. Это путает и отвлекает. Если расширить этот совет — зафиксируйте (унифицируйте) для себя структуру каталогов всех ваших проектов и строго её придерживайтесь.
Живой пример:
Никогда, никогда не кидайте шаблоны (.html) в папку templates вашего приложения. Всегда создавайте дополнительный каталог с названием, совпадающим с именем приложения.
Вот это плохо, т.к. создаёт коллизию шаблонов, например, при {% include 'main.html' %}:
Вот это — хорошо, можно использовать {% include 'reviews/main.html' %}:
К слову, если вы используете {% include 'some_template.html' %}, то велика вероятность, что что-то не так. Почему?
Пример:
1) KISS едет лесом. С одной стороны, код страницы разбит на несколько — master.html и подключаемый slave.html, и это удобно для разделения больших html-страниц на части. Но в данном случае переменная var передаётся в шаблон slave.html неявно — var передатся в master.html, а slave.html просто «цепляет» контекст master'а. Таким образом, мы видим, что шаблон внутри {% include %} зависит от контекста основного шаблона. Мы вынуждены следить за контекстом родительского шаблона, иначе в дочерний может попасть что-нибудь не то.
2) По моим наблюдениям, {% include %} дорогой в плане рендеринга. Лучше его избегать.
Что делать? Если очень хочется одни шаблоны включать в другие — используйте inclusion tags (о них читать ниже). Но проще — просто пишите всё в одном файле:
Вы же не имеете два разных settings.py на тестовом и деплой серверах, да?
Создайте дополнительные local_settings.py и deployment_settings.py, куда скиньте всё, что относится только к соответствующему серверу.
В settings.py пишем в начале:
Соответственно, на деплое удаляем local_settings.py. Чтобы он не мешался, его можно добавить в .gitignore.
Задайте корень проекта в settings.py — это облегчит жизнь потом:
Используйте контекстные процессоры только если вам нужно добавить переменные в контекст каждой страницы сайта — ведь контекстные процессоры будут вызываться для любой страницы, даже если вы не воспользуйтесь их результатами. Лично я использую их для передачи номера телефона в контекст шаблона — этот номер реально на каждой странице выводится, и не единожды. Ещё пример — меню сайта. Я прописал заголовки и ссылки в контекстном процессоре, и если мне нужно будет добавить новый раздел в меню — я просто добавлю его в контекстный процессор, и он автоматически добавится везде на сайте.
Есть одна ошибка — использование контекстных процессоров для виджетов. Например, у вас на сайте есть колонка новостей, которая выводится всегда, т.е. на каждой страничке. Казалось бы, создать 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 — плагин к debug toolbar, который замеряет время рендеринга шаблонов. А учитывая, что шаблоны django довольно «дорогие» (рендерятся долго), то для ускорения сайта этот плагин — то что доктор прописал.
django-adv-cache-tag позволяет очень гибко управлять кешированием в шаблонах — версионность, сжатие, частичное кэширование. Просто оцените:
Шаблоны email писем — это то, чего не хватает django. django-mail-templated
django-ipware определит ip пользователя за вас, и сделает это лучше.
Не пишите свой парсер html. Не парсите html сами. Всё уже есть.
Если вы создаёте форму и хотите для каждого input-а задать стиль, класс или placeholder, то django заставит вас нарушить принципы и прописать все стили прямо в forms.py:
Меня каждый раз коробит при виде html текста не в .html файлах. Это нарушает MVT архитектуру. Поэтому я создал для себя фильтр:
Данный фильтр добавляет класс к тегам, но можно переписать и добавлять любое свойство.
Иногда нужно что-то выводить в шаблоне, если открыта определённая страница. Например, подсветить кнопку «магазин» в меню, если пользователь сейчас в разделе магазина. Предлагаю следующий вариант:
Это фильтр, а не тэг, и причина тут одна: можно строить совершенно дичайшие конструкции с {% if %}. Например, если текущая страница — карточка товара, и при этом пользователь авторизован:
Есть и альтернативная, более точная, реализация, в которой используются аргументы (args или kwargs) для определения точной страницы (т.е. не просто «страница какого-либо товара», а «страница товара с id=36»):
Модели могут быть пустыми. Вот так:
В данном случае Phrase является связующим звеном между PhraseEn и PhraseRu, хотя сама в себе ничего не содержит. Полезно, когда две модели равнозначны, и их необходимо связать в единое целое.
Объекты GenericRelation всегда возвращаются QuerySet'ом, даже есть мы точно знаем, что объект один:
Если нужно получить доступ к токену, мы пишем registration.tokens.first(). Но мы-то знаем, что токен один, и хотим писать просто registration.token и получить сразу заветный токен. Это возможно при помощи mixin:
Теперь registration.token работает!
Старайтесь не писать {% url 'shop/product' id=product.id %}.
Лучше для каждой модели задайте метод get_absolute_url(), и используйте {{ object.get_absolute_url }}. Заодно и ссылка «смотреть на сайте» появится в админке.
В pre_save можно узнать, изменится ли модель после сохранения или нет. Цена — запрос к БД для получения старой записи из базы.
Этот паттерн уже был на хабре, но он слишком хорош, чтобы не упомянуть его.
На этом всё. Спасибо за внимание.
UPD. Как обычно на Хабре, в комментариях хабражители высказали свои мнения и предложили кучу замечательных идей, дополнений и замечаний к статье. Я не стал их вносить в статью, но вместо этого настоятельно рекомендую ознакомиться с комментариями к статье.
В этом посте — небольшие советы по работе с 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:
Вообще, когда модуль написан на чистом питоне, проблем не возникает — устанавливаем через pip в virtualenv. Если модуль — это смесь, скажем, c++ и питона — начинается магия.
- Создать помойку внутри 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'}
}
<!-- master.html -->
Value of variable var: {{ var }}.
{% include 'slave.html' %}
<!-- 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 (о них читать ниже). Но проще — просто пишите всё в одном файле:
<!-- master.html -->
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.date_last_updated -->
{{ obj }}
{% nocache %}
{{ now }} <!-- А это никогда не кэшируем -->
{% endnocache %}
{{ obj.date_last_updated }}
{% endcache %}
django-mail-templated
Шаблоны email писем — это то, чего не хватает django. django-mail-templated
django-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}
)
На этом всё. Спасибо за внимание.
UPD. Как обычно на Хабре, в комментариях хабражители высказали свои мнения и предложили кучу замечательных идей, дополнений и замечаний к статье. Я не стал их вносить в статью, но вместо этого настоятельно рекомендую ознакомиться с комментариями к статье.