Django tips & tricks

python > Django tips & tricks
11.05.2018 18:25:38


Наиболее часто встречающиеся слова в статье:

[request] [include] [virtualenv] [settings] [проекта] [variable] [страницы] [контекст] [local_settings] [шаблоне]


Статья:

источник https://habr.com/post/224011/

Виртуальное окружение


Если вы не используете 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 действительно изолирован от «внешней» среды — если кратко, то это лишь частичная изоляция.


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


В 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 пользователя?


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


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}
)