Cicero дорос до возможности постить в него текст. Сегодня реализовал две формы с проверкой ошибок, придумал связь с Django'вскими пользователями, и у меня есть пара вопросов к аудитории.
Но сначала вопрос немного другой. Я все никак не могу решить, нужно ли писать подробней? Если это интересно кому-нибудь из совсем новичков в Django, я могу порастекаться мыслью по древу чуть больше. Если же все и так в курсе базовых понятий фреймворка, то пересказывать документацию, конечно, скучно. Так как?
Еще по просьбам в комментариях буду выкладывать ссылки на архивы текущих версий для тех, у кого порты Subversion закрыты.
| SVN | svn://softwaremaniacs.org/cicero/trunk |
|---|---|
| Архив последней ревизии | 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
FX Poster
6.03.07 23:45
Может на меня все сейчас и набросятся, но я скажу так - мне было бы интересно, если бы ты описывал все поподробнее...
Verber
7.03.07 01:08
В принципе так как сейчас тоже нормально, но чуть больше ссылок на документацию. Спасибо
Offtop: Богатое слово "автомагически" :)
eerie
7.03.07 02:03
Вряд ли стоит растекаться. Если человека заинтересует, то он не поленится почитать документацию, тем более она написана очень простым языком.
Keep up the good work.
ay
7.03.07 03:28
Я поддерживаю FS Poster'а. Из из описания разработки форума может получится неплохой HOWTO для Django. Конечно, сильно углубляться в теоретические аспекты не стоит, но подробнее описать следовало бы, давая, где нужно ссылки на руководство;)
Внимательно следим за разработкой. Идея -- отличная. Удачи!
Горбунов Олег
7.03.07 05:56
А я вот к Django пока присматриваюсь только, но тем не менее, очень интересно читать, не отказался бы от подробностей. Возможно, стоит просто давать побольше ссылок на документацию?
Майк
7.03.07 08:08
Да, желательно чуть подробнее. Кому будет неинтересно - пропустит. Или действительно давать побольше ссылок на документацию.
Майк
7.03.07 09:41
Классное слово, возьму в цитаты :)
Иван Сагалаев
7.03.07 09:53
Поспешу сказать, что "автомагически" ("automagically") это дико древнее слово в програмерском сленге, его придумал не я!
Cleg
7.03.07 10:31
я - за подробности :-)
конкретных примеров, показывающих все "на пальцах" очень не хватает.
igorekk
7.03.07 10:39
Иван, я не нашел в документации понятия "профайл" (или не так искал). Можно ли в двух словах объяснить где посмотреть?
EntropyHacker
7.03.07 11:58
Можно использовать тотже dispatcher и слушать signals.post_save для модели User. Для этого в models.py надо добавить что-то типа:
Правда с post_save есть нюанс при использовании ManyToMany полей:
http://softwaremaniacs.org/forum/viewtopic.php?pid=2223#p2223
Иван Сагалаев
7.03.07 12:04
А... Профайл -- это да, не особенно документированная штука. Самое известное описание с примерами использования -- в блоге Джеймса Беннетта (один из основных разработчиков Джанго).
Вкратце смысл в том, что для того, чтобы к встроенной модели пользователя добавить какую-то дополнительную информацию, создает своя собственная модель. Она привязывается к стандартной модели User через OneToOneField (или как рекомендует Беннетт -- через уникальный ForeignKey). Потом название этой модели устанавливается в настройке AUTH_PROFILE_MODULE, чтобы джанговский юзер знал, какая модель является профайлом. После этого соответствующую юзеру запись профайла можно было получать через
user.get_profile().Иван Сагалаев
7.03.07 12:09
Спасибо за идею с user_post_save, действительно просто. Хотя оно не будет работать для пользователей, которые уже есть в системе на момент установки форума.
В общем-то, я уже сделал по-другому совсем, более заумно :-). Сегодня постараюсь описать.
Alexander Solovyov
7.03.07 12:29
Против более подробного расписывания, доки и так доступны и написаны неплохо.
Насчёт OneToOne - он же deprecated, разве нет? Почитал сейчас - должна измениться семантика... хрен знает шо, меняют как хотят. ;)
igorekk
7.03.07 12:30
Иван, спасибо :)
Суть ясна и довольно-таки логично. Как и всё в Django, по-большому счёту.
Иван Сагалаев
7.03.07 15:47
Чаяния народа понятны, всем спасибо! Обязательно буду давать больше ссылок, а некоторые моменты, которые можно пояснить парой фраз, буду во врезках писать.
Александр, по поводу OneToOne... Во-первых он "deprecated" уже так давно, что можно было построить и продать парочку сайтов с его использованием :-). Кроме того, я не очень понимаю, куда он так лихо поменяет семантику, разъяснений по этому поводу не было. Я помню только разговоры, что использование OneToOne для наследования -- странно, и это надо устранять. В общем, когда (или если) оно поменяется, тогда и я поменяю, не особо сложная операция.
Alexey Remizov
7.03.07 16:01
Да
Alexander Solovyov
7.03.07 17:22
Угу, для меня тоже странно... Но я помаялся, помаялся, прочитал вот эту строку:
и забил. ForeignKey и unique=True использую пока.
ivanko
7.03.07 18:11
Возможно, это и неправильно ( я в Django -100), но с профайлами следует таки поступать как и с любой базой авторизаций. А именно, на момент логина авторизовывать его в приложении (через например локальный OpenId), а при успешной авторизации - создавать профиль форума.
Miguel
9.03.07 10:40
Гм... Я Питона не знаю вообще, но мне почему-то кажется, что вместо облегчения работы фреймворк привносит, в основном, борьбу с собственными глюками. Во-всяком случае, фраз типа "извините, это работает кривовато, потом подумаем, как это сделать лучше" сильно больше, чем хвалебных.
No offence.
Иван Сагалаев
9.03.07 12:04
Спасибо за комментарий :-).
Я не ставлю целью в этом цикле хвалить Django. Восторгами по его поводу я исхожу с прошлой зимы, об этом можно почитать во всей категории Django, а здесь пишу просто подробно обо всем.
Но вот например очевидный плюс. В обработке форм автоматически присутствует валидация, хотя я для нее ничего не делал. Попробуйте в тестовом форуме оставить в форме пустое поле -- рядом с ним напишется ошибка (страшная, конечно, но это вопрос дальнейшего стайлинга).
Трудные же моменты присутствуют во многом из-за того, что я задачу поставил немножко выше, чем обычно делается: мне важно, чтобы форум был переносим и легко интегрировался в существующий Django-сайт.
Например форум подключается к системе авторизации пользователей сайта. Это, вообще, можно считать уникальной фичей, потому что я не видел нигде написанного форума, который можно было бы скачать, поставить на сайт, и там бы использовалась база пользователей этого сайта. Отчасти это как раз от того, что в Django есть само понятие "стандартная система авторизации".
Сложность с шаблонами тоже довольно уникальна: обычно скачиваемое веб-приложение само целиком определяет свой внешний вид, а мне интересно, чтобы форум увязывался в шапки сайта.
Кроме того, все только началось, следите дальше, надеюсь будет больше вещей, где Django будет себя хорошо показывать.
Michael Samoylov
9.03.07 13:53
по-моему, логичнее вынести CRUD-логику в TopicManager & ArticleManager (чтобы не рушить MVC паттерн и разгрузить view от избыточной логики)...
models.py
views.py