Некоторые особенности Django, о которых хорошо знать заранее

в 11:26, , рубрики: django, python, особенности, метки: , ,

Django

Django — это прекрасно. В самом деле: если писать о том, как много хорошего появилось в мире с приходом и развитием Django, то можно писать очень долго, и всё равно не рассказать обо всём. Лично я на данный момент постоянно использую Django уже четыре с половиной года, и всё это время Django становилась всё лучше и лучше.

И всё же в какой-то момент понимаешь, что в документации есть не всё. И тут варианты решения задач появляются разные — можно открыть исходники Django, можно посмотреть, что пишут по этому поводу на Stackoverflow и в других местах (часто очень помогают списки рассылки, а также официальный Trac), но ещё лучше — всё это вместе.

Давайте рассмотрим всего несколько примеров того, что может понадобиться в повседневной работе, но чего (пока что) не прочтёшь в официальной документации. Впрочем, если вы считаете, что нужно добавить ещё что-то — пишите, может быть, это действительно так.

1. Выбор всех объектов модели (когда объектов больше, чем помещается на одной странице) и действия административного интерфейса (admin actions) с промежуточными страницами (intermediate pages).

У вас в административном интерфейсе есть собственные действия? А в них используются промежуточные страницы? Если да, то проверьте, откуда именно берётся queryset при отображении этой самой промежуточной страницы.

Потому что если это делается так (как описано в документации):

selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)

То вы столкнётесь с тем, что действие будет срабатывать, но только, например, для первых 100 объектов (если на странице их выводится 100), а не для всех объектов модели, как было выбрано.

На самом деле обычно на промежуточной странице выводится форма, которая содержит скрытые поля («_selected_action») с номерами объектов.

То есть (в ModelAdmin):

    class AssignBooksForm(forms.Form):

        _selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
        user = forms.ModelChoiceField(queryset=User.objects.all(), label=u"Пользователь", empty_label=None)

    def assign_books(self, request, queryset):
        form = None
        if 'cancel' in request.POST:
            self.message_user(request, u'Назначение книг отменено.')
            return
        elif 'assign' in request.POST:
            form = self.AssignBooksForm(request.POST)
            if form.is_valid():
                user = form.cleaned_data['user']
                for book in queryset:
                    assign_book(book.pk, user.pk)
                self.message_user(request, u'Выбранные книги (%s) назначены для %s.' % (queryset.count(), user.username))
                return HttpResponseRedirect(request.get_full_path())
        if not form:
            form = self.AssignBooksForm(initial={'_selected_action': queryset.values_list('os_id', flat=True)})
        return render_to_response('books/assign_books.html', {'books': queryset, 'form': form, 'path':request.get_full_path()}, context_instance=RequestContext(request))
    assign_books.short_description = u'Назначить книги пользователю'

    actions = ['assign_books']

Но реально велика вероятность, что вместо такой строчки:

form = self.AssignBooksForm(initial={'_selected_action': queryset.values_list('id', flat=True)})

Такая строчка (например, тут):

form = self.AssignBooksForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

И вот в этом случае получается особенно интересно. Потому что когда пользователь выбрал все объекты (отметил галочку слева от названий столбцов, и затем нажал «Выбрать все … (…)» над таблицей со списком записей), JS указал для скрытого поля «select_across» значение 1. После этого ModelAdmin.response_action (contrib.admin.options), обнаружив, что в форме (contrib.admin.helpers.ActionForm) задан select_across (BooleanField), не ограничивает queryset до тех объектов, которые выбраны галочками, а вызывает функцию действия с полным queryset'ом (но содержимое объекта request метод response_action, разумеется, не меняет).

Причём если у вас в шаблоне выводится что-нибудь такое:

<p>Для выбранного пользователя будут назначены следующие книги:</p>
<ul>
{% for book in books %}
    <li>{{ book }}</li>
{% endfor %}
</ul>

То там список будет правильный (он будет содержать все объекты, а не только, например, первые 100) — потому что в контексте в шаблон в переменной books именно queryset. А вот в форме как раз может быть не список идентификаторов на основе queryset'а, а список отмеченных галочек. В этом случае при отправке формы с промежуточной страницы в queryset'е будет содержаться уже только 100 записей, и, соответственно, действие будет выполнено только с ними.

Кстати, про это есть обсуждение в комментариях к этой записи, а также тикет в Trac'е.

2. Журналирование в текстовые файлы с указанием даты и времени.

В общем-то, очень простая задача, но как это сделать в документации не указано. Тем не менее, там вскользь упоминается возможность задавать словарь formatters и даже даётся небольшой пример (правда, без даты и времени).

На самом деле, всё действительно просто:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '[%(levelname)s] %(asctime)s %(message)s',
            'datefmt': '%Y-%m-%d %H:%M:%S',
        },
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler'
        },
	    'books_log_file':{
	        'level': 'DEBUG',
	        'class': 'logging.FileHandler',
            'formatter': 'verbose',
            'filename': os.path.join(PROJECT_ROOT, 'logs/books.log'),
	    },
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
        'books': {
            'handlers': ['books_log_file', 'mail_admins'],
            'level': 'INFO',
            'propagate': True,
        },
    }
}

Тут, конечно же, предполагается, что выше в настройках устанавливается PROJECT_ROOT. Например, так:

import os
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))

А если требуется сделать как-то иначе, то можно обратиться к документации по Python.

3. Локализация сайта в случае, если оригинальные текстовые строки написаны не на английском.

Встроенная в Django система локализации, в общем-то, работает хорошо, но есть одна особенность. Дело в том, что если вы переводите JS-строки, и эти строки написаны не на английском (а, например, на русском), то как только вы создадите английский перевод (django-admin.py makemessages -l en -d djangojs) и скомпилируете djangojs.po, этот перевод начнёт отображаться не только когда выбран английский язык, но и когда выбран русский.

То есть вы выбираете русский язык, а JS выводит строки на английском (потому что не может найти русский перевод, и вместо него берёт английский — хотя в данном случае перевод реально не нужен, так как оригиналы строк уже на русском).

Решение (если, конечно, вы сразу отказались от идеи создать русский перевод русского же текста) тут вполне простое.

Нужно добавить в urls.py такой pattern:

url(r'^jsi18n/null/$', 'django.views.i18n.null_javascript_catalog'),

И отредактировать шаблон (обычно base.html), добавив туда:

{% if request.LANGUAGE_CODE == 'ru' %}
    <script type="text/javascript" src="{% url django.views.i18n.null_javascript_catalog %}"></script>
{% else %}
    <script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
{% endif %}

Предполагается, что urlpattern для django.views.i18n.javascript_catalog уже есть, конечно же.

4. Добавление простых страниц (flatpages) без указания сайтов.

В текущей стабильной версии Django есть один баг: если у вас подключено приложение flatpages, и вы, создавая такую страницу, случайно забудете выбрать сайты из списка, то вы получите ошибку 500, или, если включён DEBUG:

ValueError at /admin/flatpages/flatpage/add/
Cannot use None as a query value

Разумеется, об этой ошибке уже известно разработчикам — более того, она была исправлена три месяца назад. Собственно, в коммите, который это исправляет, внесены изменения всего в одну строчку (плюс тест), так что вы легко можете пропатчить Django у себя. В stable он пока не попал (в том числе в релиз Django 1.4.1, который был пару дней назад)

В общем, пользуйтесь поиском, проводите достаточно детальный сбор данных, тестируйте. Если даже какое-то решение на вид рабочее, то всё равно имейте в виду, что везде могут быть свои нюансы. Приятной разработки!

Автор: MaGIc2laNTern

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js