Cicero дорос до возможности постить в него текст. Сегодня реализовал две формы с проверкой ошибок, придумал связь с Django'вскими пользователями, и у меня есть пара вопросов к аудитории.

Но сначала вопрос немного другой. Я все никак не могу решить, нужно ли писать подробней? Если это интересно кому-нибудь из совсем новичков в Django, я могу порастекаться мыслью по древу чуть больше. Если же все и так в курсе базовых понятий фреймворка, то пересказывать документацию, конечно, скучно. Так как?

Еще по просьбам в комментариях буду выкладывать ссылки на архивы текущих версий для тех, у кого порты Subversion закрыты.

Bzr-репозиторий http://softwaremaniacs.org/code/cicero/
Архив последней ревизии http://softwaremaniacs.org/media/cicero.zip

Кто будет ставить себе не забудьте вызвать ./manage.py syncdb, а также прописать в список middleware "cicero.middleware.ProfileMiddleware" (подробности — в статье).

Автор статьи

В текущей модели статьи в качестве автора предусмотрен только зарегистрированный пользователь, что не соответствует моему намерению давать постить сообщения без регистрации. Поэтому модель нужно расширить: допустить отсутствие ссылки на пользователя, а также добавить новое поле — "guest_name" — в котором будет храниться имя, которое гость оставит в формочке. Также для отображения в шаблонах к модели прирастает метод "author_display", который будет решать, какое поле показывать, примерно в таком духе:

def author_display(self):
  return self.author or self.guest_name

А вот со ссылкой на пользователя все не так просто. Самое очевидное решение — NULL'овое поле author — на самом деле нарушает известный паттерн "Null object". Паттерн этот рекомендует в местах, где встречаются всякие "nobody", "default" и прочие "undefined" вместо тупого NULL'а использовать объект, который реализует поведение по умолчанию. Смысл этого состоит в том, что пропадает нужда везде по коду делать специальные проверки на NULL, что уменьшает сложность системы.

В Django же эта теория выражается в очень конкретную практическую пользу с несколько неожиданной стороны. В списке статей рядом с каждой надо выводить автора. Если делать это напрямую, то на каждую статью пойдет отдельный запрос, что сильно просадит производительность. Поэтому такой список — прямо таки образцовый случай для использования select_related, который подцепляет в запрос статей таблицу с юзерами и вытягивает все имена в один запрос.

Но поскольку пользователи в запрос подцепляются обычным join'ом, то статьи, у которых пользователя нет, не выведутся вообще. Join'а с включением NULL'ов на места отсутствующих записей в Django не реализовано. И поэтому, чтобы не терять записи, Django просто напрочь отключает select_related, если у модели допустимы NULL'овые ForeignKey-ссылки. Можно, конечно, обидеться за это на Django, но это как раз то, что обычно случается со всякими "специальными случаями": они всегда реализуются уже когда-нибудь потом.

Поэтому вместо обNULLения поля "author" я сделал отдельного пользователя с логином "cicero_guest", который будет проставляться для неавторизованных пользователей. Это, соответственно, позволяет использовать select_related для вывода статей. И чтобы это происходило всегда, я включил его в виде менеджера модели:

class ArticleManager(models.Manager):
  def get_query_set(self):
    return super(ArticleManager, self).get_query_set().select_related()

class Article(models.Model):
  # ...
  author = models.ForeignKey(User)

  objects = ArticleManager()

Ну и последний штрих — автоматическое создание этого гостевого юзера при установке приложения. Обычно это делается через написание в нужном месте SQL-скрипта с INSERT'ом, который автоматически выполняется при создании в базе таблиц моделей. Но тут трудность в том, что модель пользователя не моя, а джанговская, а SQL можно писать только для своих моделей.

Поэтому пришлось воспольоваться другим средством. При инициализации приложения Джанго импортирует модуль management.py, который находится в директории приложения. В нем можно подключиться к сигналу "post_syncdb", который запускается как раз после инициализации, и там сделать все что надо:

from django.dispatch import dispatcher
from django.db.models import signals

def init(signal, sender, app, created_models):
  import cicero.models
  if app == cicero.models:
    from django.contrib.auth.models import User
    try:
      cicero_guest = User.objects.get(username='cicero_guest')
    except User.DoesNotExist:
      password = User.objects.make_random_password()
      cicero_guest = User.objects.create_user('cicero_guest', 'cicero_guest@localhost', password)

dispatcher.connect(init, signal=signals.post_syncdb)

Профайл

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

В частности, Cicero нужно для каждого пользователя хранить его выбранный фильтр статей. Куда его вписывать?

Пока я проблему решил так. Я не использую стандартное джанговское понятие профайла вообще. Вместо этого в Cicero появляется еще одна модель собственного профайла:

class Profile(models.Model):
  user = models.OneToOneField(User, related_name='cicero_profile')
  filter = models.CharField(maxlength=50)

  def __init__(self, *args, **kwargs):
    super(Profile, self).__init__(*args, **kwargs)
    self.filter = 'bbcode' # implement reading default from available filters

Соответственно, у юзеров в системе появляется атрибут "cicero_profile", на наличие которого приложение уже может рассчитывать.

В связи с этим вопрос к аудитории. Есть предложения, как решить задачку лучше? Или и так нормально?

Есть, правда, еще одна кривость. При заведении в системе новых пользователей, никто автомагически не будет для них создавать записи Profile'ов, и обращение user.cicero_profile будет вызывать exception. Поэтому, надо их где-то создавать. Я для этого использую middleware, которое вешается где-нибудь после авторизации и для каждого пользователя, залогиненного на сайт, смотрит, есть ли у него профиль, и создает если надо. Минусов я вижу два:

Поэтому у меня есть идея, как это переписать с помощью умных дескрипторов, но это как-нибудь потом...

Формы постинга

Пока в приложении есть место всего для двух форм — создания топика и статьи. Обе они, к сожалению, очень "специальные", не соответствуют напрямую моделям топика и статьи, поэтому их нельзя создать автоматически. Но с другой стороны, они обе страшно маленькие, поэтому файлик forms.py, в который я их вынес, получился компактным:

from django.newforms import *

class ArticleForm(Form):
  text = CharField(label='Текст', required=True, widget=Textarea(attrs={'cols': '80', 'rows': '20'}))
  name = CharField(label='Имя', required=True)

class TopicForm(Form):
  subject = CharField(label='Тема', required=True)
  text = CharField(label='Текст', required=True, widget=Textarea(attrs={'cols': '80', 'rows': '20'}))
  name = CharField(label='Имя', required=True)

Поле "name" по задумке будет принимать либо произвольное имя гостя, либо в будущем — OpenID'шный URL. А для уже залогиненных пользователей показываться не будет вообще.

Две view-функции, обрабатывающие формы, очень похожи: одной надо создать топик и первую статью с автором, второй — только статью с автором. Сам скелет функций совершенно стандартен для обработки форм, привожу его исключительно "для порядку" :-)

# передать POST в форму
form = TopicForm(request.POST) # или ArticleForm

if not form.is_valid():
  # если ошибки, выдать ответ с формой и ошибками
  return render_to_response(... {'form': form})

# если же все хорошо, сохранить отвалидированные данные
_create_article(topic, form, request.user)

# вернуться на URL, откуда пришли  
return HttpResponseRedirect('../')

Здесь, правда, есть шероховатость. Когда возвращается ошибка, принято перепоказывать ту же самую форму с ошибками. Это легко и удобно, когда вся страница посвящена форме и выводится в том же самом view. Здесь же у меня view для постинга отдельная от view для просмотра, и просто вызвать последнюю не получится, потому что часть ее параметров получается из urlconf'а, и в постинговой функции про них ничего неизвестно.

Поэтому я сейчас сделал отдельные шаблоны, где есть только форма добавления, и при ошибках вывожу их. То есть, человек, который ошибается при попытке постинга перебрасывается на отдельную страницу с формой ввода. Кривовато :-). Единственное, что меня успокаивает в этом варианте, что он практически не будет никогда работать: я повешу на формы постинг ajax'ом, и там надо будет возвращать только словарик с ошибками, а не всю страницу заново.

Впрочем, вот прямо сейчас у меня созрела и другая мысль. Действительно перетащить код добавления топика и статьи в view-функции вывода списка топиков и списка статей. Это во-первых устранит нужду в отдельных шаблонах, а во-вторых будет совпадать по идеологии с Atom Publishing Protocol, который я в конце собираюсь навешивать... Здраво?


В итоге, я вывесил эту уже минимально работающую версию на http://softwaremaniacs.org/cicero/. Она, конечно, экстремально страшная, но пусть будет. В следующий раз займусь как раз приведением в порядок шаблонов.

Комментарии: 22

  1. FX Poster

    Может на меня все сейчас и набросятся, но я скажу так - мне было бы интересно, если бы ты описывал все поподробнее...

  2. Verber

    В принципе так как сейчас тоже нормально, но чуть больше ссылок на документацию. Спасибо

    Offtop: Богатое слово "автомагически" :)

  3. eerie

    Вряд ли стоит растекаться. Если человека заинтересует, то он не поленится почитать документацию, тем более она написана очень простым языком.
    Keep up the good work.

  4. ay

    Я поддерживаю FS Poster'а. Из из описания разработки форума может получится неплохой HOWTO для Django. Конечно, сильно углубляться в теоретические аспекты не стоит, но подробнее описать следовало бы, давая, где нужно ссылки на руководство;)
    Внимательно следим за разработкой. Идея — отличная. Удачи!

  5. Горбунов Олег

    А я вот к Django пока присматриваюсь только, но тем не менее, очень интересно читать, не отказался бы от подробностей. Возможно, стоит просто давать побольше ссылок на документацию?

  6. Майк

    Да, желательно чуть подробнее. Кому будет неинтересно - пропустит. Или действительно давать побольше ссылок на документацию.

  7. Майк

    автомагически

    Классное слово, возьму в цитаты :)

  8. Иван Сагалаев

    Поспешу сказать, что "автомагически" ("automagically") это дико древнее слово в програмерском сленге, его придумал не я!

  9. Cleg

    я - за подробности :-)

    конкретных примеров, показывающих все "на пальцах" очень не хватает.

  10. igorekk

    Иван, я не нашел в документации понятия "профайл" (или не так искал). Можно ли в двух словах объяснить где посмотреть?

  11. EntropyHacker

    Есть, правда, еще одна кривость. При заведении в системе новых пользователей, никто автомагически не будет для них создавать записи Profile’ов,

    Можно использовать тотже dispatcher и слушать signals.post_save для модели User. Для этого в models.py надо добавить что-то типа:

    def user_post_save(instance, **kwargs):
        """Creates profile for user if profile is missed"""
        pass
    
    dispatcher.connect(user_post_save, signal=signals.post_save, sender=User)
    

    Правда с post_save есть нюанс при использовании ManyToMany полей:
    http://softwaremaniacs.org/forum/viewtopic.php?pid=2223#p2223

  12. Иван Сагалаев

    А... Профайл — это да, не особенно документированная штука. Самое известное описание с примерами использования — в блоге Джеймса Беннетта (один из основных разработчиков Джанго).

    Вкратце смысл в том, что для того, чтобы к встроенной модели пользователя добавить какую-то дополнительную информацию, создает своя собственная модель. Она привязывается к стандартной модели User через OneToOneField (или как рекомендует Беннетт — через уникальный ForeignKey). Потом название этой модели устанавливается в настройке AUTH_PROFILE_MODULE, чтобы джанговский юзер знал, какая модель является профайлом. После этого соответствующую юзеру запись профайла можно было получать через user.get_profile().

  13. Иван Сагалаев

    Спасибо за идею с user_post_save, действительно просто. Хотя оно не будет работать для пользователей, которые уже есть в системе на момент установки форума.

    В общем-то, я уже сделал по-другому совсем, более заумно :-). Сегодня постараюсь описать.

  14. Alexander Solovyov

    Против более подробного расписывания, доки и так доступны и написаны неплохо.

    Насчёт OneToOne - он же deprecated, разве нет? Почитал сейчас - должна измениться семантика... хрен знает шо, меняют как хотят. ;)

  15. igorekk

    Иван, спасибо :)
    Суть ясна и довольно-таки логично. Как и всё в Django, по-большому счёту.

  16. Иван Сагалаев

    Чаяния народа понятны, всем спасибо! Обязательно буду давать больше ссылок, а некоторые моменты, которые можно пояснить парой фраз, буду во врезках писать.

    Александр, по поводу OneToOne... Во-первых он "deprecated" уже так давно, что можно было построить и продать парочку сайтов с его использованием :-). Кроме того, я не очень понимаю, куда он так лихо поменяет семантику, разъяснений по этому поводу не было. Я помню только разговоры, что использование OneToOne для наследования — странно, и это надо устранять. В общем, когда (или если) оно поменяется, тогда и я поменяю, не особо сложная операция.

  17. Alexey Remizov

    нужно ли писать подробней?

    Да

  18. Alexander Solovyov

    Угу, для меня тоже странно... Но я помаялся, помаялся, прочитал вот эту строку:

    This OneToOneField will actually replace the primary key id field (since one-to-one relations share the same primary key)

    и забил. ForeignKey и unique=True использую пока.

  19. ivanko

    Возможно, это и неправильно ( я в Django -100), но с профайлами следует таки поступать как и с любой базой авторизаций. А именно, на момент логина авторизовывать его в приложении (через например локальный OpenId), а при успешной авторизации - создавать профиль форума.

  20. Miguel

    Гм... Я Питона не знаю вообще, но мне почему-то кажется, что вместо облегчения работы фреймворк привносит, в основном, борьбу с собственными глюками. Во-всяком случае, фраз типа "извините, это работает кривовато, потом подумаем, как это сделать лучше" сильно больше, чем хвалебных.
    No offence.

  21. Иван Сагалаев

    Спасибо за комментарий :-).

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

    Но вот например очевидный плюс. В обработке форм автоматически присутствует валидация, хотя я для нее ничего не делал. Попробуйте в тестовом форуме оставить в форме пустое поле — рядом с ним напишется ошибка (страшная, конечно, но это вопрос дальнейшего стайлинга).

    Трудные же моменты присутствуют во многом из-за того, что я задачу поставил немножко выше, чем обычно делается: мне важно, чтобы форум был переносим и легко интегрировался в существующий Django-сайт.

    Например форум подключается к системе авторизации пользователей сайта. Это, вообще, можно считать уникальной фичей, потому что я не видел нигде написанного форума, который можно было бы скачать, поставить на сайт, и там бы использовалась база пользователей этого сайта. Отчасти это как раз от того, что в Django есть само понятие "стандартная система авторизации".

    Сложность с шаблонами тоже довольно уникальна: обычно скачиваемое веб-приложение само целиком определяет свой внешний вид, а мне интересно, чтобы форум увязывался в шапки сайта.

    Кроме того, все только началось, следите дальше, надеюсь будет больше вещей, где Django будет себя хорошо показывать.

  22. Michael Samoylov

    по-моему, логичнее вынести CRUD-логику в TopicManager & ArticleManager (чтобы не рушить MVC паттерн и разгрузить view от избыточной логики)...

    models.py

    class ArticleManager(models.Manager):
        def create_article(self, data):
            # Create article
            article = self.model(
                attribute=data['attribute'],
            )
            return article
    

    views.py

    def article_create(request):
        if not request.POST:
            form = ArticleForm()
            return render_to_response('template.html', {
                'form': form,
            }, context_instance=RequestContext(request))
        data = request.POST.copy()
        form = ArticleForm(data)
        if not form.is_valid():
            # Return errors dict for AJAX callback
            return HttpResponse(simplejson.dumps(form.errors, ensure_ascii=False), 'text/javascript')
        article = Article.objects.create_article(form.clean_data)
        return HttpResponseRedirect('../')
    

Добавить комментарий