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, который был пару дней назад)

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