Вчера рано ночью случилось радостное событие: я переключил свои форумы на движок Cicero! Удачно использовав первую часть отпуска, я наконец доделал две последние абсолютно необходимые вещи: импорт содержимого предыдущего форума и верстку.
И вот как раз импорту я и посвящаю последний подробный технический обзор в серии Cicero. Дальше форум будет развиваться уже по мелочам и без постописательства.
Bzr-репозиторий | http://softwaremaniacs.org/code/cicero_punbb_import/ |
---|---|
Работающий форум | http://softwaremaniacs.org/forum/ |
Импорт, вообще, более точно стоит назвать "миграцией", которая состоит из нескольких операций:
- импорт форумов, топиков, статей и пользователей из старой базы
- организация редиректов для поддержки старых ссылок
- привязка статей к новым аккаунтам
Для всего этого я сделал отдельное от Cicero приложение. Так это лучше укладывалось в голову при программировании, а также позволяет не таскать этот кусок к себе тем, кому импорт не нужен.
Импорт контента
Структурно импорт оказался очень простым. Движок PunBB, оказывается, разделяет форум на очень похожие модели: там тоже есть таблицы форумов, топиков, статей и пользователей. Поля только немного другие. Поэтому сам алгоритм выливается в "сделать select, создать по результатам новые записи".
Оформлено это все в виде пользовательской джанго-команды — самый лучший способ оформлять подобные вещи в Джанго, потому что вы получаете много мелких удобств: автоматизированный парсинг аргументов и опций, вывод help'а, сообщения об ошибках, настроенную джанго-среду. Пользователю же в итоге остается только установить приложение cicero_punbb_import и сказать в консоли:
./manage.py import_punbb <databasename>
В коде это выглядит примерно так. В папке приложения создается файл management/commands/import_punbb.py, в котором определяется класс Command:
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
# Описание опций и подсказок
option_list = BaseCommand.option_list + (
make_option('--user', action='store', dest='user', default=None,
help=u'Имя пользователя'),
make_option('--password', action='store', dest='password', default=None,
help=u'Пароль'),
make_option('--host', action='store', dest='host', default='localhost',
help=u'Хост'),
make_option('--port', action='store', dest='port', default=None,
help=u'Порт'),
make_option('--real-charset', action='store', dest='real_charset',
help=u'Игнорировать заявленную кодировку в таблицах исходной БД и \
считать данные записанными в указанной кодировке'),
)
help = u'Импортирует содержимое PunBB-форума в БД Cicero'
args = '<database>'
# Основная функция обработки команды
def handle(self, *args, **options):
# Проверка валидности аргументов
if len(args) != 1:
raise CommandError('Требуется один параметр -- имя БД PunBB')
kwargs = {
'host': options['host'],
'db': args[0],
}
if options['user'] is not None:
kwargs['user'] = options['user']
if options['password'] is not None:
kwargs['passwd'] = options['password']
if options['port'] is not None:
kwargs['port'] = int(options['port'])
self.real_charset = options.get('real_charset', None)
if not self.real_charset:
kwargs.update({
'use_unicode': True,
})
# Cоединение с MySQL
import MySQLdb
self.connection = MySQLdb.connect(**kwargs)
# Выполнение всех операций в одной транзакции
from django.db import transaction
transaction.enter_transaction_management()
try:
transaction.managed(True)
self.import_forums()
self.import_topics()
self.import_articles()
transaction.commit()
finally:
transaction.rollback()
transaction.leave_transaction_management()
# сами функции импорта не очень интересные и опущены для краткости
Привожу я этот код так подробно отчасти из-за того, что получилось все красиво, а воспользоваться этим у меня вышло всего один раз. Обидно!
Еще один момент, на котором остановлюсь — это опция "--real-charset". Так вышло, что когда-то давно, когда я переезжал на новый хостинг, и перетаскивал с собой БД, я очень долго мучался, пытаясь заставить MySQL хранить utf-8 данные, а форум — их корректно показывать. Мучился, потому что я тогда ничего не знал про MySQL в этом смысле. Так вот теперь оказалось, что тогда у меня получилось все криво: данные в таблицах закодированы в utf-8, но зато сами таблицы думают про них, что это — latin1. В итоге, если положиться на MySQLdb, чтобы он сам раскодировал данные, они приходят очевидно побитые.
Поэтому я и придумал опцию --real-charset, в которую пользователь может написать кодировку, которую ему надо. В этом случае я забираю из БД не юникод, а просто байты и раскодировку провожу уже вручную, decode'ом:
for id, forum_id, subject, posted in cursor.fetchall():
if self.real_charset:
subject = subject.decode(self.real_charset, 'replace')
topic = Topic.objects.create(
forum_id=self.forum_ids[forum_id],
subject=subject,
)
Редиректы
Редиректы — это очень важно для моего форума. Одна из его основных целей в том, чтобы служить базой знаний, в которую можно сослаться. И моя статистика показывает, что это востребовано: ссылок извне на мой форум довольно много. Поэтому ломать их мне ни в коем случае нельзя.
Для этой цели в приложении cicero_punbb_import определены несколько моделей, которые фактически хранят соответствие старых ID форумов, топиков и статей их новым импортированным копиям.
class ForumImport(models.Model):
id = models.IntegerField(primary_key=True)
forum = models.ForeignKey(Forum)
class TopicImport(models.Model):
id = models.IntegerField(primary_key=True)
topic = models.ForeignKey(Topic)
class ArticleImport(models.Model):
id = models.IntegerField(primary_key=True)
article = models.ForeignKey(Article)
user = models.ForeignKey(UserImport)
Соответственно при импорте создаются не только записи в таблицах самого Сicero, но и вот эти, служебные. Дальше обслуживание редиректов — дело пары вьюх, которые по старым IDшникам ищут новые и возвращают HttpResponseRedirect:
# вспомогательная функция
def get_object_by_int(model, pk):
try:
return model.objects.get(pk=int(pk))
except (ValueError, model.DoesNotExist):
raise Http404
# вьюха
def redirect_forum(request):
fi = get_object_by_int(ForumImport, pk=request.GET.get('id'))
return HttpResponseRedirect(fi.forum.get_absolute_url())
Вспомогательная функция — замена стандартной get_object_or_404. В случае с джанговскими URL'ами базовый тип параметров в них обычно проверяется еще в urlconf'ах регулярками, а здесь все передается в старой манере — через GET. И вот через них в первые минут 15 работы форума я получил "привет" в виде запросов от реферальных спамеров в духе "viewtopic.php?id=http://...viagra...". Эта штука ломала выборки из базы, потому что URL'ы про виагру к целым числам автоматически не приводятся.
Кстати, еще одно мое недовольство PunBB: на такие спамные запросы он отвечает с кодом 200, а не 404, и их из-за этого трудно вырезать из статистики.
Привязка статей к аккаунтам
Авторизацию в новом форуме я сделал на OpenID — никаких паролей! В старом же форуме авторизация была традиционной. Это означает, что при импорте я не могу автоматически сопоставить старых юзеров и новых. Поэтому при импорте к статье приписывается только имя юзера в поле для гостевой подписи. Но хочется все таки информацию о том, кто что писал, восстановить.
Поэтому у приложения импортера есть еще одна функция: дать человеку место, где он, будучи уже залогиненным в новом форуме, сможет вписать свои старые логин с паролем, заявив тем самым, что "вот этот я новый — тот же самый, что тот я старый". И поскольку соответствие старых статей к новым тоже хранится, в этот момент можно приписать к новому юзеру все его импртированные статьи.
Для этого есть соответственно моделька, которая хранит старые данные юзера и поле со старым ID юзера в импортированных статьях:
class UserImport(models.Model):
id = models.IntegerField(primary_key=True)
username = models.CharField(max_length=200)
password = models.CharField(max_length=40)
class ArticleImport(models.Model):
id = models.IntegerField(primary_key=True)
article = models.ForeignKey(Article)
user = models.ForeignKey(UserImport)
А также стандартный набор из URL'а, вьюхи и формочки, которые эти постинги обрабатывают. Там никакой магии нет. Единственное, что стоит подчеркнуть, что с хранением паролей в PunBB все хорошо: они там в виде sha1-хешей.
urlconf
Для полноты картины хочу описать гибкость urlconf'ов, которые все это обслуживают.
Что нетипично, но абсолютно нормально для джанговских приложений, у cicero_punbb_import urlconf'ов два. Один для привязки статей, другой — для редиректов. Сделано так для тех случаев, когда форум на сайте при смене движка меняет еще и корень URL'а, и в этом случае привязку статей возможно захочется привязать к новому корню, а редиректы конечно должны остаться на старом. Я, впрочем, сам этой идеей не воспользовался, и у меня сейчас главный urlconf проекта выглядит в части форума так:
(r'^forum/', include('cicero_punbb_import.redirect_urls')), # редиректы
(r'^forum/', include('cicero.urls')), # собственно форум
(r'^forum/', include('cicero_punbb_import.urls')), # привязка статей
То есть три совершенно разных схемы все висят внутри одного корня. Тоже нетипично, но тоже никаких космических сложностей не вызывает.
Ну и напоследок о том, как выглядят url'ы для редиректов:
urlpatterns = patterns('',
(r'^viewforum\.php$', views.redirect_forum),
(r'^viewtopic\.php$', views.redirect_topic),
)
Это то самое, за что джанговский urlconf на регулярных выражениях стоит любить: нет никакой жестко заданной структуры типа {controller}/{id}/{action}
, можно описать все что угодно, даже PHP-приложение!
Итог
Ну вот и все! Первая версия Cicero наконец дописана. Единственное, что огорчает, так это то, что заняло это год с четверью, но на то он и неосновной проект, чтобы страдать от невыделяемого времени. Что же, в итоге, получилось из заявленного в первом посте серии:
- автономное подключаемое Джанго-приложение
- форумы с группировкой
- ориентация на короткие топики без всякой (о, ужас) древовидности
- полнотекстовый поиск с морфологией с помощью Sphinx
- большая textarea для постинга!
- bbcode и markdown
- подсветка кода
- лаконичный (даже очень :-) ) интерфейс
- удобное модерирование без лишних интерфейсов и лишних кликов
- отщепление новых топиков от старых
- авторизация по OpenID
- визуализация OpenID эльфами-мутантами!
- рассылка pingback'ов (черт дери WordPress, в котором это сейчас сломано)
- conditional get
- Atom-фиды форумов и топиков
- антиспам через белые списки, ловушки для роботов и Akismet
Также немного осталось за педелами первой версии, но будет доделано когда-то позднее:
- AtomPub
- ajax
- ... и всякие мелочи
Мне кажется, хорошо получилось :-). Прошу осваиваться и непременно сообщать о багах прямо там в форуме для тестов.
Комментарии: 24
Все же мутанты прекрасные получились. Я своего даже в юзерпики в ЖЖ утащил, да и чужих периодически встречаю. Гораздо симпатичнее всех других реализаций автоматических аватар, которые я видел.
Осталось ещё блог перевести..
не отображается кол-во сообщений в топике — неудобно… или непривычно?
Как только markdown-extra на Питоне появится :-)
Уверен, что просто непривычно. Я заметил, что во время пользования тем своим форумом на эту цифру не смотрел никогда. Часто смотрю на автора и дату, но тоже, кажется, без особой необходимости. Впрочем, может что-нибудь из этого еще добавится. Самое важное там — признак непрочитанности.
а реализован ли механизм непрочитанных сообщений, если да то где почитать как?
Да, реализован. Вот тут подробно: http://softwaremaniacs.org/blog/2007/04/19/read-messages/
Иван, а почему Вы отказались от PunBB?
Могу предположить, что just for fun и потому что он не на джанге;)
Иван вы оправдали звание маньяка. Я отпуск бы потратил как раз на отдых от компъютера, где-то в глуши.
Отлично! Вот такой минимализм я обожаю. Замечательный интерфейс. И в блоге стало по-другому. Прямо завидую маленько :) Молодчина, что смог выделить время на всё это. Приятно видеть.
Прошу прощения за второй комментарий, предыдущие прочел только что. Хочу написать про PunBB, что он, конечно, быстр, но код написан ой-ё-ёй... Ни намека на разделение представления и содержания, всё в перемешку. Намучался я с ним.
Пока для PHP мой однозначный выбор - Vanilla.
Проблема в том что без дат (создание темы, последний ответ) форум не воспринимается как форум. Да и автора темы сразу видеть хочется. Перебрали вы сильно с минимализмом Иван в этот раз!
Не отображается количество сообщений в теме.
Ну вот "не воспримниается" — это как раз не проблема. Мне надо, чтобы им было удобно пользоваться, и тут как раз большинство существующих форумов меня не устраивают.
Другое дело, что даты, похоже, и правда нужны. Потому что если не ходить на форум каждый день, то по одному признаку прочитанности невозможно ориентироваться; совершенно теряешься, когда туда смотришь.
Хех :-). Мы, вот, сейчас с друзьями сидим в одном хорватском курортном кафе, и я таки нашел тут WiFi, чтобы на комментарии ответить. Да, я маньяк :-)
imho междустрочный интервал в списке тем побольше надо сделать
или ещё как-то разделить темы визуально
Ну, как же - готовая утилита миграции с punbb на cicero. В репозиторий!
А есть какая-то веская причина по которой для редиректов не использован штатный django.contrib.redirects?
Да, даты обязательно нужны! Без них никак! И интервал междустрочный, про которые говорил alex3d надо действительно увеличить - юзабильнее будет, хотя это тоже моё имхо.
Потому что мне надо переводить одни IDшки в другие, а стандартный редиректор так не умеет, он просто один статический URL на другой меняет.
А предполагается ли публично выкладывать форум? Ну чтобы обычные люди могли пользоваться ;)
Так он, в общем-то, "выложен": svn://softwaremaniacs.org/cicero/
Превращать его в цветущий проект у меня, к сожалению, никакого времени нет, но забрать и использовать у себя его может кто угодно. В одном месте уже даже установлен: http://pyobjc.ru/forum/.
Хм.. Хм.. Хм.. Ну ладно, да, посмотрим :) Но всё равно спасибо ;)
P.S. Ищу для себя небольшие общеупотребительные success strories :) на питоне и django. В этой нише как-то всё очень сыровато...
А конверт в другую субд (из мускуля в постгрис) предусмотрен?
И чего-то ссылки не работают и на сам форум и на конвертор. Что-то изменилось?
up: и еще чего-то не комментит в блог через опен-ид:
Error: please fill the required fields (name, email).
Или это фича странная? :)
Очень странный вопрос :-). Поскольку вся запись в новую базу делается средствами Джанго, то очевидно, какая СУБД в настройках прописана, туда и будут все переезжать.
Да, я переключился с svn на bzr. В ближайшее время все опубликую и обновлю ссылки.
Это убогость WordPress'а с OpenID плагином :-(