<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	>

<channel>
	<title>Маниакальный Веблог &#187; Django</title>
	<atom:link href="http://softwaremaniacs.org/blog/category/django/feed/" rel="self" type="application/rss+xml" />
	<link>http://softwaremaniacs.org/blog</link>
	<description>Иван Сагалаев о программировании и веб-разработке</description>
	<pubDate>Tue, 15 Jul 2008 11:07:54 +0000</pubDate>
	<generator>http://wordpress.org/?v=2.6</generator>
	<language>en</language>
			<item>
		<title>Cicero: месяц спустя</title>
		<link>http://softwaremaniacs.org/blog/2008/07/15/cicero-one-month-later/</link>
		<comments>http://softwaremaniacs.org/blog/2008/07/15/cicero-one-month-later/#comments</comments>
		<pubDate>Tue, 15 Jul 2008 06:42:09 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Cicero]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=342</guid>
		<description><![CDATA[Прошел месяц с переезда моих форумов на Cicero и мне захотелось поделиться тем, как оно все живет и работает. Благо, живет оно, если вкратце, хорошо, и делиться этим приятно :-)



Месяц прошел в неспешном рефакторинге (наконец перевел формы на ModelForm, которых не сущетсвовало, когда начинал их писать), добавлении мелких удобств (автоподсветка URL&#8217;ов, чтение SRE из OpenID) [...]]]></description>
			<content:encoded><![CDATA[<p>Прошел месяц с <a href="/blog/2008/06/15/forum-import-release/">переезда моих форумов на Cicero</a> и мне захотелось поделиться тем, как оно все живет и работает. Благо, живет оно, если вкратце, хорошо, и делиться этим приятно :-)</p>

<p><span id="more-342"></span></p>

<p>Месяц прошел в неспешном рефакторинге (наконец перевел формы на <a href="http://www.djangoproject.com/documentation/modelforms/">ModelForm</a>, которых не сущетсвовало, когда начинал их писать), добавлении мелких удобств (автоподсветка URL&#8217;ов, чтение SRE из OpenID) и правке мелких багов (например, пропускались топики из одних пробелов). В общем, ничего существенного не изменилось.</p>

<p>А вот по ощущениям от использования могу не стесняясь сказать, что меня просто <em>прет</em> от того, как оно работает :-). Радикальный минимализм сначала казался странным, но чем дальше, тем больше привыкаешь к этому свежему ощущению. Не знаю, правда, как оно для остальных участников&#8230;</p>

<h2>Регистрация и спам</h2>

<p>Похоже, я таки <a href="/blog/2005/06/20/forum-registration/">был прав</a>: форум без регистрации живет лучше. По ощущениям, у меня сейчас примерно половина вопросов и обсуждений начинается с незарегистрированных новичков. И это создает контент, полезный для всего форума. То есть, задавание человеком вопроса &#8212; это не только услуга человеку, но и услуга форуму.</p>

<p>А регистрации начинаются уже потом. Вот <a href="http://softwaremaniacs.org/forum/django/2578/">типичный случай</a>.</p>

<p>На прошлом движке (<a href="http://punbb.informer.com/">PunBB</a>) спам приходил сотнями в день. На Cicero спама нет. <em>Совсем</em>. Причем, не только в самом форуме, но и в карантинной очереди. Я четко помню там ровно четыре поста за месяц: одно тестовое, а три других &#8212; false positives, про которые <a href="http://akismet.com/">Акисмет</a> поосторожничал и посчитал спамом, хотя они таковым не являлись. Их я, конечно, пропихнул в форум, научив собачку, что на них лаять больше не надо.</p>

<p>По большей части отсутствие спама я связываю с нестандартной формой постинга. <a href="http://webnewage.org/">Саша Кошелев</a>, правда, предрек мне, что &#8220;со временем научатся&#8221;, но я думаю, что это маловероятно: кому нужно специально затачивать роботов под форум, существующий пока в единственном экземпляре, на котором общаются гики?</p>

<h2>Фичи Cicero</h2>

<p>Если говорить о специфических фичах, то уже пару раз я <a href="http://softwaremaniacs.org/forum/web/2493/#10258">воспользовался отщеплением топиков</a> &#8212; это когда поднимается вопрос, явно выходящий за пределы текущей темы. Результат пока нравится.</p>

<p>А еще я, кажется, стал узнавать &#8220;в лицо&#8221; некоторых мутантов :-).</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/07/15/cicero-one-month-later/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Спринт финишировал</title>
		<link>http://softwaremaniacs.org/blog/2008/07/13/sprint-finished/</link>
		<comments>http://softwaremaniacs.org/blog/2008/07/13/sprint-finished/#comments</comments>
		<pubDate>Sun, 13 Jul 2008 12:59:32 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=341</guid>
		<description><![CDATA[Ну что же, мы поспринтовали! С меня &#8212; короткий отчет.



Из собиравшихся 30 человек пришли, если не ошибаюсь, 18. Учитывая всякие тенические накладки, начали спринт мы где-то через час после назначенного времени. WiFi был, электричество тоже :-).

И тут я должен сказать, что думал, что этого будет достаточно. Но ошибся. Довольно скоро стало понятно, что схема, когда [...]]]></description>
			<content:encoded><![CDATA[<p>Ну что же, мы <a href="/blog/2008/07/03/django-sprin/">поспринтовали</a>! С меня &#8212; короткий отчет.</p>

<p><span id="more-341"></span></p>

<p>Из собиравшихся 30 человек пришли, если не ошибаюсь, 18. Учитывая всякие тенические накладки, начали спринт мы где-то через час после назначенного времени. WiFi был, электричество тоже :-).</p>

<p>И тут я должен сказать, что думал, что этого будет достаточно. Но ошибся. Довольно скоро стало понятно, что схема, когда каждый выбирает себе тикеты, не очень работает. Особенно, когда среди пришедших есть те, кто только хочет попробовать себя в работе над откытым проектом. Поэтому ставлю себе галочку, что внутренний процесс все-таки надо подробно организовывать.</p>

<p>Еще одна штука, которая явилась для меня откровением, что 5 часов на такое событие мало; у нас в офисе в тот день полы менялись, поэтому около шести вечера нас выгнали. Мало, потому что первые часса три ушли у всех на то, чтобы &#8220;войти в поток&#8221; и начать эффетктивно работать. Под конец, я заметил, работа пошла веселей. Да еще и на IRC-канале появился один из джанговский triager&#8217;ов и прямо в онлайне несколько наших тикетов подвинул в &#8220;Ready for checkin&#8221;, а также дал несколько советов.</p>

<p>В итоге, тикетов мы порешали <a href="http://code.djangoproject.com/query?keywords=~yandex-sprint&amp;order=priority">не очень много</a>. Но было несколько интересных обсуждений. А особенно мне запомнились две вещи:</p>

<ul>
<li><p><a href="http://barbuza.info/">Виктор</a> бессстрашно взялся за древний <a href="http://code.djangoproject.com/ticket/13">ticket 13</a>, и добился там неплохого прогресса (Викто, патчик доделаешь?) Тикет этот во время спринта успел обрасти собственным издевательским фольклором: &#8220;не знаешь что делать &#8212; а вон, возьми тринадцатый&#8221; :-)</p></li>
<li><p>Трое джангистов &#8212; <a href="http://gsql.org/">Тарас Халтурин</a>, <a href="http://oraclub.ru/">Николай Лещев</a> и <a href="http://plesser.livejournal.com/">Олег Плессер</a> сваяли целый <a href="http://code.djangoproject.com/ticket/7732">БД-бэкенд для Оракла с поддержкой пула коннектов</a>. Базовое &#8220;тупое&#8221; приложение с ним ускорилось в три раза.</p></li>
</ul>

<p>Ну а в завершение мероприятия мы накормили программистов пиццей. Кажется, все были довольны :-). Я считаю, будем собираться еще!</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/07/13/sprint-finished/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Спринт!</title>
		<link>http://softwaremaniacs.org/blog/2008/07/03/django-sprin/</link>
		<comments>http://softwaremaniacs.org/blog/2008/07/03/django-sprin/#comments</comments>
		<pubDate>Thu, 03 Jul 2008 10:42:13 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=338</guid>
		<description><![CDATA[Итак, вроде бы мы все утрясли, и спринту быть! Чтобы не повторяться о подробностях и порядке &#8220;регистрации&#8221;, сошлюсь на свой пост в корп-блог.

Да, вот забыл: кто уже написал в прошлом посте, что придет, пришлите пожалуйста еще разок свои имя и фамилию мне на &#105;&#115;&#97;&#103;&#97;&#108;&#97;&#101;&#118;&#64;&#121;&#x61;&#x6e;&#x64;&#x65;&#x78;&#x2d;&#x74;&#x65;&#x61;&#x6d;&#x2e;&#x72;&#x75;. Надо надо заявку в охрану сделать.
]]></description>
			<content:encoded><![CDATA[<p>Итак, вроде бы мы все утрясли, и спринту быть! Чтобы не повторяться о подробностях и порядке &#8220;регистрации&#8221;, сошлюсь на свой <a href="http://company.yandex.ru/blog/message.xml?msg=102921">пост в корп-блог</a>.</p>

<p><strong>Да, вот забыл</strong>: кто уже написал в прошлом посте, что придет, пришлите пожалуйста еще разок свои имя и фамилию мне на <a href="mai&#108;&#116;&#111;&#58;&#105;&#115;&#97;&#103;&#97;&#108;&#97;&#101;&#118;&#64;&#121;&#x61;&#x6e;&#x64;&#x65;&#x78;&#x2d;&#x74;&#x65;&#x61;&#x6d;&#x2e;&#x72;&#x75;">&#105;&#115;&#97;&#103;&#97;&#108;&#97;&#101;&#118;&#64;&#121;&#x61;&#x6e;&#x64;&#x65;&#x78;&#x2d;&#x74;&#x65;&#x61;&#x6d;&#x2e;&#x72;&#x75;</a>. Надо надо заявку в охрану сделать.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/07/03/django-sprin/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Может, спринт?</title>
		<link>http://softwaremaniacs.org/blog/2008/06/30/fancy-a-sprint/</link>
		<comments>http://softwaremaniacs.org/blog/2008/06/30/fancy-a-sprint/#comments</comments>
		<pubDate>Mon, 30 Jun 2008 13:33:53 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=337</guid>
		<description><![CDATA[У меня есть идея провести 10-12 июля (четверг-суббота) Джанго-спринт в стенах Яндекса в Москве. То есть, собраться заинтересованным людям командой и поработать над Джангой вживую вместе с остальным миром. Описание грядущего спринта уже какое-то время лежит на wiki. Вкратце &#8212; будем доделывать newforms-admin.

В Яндексе у меня есть предварительная договоренность, что технически мы это организовать сможем. [...]]]></description>
			<content:encoded><![CDATA[<p>У меня есть идея провести 10-12 июля (четверг-суббота) Джанго-спринт в стенах Яндекса в Москве. То есть, собраться заинтересованным людям командой и поработать над Джангой вживую вместе с остальным миром. <a href="http://code.djangoproject.com/wiki/SprintEuroPython2008">Описание грядущего спринта</a> уже какое-то время лежит на wiki. Вкратце &#8212; будем доделывать newforms-admin.</p>

<p>В Яндексе у меня есть предварительная договоренность, что технически мы это организовать сможем. Пожалуйста, напишите тут в комментариях, кому это было бы интересно, и когда было бы удобно прийти.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/06/30/fancy-a-sprint/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Импорт и релиз форума</title>
		<link>http://softwaremaniacs.org/blog/2008/06/15/forum-import-release/</link>
		<comments>http://softwaremaniacs.org/blog/2008/06/15/forum-import-release/#comments</comments>
		<pubDate>Sat, 14 Jun 2008 21:03:02 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Cicero]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=334</guid>
		<description><![CDATA[Вчера рано ночью случилось радостное событие: я переключил свои форумы на движок Cicero! Удачно использовав первую часть отпуска, я наконец доделал две последние абсолютно необходимые вещи: импорт содержимого предыдущего форума и верстку.

И вот как раз импорту я и посвящаю последний подробный технический обзор в серии Cicero. Дальше форум будет развиваться уже по мелочам и без [...]]]></description>
			<content:encoded><![CDATA[<p>Вчера рано ночью случилось радостное событие: я переключил <a href="http://softwaremaniacs.org/forum/">свои форумы</a> на движок Cicero! Удачно использовав первую часть отпуска, я наконец доделал две последние абсолютно необходимые вещи: импорт содержимого предыдущего форума и верстку.</p>

<p>И вот как раз импорту я и посвящаю последний подробный технический обзор в <a href="http://softwaremaniacs.org/blog/category/django/cicero/">серии Cicero</a>. Дальше форум будет развиваться уже по мелочам и без постописательства.</p>

<p><span id="more-334"></span></p>

<table class="simple">
  <tr>
    <th>SVN
    <td>svn://softwaremaniacs.org/cicero/tools/cicero_punbb_import
  <tr>
    <th>Архив последней ревизии
    <td><a href="http://softwaremaniacs.org/media/cicero_punbb_import.zip">http://softwaremaniacs.org/media/cicero_punbb_import.zip</a>
</table>

<p>Импорт, вообще, более точно стоит назвать &#8220;миграцией&#8221;, которая состоит из нескольких операций:</p>

<ul>
<li>импорт форумов, топиков, статей и пользователей из старой базы</li>
<li>организация редиректов для поддержки старых ссылок</li>
<li>привязка статей к новым аккаунтам</li>
</ul>

<p>Для всего этого я сделал отдельное от Cicero приложение. Так это лучше укладывалось в голову при программировании, а также позволяет не таскать этот кусок к себе тем, кому импорт не нужен.</p>

<h2>Импорт контента</h2>

<p>Структурно импорт оказался очень простым. Движок <a href="http://punbb.informer.com/">PunBB</a>, оказывается, разделяет форум на очень похожие модели: там тоже есть таблицы форумов, топиков, статей и пользователей. Поля только немного другие. Поэтому сам алгоритм выливается в &#8220;сделать select, создать по результатам новые записи&#8221;.</p>

<p>Оформлено это все в виде пользовательской джанго-команды &#8212; самый лучший способ оформлять подобные вещи в Джанго, потому что вы получаете много мелких удобств: автоматизированный парсинг аргументов и опций, вывод help&#8217;а, сообщения об ошибках, настроенную джанго-среду. Пользователю же в итоге остается только установить приложение cicero_punbb_import и сказать в консоли:</p>

<pre><code>./manage.py import_punbb &lt;databasename&gt;
</code></pre>

<p>В коде это выглядит примерно так. В папке приложения создается файл management/commands/import_punbb.py, в котором определяется класс Command:</p>

<pre><code>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 = '&lt;database&gt;'

  # Основная функция обработки команды
  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()

# сами функции импорта не очень интересные и опущены для краткости
</code></pre>

<p class="note"><small>Привожу я этот код так подробно отчасти из-за того, что получилось все красиво, а воспользоваться этим у меня вышло всего один раз. Обидно!</small></p>

<p>Еще один момент, на котором остановлюсь &#8212; это опция &#8220;&#8211;real-charset&#8221;. Так вышло, что когда-то давно, когда я <a href="/blog/2007/01/19/moved-to-tektonic/">переезжал на новый хостинг</a>, и перетаскивал с собой БД, я очень долго мучался, пытаясь заставить MySQL хранить utf-8 данные, а форум &#8212; их корректно показывать. Мучился, потому что я тогда ничего не знал про MySQL в этом смысле. Так вот теперь оказалось, что тогда у меня получилось все криво: данные в таблицах закодированы в utf-8, но зато сами таблицы думают про них, что это &#8212; latin1. В итоге, если положиться на MySQLdb, чтобы он сам раскодировал данные, они приходят очевидно побитые.</p>

<p>Поэтому я и придумал опцию &#8211;real-charset, в которую пользователь может написать кодировку, которую ему надо. В этом случае я забираю из БД не юникод, а просто байты и раскодировку провожу уже вручную, decode&#8217;ом:</p>

<pre><code>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,
  )
</code></pre>

<h2>Редиректы</h2>

<p>Редиректы &#8212; это очень важно для моего форума. Одна из его основных целей в том, чтобы служить базой знаний, в которую можно сослаться. И моя статистика показывает, что это востребовано: ссылок извне на мой форум довольно много. Поэтому ломать их мне ни в коем случае нельзя.</p>

<p>Для этой цели в приложении cicero_punbb_import определены несколько моделей, которые фактически хранят соответствие старых ID форумов, топиков и статей их новым импортированным копиям.</p>

<pre><code>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)
</code></pre>

<p>Соответственно при импорте создаются не только записи в таблицах самого Сicero, но и вот эти, служебные. Дальше обслуживание редиректов &#8212; дело пары вьюх, которые по старым IDшникам ищут новые и возвращают HttpResponseRedirect:</p>

<pre><code># вспомогательная функция
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())
</code></pre>

<p>Вспомогательная функция &#8212; замена стандартной get_object_or_404. В случае с джанговскими URL&#8217;ами базовый тип параметров в них обычно проверяется еще в urlconf&#8217;ах регулярками, а здесь все передается в старой манере &#8212; через GET. И вот через них в первые минут 15 работы форума я получил &#8220;привет&#8221; в виде запросов от реферальных спамеров в духе &#8220;viewtopic.php?id=http://&#8230;viagra&#8230;&#8221;. Эта штука ломала выборки из базы, потому что URL&#8217;ы про виагру к целым числам автоматически не приводятся.</p>

<p class="note"><small>Кстати, еще одно мое недовольство PunBB: на такие спамные запросы он отвечает с кодом 200, а не 404, и их из-за этого трудно вырезать из статистики.</small></p>

<h2>Привязка статей к аккаунтам</h2>

<p>Авторизацию в новом форуме я сделал на OpenID &#8212; никаких паролей! В старом же форуме авторизация была традиционной. Это означает, что при импорте я не могу автоматически сопоставить старых юзеров и новых. Поэтому при импорте к статье приписывается только имя юзера в поле для гостевой подписи. Но хочется все таки информацию о том, кто что писал, восстановить.</p>

<p>Поэтому у приложения импортера есть еще одна функция: дать человеку место, где он, будучи уже залогиненным в новом форуме, сможет вписать свои старые логин с паролем, заявив тем самым, что &#8220;вот этот я новый &#8212; тот же самый, что тот я старый&#8221;. И поскольку соответствие старых статей к новым тоже хранится, в этот момент можно приписать к новому юзеру все его импртированные статьи.</p>

<p>Для этого есть соответственно моделька, которая хранит старые данные юзера и поле со старым ID юзера в импортированных статьях:</p>

<pre><code>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)
</code></pre>

<p>А также стандартный набор из URL&#8217;а, вьюхи и формочки, которые эти постинги обрабатывают. Там никакой магии нет. Единственное, что стоит подчеркнуть, что с хранением паролей в PunBB все хорошо: они там в виде sha1-хешей.</p>

<h2>urlconf</h2>

<p>Для полноты картины хочу описать гибкость urlconf&#8217;ов, которые все это обслуживают.</p>

<p>Что нетипично, но абсолютно нормально для джанговских приложений, у cicero_punbb_import urlconf&#8217;ов два. Один для привязки статей, другой &#8212; для редиректов. Сделано так для тех случаев, когда форум на сайте при смене движка меняет еще и корень URL&#8217;а, и в этом случае привязку статей возможно захочется привязать к новому корню, а редиректы конечно должны остаться на старом. Я, впрочем, сам этой идеей не воспользовался, и у меня сейчас главный urlconf проекта выглядит в части форума так:</p>

<pre><code>(r'^forum/', include('cicero_punbb_import.redirect_urls')), # редиректы
(r'^forum/', include('cicero.urls')),                       # собственно форум
(r'^forum/', include('cicero_punbb_import.urls')),          # привязка статей
</code></pre>

<p>То есть три совершенно разных схемы все висят внутри одного корня. Тоже нетипично, но тоже никаких космических сложностей не вызывает.</p>

<p>Ну и напоследок о том, как выглядят url&#8217;ы для редиректов:</p>

<pre><code>urlpatterns = patterns('',
  (r'^viewforum\.php$', views.redirect_forum),
  (r'^viewtopic\.php$', views.redirect_topic),
)
</code></pre>

<p>Это то самое, за что джанговский urlconf на регулярных выражениях стоит любить: нет никакой жестко заданной структуры типа <code>{controller}/{id}/{action}</code>, можно описать все что угодно, даже PHP-приложение!</p>

<h2>Итог</h2>

<p>Ну вот и все! Первая версия Cicero наконец дописана. Единственное, что огорчает, так это то, что заняло это год с четверью, но на то он и неосновной проект, чтобы страдать от невыделяемого времени. Что же, в итоге, получилось из заявленного в <a href="/blog/2007/03/01/cicero/">первом посте</a> серии:</p>

<ul>
<li>автономное подключаемое Джанго-приложение</li>
<li>форумы с группировкой</li>
<li>ориентация на короткие топики без всякой (о, ужас) древовидности</li>
<li>полнотекстовый поиск с морфологией с помощью <a href="http://www.sphinxsearch.com/">Sphinx</a></li>
<li>большая textarea для постинга!</li>
<li>bbcode и markdown</li>
<li>подсветка кода</li>
<li>лаконичный (даже очень :-) ) интерфейс</li>
<li>удобное модерирование без лишних интерфейсов и лишних кликов</li>
<li>отщепление новых топиков от старых</li>
<li>авторизация по OpenID</li>
<li>визуализация OpenID эльфами-мутантами!</li>
<li>рассылка pingback&#8217;ов (черт дери WordPress, в котором это сейчас сломано)</li>
<li>conditional get</li>
<li>Atom-фиды форумов и топиков</li>
<li>антиспам через белые списки, ловушки для роботов и <a href="http://akismet.com/">Akismet</a></li>
</ul>

<p>Также немного осталось за педелами первой версии, но будет доделано когда-то позднее:</p>

<ul>
<li>AtomPub</li>
<li>ajax</li>
<li>&#8230; и всякие мелочи</li>
</ul>

<p>Мне кажется, хорошо получилось :-). Прошу <a href="http://softwaremaniacs.org/forum/">осваиваться</a> и непременно сообщать о багах прямо там в <a href="http://softwaremaniacs.org/forum/test/">форуме для тестов</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/06/15/forum-import-release/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Exception #08. Отчет.</title>
		<link>http://softwaremaniacs.org/blog/2008/06/02/exception-08-report/</link>
		<comments>http://softwaremaniacs.org/blog/2008/06/02/exception-08-report/#comments</comments>
		<pubDate>Sun, 01 Jun 2008 23:21:19 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=331</guid>
		<description><![CDATA[

Аж на неделю задержался я с отчетом об Exception Masterclass #08. Да и сейчас тоже не все материалы выложу &#8212; видео я пока так и не пожал. Доделаю на следующей неделе, видимо.

UPDATE: Доделал



Киев

На этот Киев своих гостей погодой не радовал. В оба дня события было прохладно, ветренно и временами дождливо. Поэтому как-то я так и [...]]]></description>
			<content:encoded><![CDATA[<p class="left picture"><img src="/blog/wp-content/exception-bage.jpg"></p>

<p>Аж на неделю задержался я с отчетом об <a href="http://exception.org.ua/events/exception-masterclass-08/">Exception Masterclass #08</a>. Да и сейчас тоже не все материалы выложу &#8212; видео я пока так и не пожал. Доделаю на следующей неделе, видимо.</p>

<p><strong>UPDATE</strong>: <a href="http://softwaremaniacs.org/blog/2008/06/02/exception-08-report/#comment-30179">Доделал</a></p>

<p><span id="more-331"></span></p>

<h2>Киев</h2>

<p>На этот Киев своих гостей погодой не радовал. В оба дня события было прохладно, ветренно и временами дождливо. Поэтому как-то я так и не прочувствовал, что же это за золотое время такое в Киеве &#8212; конец мая, когда тепло и цветут каштаны. Придется ехать еще :-).</p>

<p>Фоток на этот раз не так много, потому что большая часть времени была посвящена подготовке к мероприятию, да и в Киеве я свои обязательные туристические фотографии отснимал в прошлый раз. На этот же раз Дима Кожевин <a href="http://fotki.yandex.ru/users/isagalaev/album/39238/">прогулял нас по Оболони</a>. Фотографии с самого мероприятия &#8212; <a href="http://fotki.yandex.ru/users/isagalaev/album/39100/">в отдельном альбоме</a>.</p>

<h2>Мастер-класс</h2>

<p>Никогда не делал мастер-классов. Соответственно, усиленно готовился, вплоть до ночи перед выступлением. И судя по всему, не зря: многие говорят, что рассказ получился складным.</p>

<p class="note"><small>А точнее, говорят в том духе, что &#8220;тема дурацкая, но представлена была хорошо&#8221; :-).</small></p>

<p>Я рассказывал и показывал то, как можно аккуратно внедрить джанговское приложение внутрь приложения на Pylons &#8212; мой собственный форум <a href="http://softwaremaniacs.org/blog/category/django/cicero/">Cicero</a> в пилонсовскую <a href="http://wiki.pylonshq.com/display/pylonsdocs/QuickWiki+Tutorial">QuickWiki</a> соответственно. По идее, все получилось более менее, как было задумано. Да и народ, похоже, не скучал. Огромной практической отдачи, правда, никто не вынес, но кое-кто благодарил за интересные мысли. Я рад :-).</p>

<p>Как водится, выкладываю материалы. На этот раз это не слайды, а архив <a href="/blog/wp-content/django-in-pylons.zip">django-in-pylons.zip</a> с bazaar-репозиторием, в котором по шагам записаны изменения кода по ходу мастер-класса. Правда, читать его &#8220;на сон грядущий&#8221; все таки не очень интересно, в связи с чем я собираюсь выложить видео с выступлений.</p>

<p>А вот Андрею хватило энергии превратить свой мастер-класс &#8220;<a href="http://homo-virtualis.livejournal.com/25634.html">Поиск &#8220;утечек&#8221; памяти в python-программе</a>&#8221; в полноценный доклад, который можно читать прямо сейчас.</p>

<h2>Забытый вывод</h2>

<p>Я таки забыл сказать самое главное, что хотел сказать этим самым мастер-классом, и что собственно и сформулировал для себя самого во время его подготовки. Скажу сейчас.</p>

<p>Интеграция сама по себе &#8212; это не та задача, которую можно решить каким-то обощенным способом для всего на свете. Интегрируются вещи только двумя способами:</p>

<ul>
<li>либо кто-то пишет код для совместной работы каких-то <em>конкретных</em> существущих компонент</li>
<li>либо существует стандартный протокол, связывающий любые компоненты, которые его используют</li>
</ul>

<p>Второе очевидно универсальней, но придумать протоколы для всего заранее невозможно. Наоборот, они возникают тогда, когда люди понимают, что похожую задачу уже кто-то решал. Несколько раз. И похоже, она будет решаться еще.</p>

<p>В частности в мире питоновского веб-программирования мы обязаны неплохими возможностями по интеграции двум протоколам: <a href="http://www.wsgi.org/">WSGI</a> и <a href="http://www.python.org/dev/peps/pep-0249/">DB API</a>. А отнюдь не какой-то специальной магии, которая присутствует в фреймворках, которые вокруг них строятся.</p>

<h2>Видео</h2>

<p>Сразу извинюсь, что к сожалению мы догадались, что &#8220;а неплохо бы было это все заснять, ведь у Саши есть камера&#8221; только после доклада наших коллег Андрея Светлова и Юрия Богданова, поэтому с ними видео нет :-(. Да еще и во время рассказа нашего Андрея Татаринова в камере кончилась батарейка, в результате чего потерялся кусок из середины&#8230;</p>

<p>Тем не менее, у меня на компьютере образовалось 7 ГБ видео, которое я пока думаю, как пожать.</p>

<p class="note"><small>К слову, ситуация с линуксовым софтом для такой, вроде бы, нередкой задачи как превращение сырого видео в то, что можно выложить людям, просто ужасная. Софт либо непроходимо сложен, либо непроходимо убог, либо глючит.</small></p>

<p>В связи с этим у меня есть вопрос к читателям. В видео присутствует &#8220;много букв&#8221; (поскольку мастер-класс заключается в их наборе на клавиатуре), и поэтому мне не хочется ужимать размер очень сильно. Как вы думаете, нормально ли будет сделать результирующие файлик общим размером где-то на 700 МБ? Или лучше поменьше? И многие ли не смогут просмотреть его, если оно будет в OGG?</p>

<h2>Технические детали</h2>

<p>Несколько разрозненных мыслей посетило нас с товарищами, которыми хочется поделиться:</p>

<ul>
<li><p>Чтобы записывать мастер-классы, нужны на самом деле два потока информации: отдельно видеосъемка оратора и отдельно &#8212; скринкаст того, что происходит на экране. И смотреть их надо синхронно.</p></li>
<li><p>Графические среды современных линуксов и маков обладают функцией плавного увеличения изображения, которая оказалась как нельзя кстати как раз для мастер-класса. Перед любым набором текста мы с Андреем просто приближали тот участок, в котором набираются буквы, и всем все было видно (кажется).</p></li>
<li><p>Светлая аудитория для таких мероприятий &#8212; зло.</p></li>
</ul>

<h2>Напоследок</h2>

<p>Снова большое спасибо <a href="http://www.ivanpirog.com/">Ивану Пирогу</a> и его друзьям и спонсорам, что не устают собирать такую замечательную тусовку. Хочется возвращаться и звать с собой друзей.</p>

<p class="center picture"><img src="/blog/wp-content/booth-girls.jpg"></p>

<p>Приезжайте! На Exception&#8217;е &#8212; <em>хорошо</em> ;-)</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/06/02/exception-08-report/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Тема на Exception #08</title>
		<link>http://softwaremaniacs.org/blog/2008/05/16/exception-08-theme/</link>
		<comments>http://softwaremaniacs.org/blog/2008/05/16/exception-08-theme/#comments</comments>
		<pubDate>Fri, 16 May 2008 15:48:05 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=329</guid>
		<description><![CDATA[Exception успел поменять номер с прошлого поста :-). А я таки определился с темой: по совету Макса Ищенко буду интегрировать Django с каким-нибудь WSGI-проектом. Причем, видимо, на Pylons.

Мы с ним также наметили подробнее, что именно входит в понятие &#8220;интеграция&#8221;.




включение Джанго-приложение в URL-пространство чужого проекта с корректной генерацией ссылок
ввязывание шаблона приложения на Джанго в общепроектные шапки/футеры
использование [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://exception.org.ua/events/exception-masterclass-08/">Exception</a> успел поменять номер с прошлого поста :-). А я таки определился с темой: по <a href="/blog/2008/05/07/exception-masterclass-09/#comment-29991">совету Макса Ищенко</a> буду интегрировать Django с каким-нибудь WSGI-проектом. Причем, видимо, на Pylons.</p>

<p>Мы с ним также наметили подробнее, что именно входит в понятие &#8220;интеграция&#8221;.</p>

<p><span id="more-329"></span></p>

<ul>
<li>включение Джанго-приложение в URL-пространство чужого проекта с корректной генерацией ссылок</li>
<li>ввязывание шаблона приложения на Джанго в общепроектные шапки/футеры</li>
<li>использование пула соединений SQLAlchemy для ORM Джанго</li>
</ul>

<p>Интрига в том, что я хотя я и не вижу принципиальных проблем для всего перечисленного, в точности я этого тоже не представляю :-). Так что буду за время до мастер-класса все это и исследовать. Может быть даже что и поглубже получится&#8230;</p>

<p>О других темах, предложенных в комментариях к предыдущему посту, хочу всем сказать <strong>большое спасибо</strong>! Я до последнего момента динамил Ивана Пирога тем, что не мог выбрать тему, потому что популярная в комментариях идея использования не-СУБД источников данных тоже меня привлекает.</p>

<p>Беда в том, что я не смог придумать, в чем же конкретно будет заключаться интеграция. Например можно представить себе некий шаблонный тег, который вытягивает с Last.fm топ песен для текущего юзера. Но только Джанго тут ни причем: просто HTTP-запрос, просто XML-результаты, просто вывод их в HTML&#8230; Еще была мысль: сделать queryset, который будет вытягивать какие-нибудь, скажем, закладки с del.icio.us. Беда только, что он будет бесполезен: какой смысл делать выборку в виде queryset&#8217;а, если эта выборка прекрасно живет в виде обычного питоновского списка. Никакая ленивость и прочие вкусности джанговского DB API тут не имеют смысла просто.</p>

<p>Вот как-то так. Впрочем, сама мысль никуда не делась, и возможно во что-нибудь еще выродится.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/05/16/exception-08-theme/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Exception Masterclass #09</title>
		<link>http://softwaremaniacs.org/blog/2008/05/07/exception-masterclass-09/</link>
		<comments>http://softwaremaniacs.org/blog/2008/05/07/exception-masterclass-09/#comments</comments>
		<pubDate>Wed, 07 May 2008 16:54:56 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=327</guid>
		<description><![CDATA[24 мая в Киеве состоится очередной из серии семинаров &#8212; Exception Masterclass #09. Как уже поняли наиболее прозорливые читатели, семинар будет проходит в виде мастер-класса. И как догадываются самые прозорливые из тех прозорливых, я пишу про это, потому что буду там с одним из мастер-классов выступать. Ура!

Однако у меня есть небольшая проблема, за помощью с [...]]]></description>
			<content:encoded><![CDATA[<p><span class="first-letter">24</span> мая в Киеве состоится очередной из серии семинаров &#8212; <a href="http://exception.org.ua/">Exception Masterclass #09</a>. Как уже поняли наиболее прозорливые читатели, семинар будет проходит в виде мастер-класса. И как догадываются самые прозорливые из тех прозорливых, я пишу про это, потому что буду там с одним из мастер-классов выступать. Ура!</p>

<p>Однако у меня есть небольшая проблема, за помощью с которой я хочу обратиться к джанговско-питоньему коммьюнити.</p>

<p><span id="more-327"></span></p>

<p>Я пока никак не могу выбрать, по какой теме сделать выступление. Пока идеи такие:</p>

<ul>
<li><p>Так как меня порядком анноят утверждения, что Джанго &#8220;монолитная&#8221; и &#8220;негибкая&#8221;, я хочу в прямом эфире сделать какую-нибудь несложную интеграционную фенечку. Например написать авторизационный бэкенд с внешним источником. Правда, слова &#8220;авторизационный бэкенд&#8221; вряд ли заставят кого-то биться в экстазе восторга, поэтому я прошу идей, что бы такое сделать в области увязывания Джанго с чем-нибудь еще.</p></li>
<li><p>В связи с недавним рефакторингом queryset&#8217;а можно написать какой-нибудь интересный пользовательский queryset, который будет работать, скажем, не с БД, а с чем-нибудь еще. С чем? Тоже пока точно не знаю&#8230;</p></li>
<li><p>Можно написать какой-нибудь интересный виджет для админки.</p></li>
</ul>

<p>Это пока все, что пришло в голову. Поделитесь, пожалуйста, идейками!</p>

<p>Больше того, идей может быть больше одной, потому что мы туда поедем вместе с моим коллегой <a href="http://homo-virtualis.livejournal.com/">Андреем Татариновым</a>, тем самым, которого я <a href="http://softwaremaniacs.org/blog/wp-content/exception06/#slide13">цитировал</a> на прошлом семинаре :-). Он тоже будет показывать что-то интересное, и тоже пока не знает, что именно.</p>

<p>Ну а в общем &#8212; приезжайте/приходите все на семинар. Познакомимся, пообщаемся!</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/05/07/exception-masterclass-09/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Антиспам и сеть белых списков</title>
		<link>http://softwaremaniacs.org/blog/2008/04/28/antispam-and-social-whitelisting/</link>
		<comments>http://softwaremaniacs.org/blog/2008/04/28/antispam-and-social-whitelisting/#comments</comments>
		<pubDate>Mon, 28 Apr 2008 03:47:13 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Cicero]]></category>

		<category><![CDATA[OpenID]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=326</guid>
		<description><![CDATA[Вы не представляете, как меня достал антиспамный плагин в моем текущем форуме на движке PunBB :-(. Он ловит действительно много спама, но он мне складывает этот спам в карантин пачками по 150 сообщений несколько раз в день, и мне надо это все глазками просматривать и ручками удалять. Причем эти сообщения он старательно рисует полностью, а [...]]]></description>
			<content:encoded><![CDATA[<p>Вы не представляете, как меня достал антиспамный плагин в моем текущем форуме на движке PunBB :-(. Он ловит действительно много спама, но он мне складывает этот спам в карантин пачками по 150 сообщений несколько раз в день, и мне надо это все глазками просматривать и ручками удалять. Причем эти сообщения он старательно рисует полностью, а многие из них сами занимают по нескольку экранов. А не глядя удалять спам я не могу, потому что никакая эвристика (там работает <a href="http://akismet.com/">Akismet</a>) не гарантирует отсутствие ham&#8217;а. И тут очередная беда: когда ошибочно задержанное письмо объявляешь чистым, плагин не сообщает об этом в Akismet, и тот не тренирует свой анализатор. <a href="http://webnewage.org/">Саша Кошелев</a> не даст соврать &#8212; его посты я доставал из карантина чуть не пару месяцев кряду.</p>

<p>Это, как ничто другое, сподвигло меня взять себя в руки и вернуться к <a href="http://softwaremaniacs.org/blog/category/django/cicero/">Cicero</a>. Ему, по большому счета, только антиспама и не хватало до релиза.</p>

<p><span id="more-326"></span></p>

<table class="simple">
  <tr>
    <th>SVN
    <td>svn://softwaremaniacs.org/cicero/trunk
  <tr>
    <th>Архив последней ревизии
    <td><a href="http://softwaremaniacs.org/media/cicero.zip">http://softwaremaniacs.org/media/cicero.zip</a>
  <tr>
    <th>Тестовая инсталляция
    <td><a href="http://softwaremaniacs.org/cicero/">http://softwaremaniacs.org/cicero/</a>
</table>

<h3>Антиспамная цепочка</h3>

<p>Сначала я приведу кусочек кода, а потом разъясню идею за ним.</p>

<pre><code>def spam_validators():
  for module_name in settings.ANTISPAM_PLUGINS:
    yield __import__(module_name, {}, {}, [''])

def validate(request, article, is_new_topic):
  status = None
  for module in spam_validators():
    result = module.validate(request, article, is_new_topic)
    if result in ['clean', 'spam']:
      return result
    if result is not None:
      status = result
  return status or 'clean'
</code></pre>

<p>Анализ спама в Cicero делается цепочкой фильтров. Каждый фильтр &#8212; это функция, которая получает на вход созданную пользователем статью и сопровождающие параметры запроса. Эта функция смотрит на переданную информацию и может вернуть четыре варианта ответа:</p>

<ul>
<li><code>'clean'</code>: точно не спам</li>
<li><code>'spam'</code>: точно спам</li>
<li><code>'suspect'</code>: возможно спам</li>
<li><code>None</code>: нет никакого определенного ответа</li>
</ul>

<p>Если фильтр в цепочке точно уверен в своем заключении (возвращает &#8220;clean&#8221; или &#8220;spam&#8221;), то обработка прерывается и этот ответ считается окончательным. При других вариантах ответа обработка продолжается.</p>

<p>У этого нюанса есть свои причины. Автоматическая проверка на спам по сути своей операция с нечетким ответом. Однако эта нечеткость бывает разная. Например если статья идет от имени давно зарегистрированного пользователя, то она практически точно не спамная (&#8221;практически&#8221; &#8212; это на случай, если человека заставляли запостить спам, угрожая проигрыванием песен Ф. Киркорова). Аналогично, есть проверки, показывающие, что пост &#8212; практически точно спам. А вот анализу контента сервисами вроде Akismet&#8217;а доверять так без оглядки уже нельзя. Такое разделение очень полезно, потому что более четкие проверки, прекращая обработку поста, позволяют разгрузить карантинную очередь. А это, как я убедился (см. начало статьи) &#8212; <em>очень важно</em> в первую очередь для здоровья модератора.</p>

<p class="picture right frame"><img src="/blog/wp-content/mojito.jpg"><br /><small>Фото с <a href="http://www.drinkinghabits.com/">drinkinghabits.com</a></small></p>

<p>Далее, если ни один из фильтров цепочки не дал никакого определенного ответа, то новой статье присваивается статус &#8220;clean&#8221;. Я решил, что перестраховка (автоматический &#8220;suspect&#8221;) здесь будет работать плохо. Во-первых, мне кажется, что пролезшего по ошибке спама, который модераторам придется вычищать, будет меньше, чем нормальных статей, которые пришлось бы пропускать. А во-вторых, это позволит форуму, хоть и со спамом, но функционировать, когда модераторы не захотят уделять ему много времени, лежа под зонтиком на белом песке на удаленном карибском пляже, потягивая мохито из заиндевелого бокальчика.</p>

<p>Настройка settinngs.ANTISPAM_PLUGINS содержит строчки питоновских путей к модулям, в лучших традициях Джанго. Каждый модуль должен содержать функцию <code>validate(request, article, is_new_topic)</code>, которая и вызывается для проверки. Все это теоретически позволяет сказать, что Cicero позволяет подключать сторонние фильтры, но по-честному этого недостаточно. Потому что помимо информации, которая пришла извне, фильтру нужно минимум еще уметь влиять на содержимое формы во время рисования. А этого пока никак сделать нельзя. Поэтому можно пока считать, что архитектура допускает только предустановленный набор.</p>

<h3>Фильтры</h3>

<p>Написание самих фильтров оказалось довольно несложным делом. Их у меня сейчас три.</p>

<p>&#8220;Honeypots&#8221; &#8212; основан на <a href="http://nedbatchelder.com/text/stopbots.html">статье Неда Батчелдера</a>, но сделан сильно проще. Фактически я вставляю в форму постинга поле с <code>name="email"</code> и скрываю его от взора людей CSS&#8217;ом. Если оно приходит заполненным &#8212; это был спам-бот. Я не стал зашифровывать названия полей, потому что это убивает браузерное автозаполнение, да и нужно будет, только когда боты будут учитывать особенности Cicero. Тогда я буду просто смотреть, что они делают, и дописывать фильтры по факту.</p>

<p>&#8220;Whitelist&#8221; &#8212; помечает посты от известных пользователей. Все пользователи с OpenID, которые получают свой профиль в Cicero, изначально имеют нейтральный статус относительно спамерства. Этот статус можно изменить на &#8220;спамер&#8221; и &#8220;не спамер&#8221;. Именно им фильтр и руководствуется, выдавая посту соответствующий статус (таким образом он не только whitelist, но еще и blacklist, но название сложилось исторически). Изменение статуса делается либо вручную в админке, либо если модератор явно помечает пост пользователя как спам, либо наоборот помечает как не спам в карантинной очереди.</p>

<p>Наконец последним фильтром стоит &#8220;Akismet&#8221;. Передача статьи на анализ на сервер Akismet&#8217;а делается с помощью <a href="http://kemayo.wordpress.com/2005/12/02/akismet-py/">библиотеки Дэвида Линча</a>, выбранной наугад из двух питоньих библиотек на сайте. Ее, правда, пришлось немножко поправить от небольших багов. Хотя, там, в общем-то, библиотека особенно и не нужна, потому что весь протокол &#8212; это один вызов функции через XML-RPC. А черную работа &#8212; перекладывание параметров запроса из джанговского request&#8217;а в параметры функции &#8212; пришлось все равно делать вручную :-).</p>

<p>Ну и кроме того, я не поленился все же сделать отсылку на сайт Akismet&#8217;а уведомлений об исправлении его решений &#8212; непойманного спама и пойманного не спама.</p>

<h3>Логика view</h3>

<p>Вот где мне пришлось помучиться &#8212; это в логике уровня view, где POST из формы превращается в статью, которую надо проверить на спам. Главная сложность в том, что у меня постинг статьи может происходить за два запроса. Это происходит, если человек, не будучи залогиненным во время отправки формы, указывает в качестве подписи свой OpenID. Тогда его статья сохраняется в базе от имени гостевого пользователя, но после этого человек переправляется на авторизацию на свой OpenID-сервер, в результате чего может вернуться с подтверждением успешной авторизации обратно. В этот момент сохраненная статья приписывается уже ему лично.</p>

<p>Проверка на спам полностью происходит в обоих этих случаях. Интересный эффект получается, если OpenID человека объявлен спамерским. Тогда, если его статья не поймана по другим признакам, она уже сохраняется в форуме. И только если он подтвердит, кто он такой, уже будет удалена как явный спам :-)</p>

<h3>Сеть белых списков</h3>

<p>Эта часть статьи на самом деле важнее всего остального. Мне хочется дать толчок идее, которую я впервые вычитал у <a href="http://simonwillison.net/">Саймона Виллисона</a> &#8212; <a href="http://simonwillison.net/2007/Jan/22/whitelisting/">Social Whitelisting</a>.</p>

<p>Поскольку OpenID не дает никаких гарантий по степени доверия пользователю, правильным поведением считается пользователю не доверять до тех пор, пока он как-то это самое доверие не заработает. Тогда он вносится в некий белый список, и сайт предоставляет ему какие-то дополнительные привилегии. Также можно утверждать, что чаще всего &#8220;вменяемость&#8221; пользователя не зависит от того, какими сайтами он пользуется. И вот отсюда вытекает идея о том, что сайты могут делиться друг с другом информацией о том, какие их пользователи являются вменяемыми. Самое главное для этого &#8212; универсальность имени пользователя &#8212; предоставляется как раз OpenID.</p>

<p>Важная особенность такой системы &#8212; ее децентрализованность. Если бы был некий единый реестр &#8220;чистых&#8221; OpenID, то он не относился бы к пользователям индивидуально, и спамеры могли бы явно нацелиться на то, чтобы понять, как он работает, выяснить необходимый минимум усилий для попадания туда OpenID-идентификатора, потом насоздавать их там большое количество и просто пользоваться этим пулом для спама везде. Децентрализованная система позволяет хозяину каждого конкретного форума самостоятельно выбирать несколько источников, чьему мнению о чистоте пользователей он доверяет. И каждый из этих источников &#8212; хозяин, следящий за своим хозяйством &#8212; будет справляться с этим лучше, и быстрее исправлять ошибки, чем любая глобальная система.</p>

<h3>Публикация белых списков</h3>

<p>Так вот главная цель этого поста &#8212; призвать рунетовских хозяев форумов и блогов, где есть вход по OpenID, организовать у себя такой белый список. <em>Особенно</em> хорошо было бы, если бы такую фичу поддержали в <a href="http://byteflow.su/">определенных популярных блог-движках</a>. А как именно это можно сделать, поделюсь на примере реализации в Cicero.</p>

<p>Есть отдельная view&#8217;ха, которая просто напросто выдает список всех OpenID, которые явно помечены как не спамеры. Причем ответ она умеет выдавать в трех форматах:</p>

<ul>
<li>простой текст с отдельным OpenID URL&#8217;ом на каждой строчке</li>
<li>XML вида <code>&lt;whitelist&gt;&lt;openid&gt;http://.../&lt;/openid&gt; .. &lt;/whitelist&gt;</code></li>
<li>список строк в формате JSON</li>
</ul>

<p>Делать это именно так &#8212; никакое не требование, потому что идея еще не распространена, и формат не устоялся. Однако минимально, видимо, надо уметь по крайней мере простой текст. Как, например, <a href="http://simonwillison.net/comments/whitelist/">сделано</a> у того же Саймона.</p>

<p>Мой код определяет формат по тому, что клиент запрашивает в заголовке Accept, используя для его парсинга библиотечку <a href="http://code.google.com/p/mimeparse/">mimeparse</a>:</p>

<pre><code>def openid_whitelist(request):

  # получение списка URL'ов
  openids = (p.openid for p in Profile.objects.filter(spamer=False) if p.openid)

  # определение предпочтительного формата из списка поддерживаемых
  from cicero.utils.mimeparse import best_match
  MIMETYPES = ['application/xml', 'text/xml', 'application/json', 'text/plain']
  accept = request.META.get('HTTP_ACCEPT')
  if accept:
    mimetype = best_match(MIMETYPES, accept)
  else:
    mimetype = 'text/plain'  # text/plain -- по умолчанию

  # выдача ответов разных типов
  if mimetype.endswith('/xml'):
    try:
      import xml.etree.ElementTree as ET
    except ImportError:
      import elementtree.ElementTree as ET
    root = ET.Element('whitelist')
    for openid in openids:
      ET.SubElement(root, 'openid').text = openid
    xml = ET.ElementTree(root)
    response = HttpResponse(mimetype=mimetype)
    xml.write(response, encoding='utf-8')
    return response
  if mimetype == 'application/json':
    from django.utils import simplejson
    response = HttpResponse(mimetype=mimetype)
    simplejson.dump(list(openids), response)
    return response
  if mimetype == 'text/plain':
    return HttpResponse((o + '\n' for o in openids), mimetype=mimetype)

  # если запрошен неизвестный формат - выдать ошибку
  response = HttpResponse('Can accept only: %s' % ', '.join(MIMETYPES))
  response.status_code = 406
  return response
</code></pre>

<p>На мой, пока небогатый, whitelist можно <a href="http://softwaremaniacs.org/cicero/users/openid_whitelist/">посмотреть</a>. В большинстве браузеров он, скорее всего, выдастся в XML-ном виде.</p>

<p>Вторая важная вещь &#8212; это собственно публикация. В вашем форуме или блоге обязательно должна быть где-нибудь в футере или сайдбаре ссылка типа &#8220;OpenID whitelist&#8221;. Наверное было бы еще хорошо, чтобы кто-нибудь придумал для нее уникальную иконку, но пока можно пользоваться <a href="http://shareicons.com/">универсальной иконкой &#8220;Share&#8221;</a>.</p>

<h3>Чтение белых списков</h3>

<p>Cicero для чтения белых списков имеет две таблички: WhitelistSource, где хранятся URL&#8217;а источников, и CleanOpenID, где хранятся сами OpenID. С последней как раз и сверяется плагин &#8220;whitelist&#8221;, если OpenID человека не имеет собственного статуса в самом Cicero.</p>

<pre><code>class WhitelistSource(models.Model):
  url = models.URLField()

  class Admin:
    pass

  def __unicode__(self):
    return self.url

class CleanOpenID(models.Model):
  openid = models.CharField(max_length=200, db_index=True)
  source = models.ForeignKey(WhitelistSource)

  class Meta:
    unique_together = [('openid', 'source')]
    ordering = ['openid']
    verbose_name = 'Clean OpenID'
    verbose_name_plural = 'Clean OpenIDs'

  class Admin:
    list_display = ['openid', 'source']
    list_filter = ['source']

  def __unicode__(self):
    return self.openid
</code></pre>

<p>Обновляются списки &#8220;тупо по крону&#8221;™. Однако для вящего удобства тех, кто захочет Cicero использовать, я оформил скрипт обновления в виде <a href="http://webnewage.org/post/2008/2/5/komandovat-paradom-budet-django/">джанговской команды</a>, вызов которой в кроне выглядит примерно так:</p>

<pre><code>1    6    *    *    *    /home/maniac/sm_org/manage.py update_whitelist
</code></pre>

<p>Еще наверное осталось упомянуть, что считанные OpenID из чужих белых списков внедрять автоматически в свой белый список нельзя. Потому что иначе ошибки с допусканием спамеров в них, которые будут так или иначе иногда возникать, труднее будет исправлять, если спамера надо будет удалять из многих списков, куда он успел просочиться. Поэтому каждый хозяин должен контролировать только свой собственный список.</p>

<hr />

<p>Что же касается Cicero, то до его публикации осталось совсем немного: нормально сверстать и импортировать посты из текущего форума. Остальные наполеоновские планы уже будут реализовываться потом.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/04/28/antispam-and-social-whitelisting/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Рефакторинг Queryset&#8217;ов</title>
		<link>http://softwaremaniacs.org/blog/2008/04/27/queryset-refactoring/</link>
		<comments>http://softwaremaniacs.org/blog/2008/04/27/queryset-refactoring/#comments</comments>
		<pubDate>Sun, 27 Apr 2008 10:37:28 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=324</guid>
		<description><![CDATA[Сегодня Малколм Трединник влил бранч queryset-refactoring в Джановский транк. Это изменение по крутости в принципе сравнимо с давнишней демагизацией. Однако в отличие от нее, на этот раз все гораздо более обратно совместимо.



Вообще, Малколм &#8212; велик. Фактически он переделал с нуля всю душу конструирования запросов в джанговском ORM, превратив ее из мельницы строковых кусочков в набор [...]]]></description>
			<content:encoded><![CDATA[<p>Сегодня <a href="http://www.pointy-stick.com/blog/">Малколм Трединник</a> <a href="http://groups.google.com/group/django-users/browse_thread/thread/f4cd02d8d9389669">влил</a> бранч <a href="http://code.djangoproject.com/wiki/QuerysetRefactorBranch">queryset-refactoring</a> в Джановский транк. Это изменение по крутости в принципе сравнимо с давнишней <a href="/blog/2006/05/02/magic-removal/">демагизацией</a>. Однако в отличие от нее, на этот раз все гораздо более обратно совместимо.</p>

<p><span id="more-324"></span></p>

<p>Вообще, Малколм &#8212; велик. Фактически он переделал с нуля всю душу конструирования запросов в джанговском ORM, превратив ее из мельницы строковых кусочков в набор классов с четким разделением функциональности и возможностью гибкого наследования для ее расширения. И при этом умудрился сделать все так, чтобы обычный юзерский код не сломался. Мелкие <a href="http://code.djangoproject.com/wiki/QuerysetRefactorBranch#Backwardsincompatiblechanges">исключения из обратной совметимости</a> есть, но они действительно ерундовые.</p>

<p>На основе это кода уже сделано несколько вкусных улучшений. Первый пример &#8212; <a href="http://www.djangoproject.com/documentation/model-api/#model-inheritance">наследование моделей</a>, про которое люди твердили прямо с первой публикации Джанго. Я, признаться, не нахожу его особо полезным, но отметить стоит.</p>

<p>Лично же мне почему-то особенно понравились две вещи:</p>

<ul>
<li><p>Если у вас есть модель Department и у ней есть зависимая модель Employee, то можно сделать две разные вещи:</p>

<ul>
<li><code>Department.objects.filter(employee__gender='male', employee__age__gt=40)</code> &#8212; выбирает отделы, где есть мужчины старше 40 лет. То есть оба условия пола и возраста накладывается на каждого сотрудника.</li>
<li><code>Department.objects.filter(employee__gender='male').filter(employee__age__gt=40)</code> &#8212; выбирает отделы, где есть мужчины &#8212; раз, и из них те отделы, где есть люди старше 40 лет &#8212; два. Тут условия накладываются отдельно одно за другим на всех сотрудников отдела.</li>
</ul>

<p>Другими словами, Джанго научилась делать join с несколькими копиями одной и той же таблицы в одном запросе.</p></li>
<li><p>Можно делать массовый update: <code>Employee.objects.filter(salary=20000).update(salary=50000)</code></p></li>
</ul>

<p>Об остальных изменениях читайте в <a href="http://code.djangoproject.com/wiki/QuerysetRefactorBranch">документе по бранчу</a> на wiki.</p>

<p>Но главное, конечно, что теперь этот код гораздо проще контролировать и развивать. Например, агрегацию добавить&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/04/27/queryset-refactoring/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Google App Engine</title>
		<link>http://softwaremaniacs.org/blog/2008/04/10/google-app-engine/</link>
		<comments>http://softwaremaniacs.org/blog/2008/04/10/google-app-engine/#comments</comments>
		<pubDate>Wed, 09 Apr 2008 22:57:49 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Python]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=320</guid>
		<description><![CDATA[Позвольте присоединиться ко всеобщему шуму про Google App Engine со слегка упорядоченным дампом своих мыслей последней пары дней.



Хорошо!


Питон, Питон, Питон. Само по себе то, что Google сделал массовый специализированный хостинг приложений именно с этим языком &#8212; это увесистый такой аргумент для бюрократов, которые не очень любят разбираться в деталях и брать на себя ответственность. Надеюсь [...]]]></description>
			<content:encoded><![CDATA[<p>Позвольте присоединиться ко всеобщему шуму про <a href="http://code.google.com/appengine/">Google App Engine</a> со слегка упорядоченным дампом своих мыслей последней пары дней.</p>

<p><span id="more-320"></span></p>

<h3>Хорошо!</h3>

<ul>
<li><p>Питон, Питон, Питон. Само по себе то, что Google сделал массовый специализированный хостинг приложений именно с этим языком &#8212; это увесистый такой аргумент для бюрократов, которые не очень любят разбираться в деталях и брать на себя ответственность. Надеюсь теперь начинающим питонистам будет гораздо проще объяснять боссам, почему они пишут на этом &#8220;неизвестном&#8221; языке.</p></li>
<li><p>Хостинг WSGI-приложений &#8212; это очень нужная вещь в принципе. Не обязательно в том виде, в котором это сделано на GAE. Главное, что они, кажется, угадали с тем, что многим питонистам на хостинге не нужна тупая файловая система, где придется вручную писать запускающие скрипты. А нужна просто некая дырка, куда можно скормить свой хендлер. А с путями и прочей инфраструктурой пусть разбирается хостинг сам. Теперь уже как-то даже и странно, что так не делают все&#8230;</p></li>
<li><p>Такой реверанс в сторону Джанго &#8212; с явной поддержкой, копированием архитектурных решений в БД-библиотеке, использованием шаблонного движка &#8212; очень приятен. Хотя не думаю, что это сильно скажется на самом фреймворке. Мне это видится скорее признанием очевидного (<a href="http://turbogears.org/">sorry</a>, <a href="http://pylonshq.com/">guys</a>!)</p></li>
<li><p>Хороший шанс поработать вживую с <a href="http://labs.google.com/papers/bigtable.html">BigTable</a>. Я, как давний непоклонник реляционных БД, очень рад, если многие разработчики перестанут автоматически думать про хранение данных в терминах реляционной алгебры и начнут смотреть в другие стороны.</p></li>
</ul>

<h3>Не особенно хорошо.</h3>

<ul>
<li><p>Питонья среда урезана практически до неюзабельности. Повспоминав короткое время, я понял, что практически ни один из моих проектов не смог бы работать на GAE. Где-то PIL был нужен &#8212; так оказывается нельзя библиотеки с сишным кодом использовать. В музыкальном сервисе кастомный HTTP-сервер на сокетах тоже реализовать было бы нельзя, потому что нельзя пользоваться собственно сокетами. Форум Cicero &#8212; в пролете из-за поиска Sphinx&#8217;ом, который хранит индексы в виде файлов&#8230; В общем, пока эта среда выглядит пригодной для простеньких чисто HTML&#8217;ных и javascript&#8217;овых mash-up&#8217;ов.</p>

<p>Впрочем, мне думается, что вот здесь-то большинство зияющих проблем починят. Просто будут со временем добавлять собственные &#8220;стерилизованные&#8221; для работы в ограниченной среде аналоги стандартных модулей. Сейчас о степени убогости судить, конечно, рано. Потому как это вообще-то бета :-).</p></li>
<li><p>Невозможно отлаживаться на сервере. Чем нетривиальней приложение, тем больше вероятность, что разница между средой на машине разработчика и на сервере начнет как-то сказываться. И тогда захочется влезть на сервер shell&#8217;ом, чтобы контроллировать процесс. Пока GAE предлагает только пакетную обработку: закачал, запустил, почитал лог.</p></li>
<li><p>&#8220;Вы можете использовать Джанго&#8221; &#8212; это, мягко говоря, преувличение. Даже если отвлечься от того, что вы не можете использовать половину самого Питона, то и с Джанго тоже полно проблем:</p>

<ul>
<li><p>Отсутствие ORM&#8217;а замечают все. И тут же очевидную невозможность использовать админку, авторизацию и много другого contrib&#8217;а. А заодно и большу́ю часть сторонних приложений, которые под Джанго пишутся в расчете на то, что там будет родной ORM.</p>

<p>Эта ситуация, кстати, хорошо иллюстрирует, почему джанговская интегрированность, на которую любят безоглядно ругаться, на самом деле хорошая штука. Не будь ее &#8212; у нас бы был вот такой висящий в воздухе фреймворк, как GAE&#8217;шный Джанго, под который ничего нельзя надежно написать.</p></li>
<li><p>Из-за отсутствия ORM разработчикам GAE пришлось написать свой портированный вариант newforms. В итоге получается форк, который не будет развиваться за Джанго, и соответственно, еще больше удалять &#8220;Django-GAE&#8221; от &#8220;just Django&#8221;.</p></li>
</ul></li>
</ul>

<h3>Пугающе&#8230;</h3>

<ul>
<li><p>Здравствуй, здравствуй, старый &#8220;добрый&#8221; vendor lock-in. Пока что по всему выходит, что Гугл хочет, чтобы разработчики разрабатывали приложения не для себя, а для него:</p>

<ul>
<li>проприетарная среда не допускает легко переноса приложений на другие платформы, равно как и портирование существующих приложений к себе</li>
<li>вы &#8220;легко и непринужденно&#8221; можете использовать базу пользователей Гугла, а не свою</li>
<li>легкий порог входа привлекает начинающих программистов, что создает у них неосознанный громадный кредит доверия к &#8220;доброй маме&#8221; Гуглу, что бы тот в последствие ни делал</li>
</ul>

<p>Меня это все пугало и раньше, когда вендор назывался на &#8220;M&#8221;, а платформа на &#8220;W&#8221;, пугает и сейчас. Закрытая платформа &#8212; плохо.</p>

<p>Еще точно и кратко про это у Тима Брея: <a href="http://www.tbray.org/ongoing/When/200x/2008/04/09/Google-Users-API">Sharecropper Alert</a></p></li>
</ul>

<h3>&#8220;А я не поехал&#8230;&#8221;</h3>

<p>Ну а мне приятно осознавать, что мне туда, в общем-то, и не зачем. Заведя когда-то собственный домен, я не понимал, зачем мне почта GMail, если у меня есть свой красивый адрес, IMAP и больше места, чем я смогу занять. Так же меня не впечатлил и Google Code, потому что subversion-сервер у меня тоже свой. Так же я пока буду свысока смотреть на GAE, потому что проблем с разворачиванием питоньего кода на softwaremaniacs.org у меня никаких нет. Ни в сложности, ни в деньгах.</p>

<p>Но понаблюдать за тем, во что вся эта идея превратится, конечно, интересно.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/04/10/google-app-engine/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Django со стороны клиентской части веба</title>
		<link>http://softwaremaniacs.org/blog/2008/04/04/django-from-client-side/</link>
		<comments>http://softwaremaniacs.org/blog/2008/04/04/django-from-client-side/#comments</comments>
		<pubDate>Fri, 04 Apr 2008 11:44:44 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Web]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/?p=319</guid>
		<description><![CDATA[Никак не мог пройти мимо такой замечательной статьи Гарета Рашгроува: &#8220;Why the webstandards world appears to be choosing Django&#8220;. Давно не помню, что столько раз по ходу текста внутренне согласно кивал: &#8220;Ага, так и есть&#8221;. И про простые шаблоны, и про отсутствие генерации javascript&#8217;а, и про уважение HTTP&#8230; Замечательно написано. Маленькая цитата для затравки:


  [...]]]></description>
			<content:encoded><![CDATA[<p>Никак не мог пройти мимо такой замечательной статьи Гарета Рашгроува: &#8220;<a href="http://morethanseven.net/posts/why-the-webstandards-world-appears-to-be-choosing-django/">Why the webstandards world appears to be choosing Django</a>&#8220;. Давно не помню, что столько раз по ходу текста внутренне согласно кивал: &#8220;Ага, так и есть&#8221;. И про простые шаблоны, и про отсутствие генерации javascript&#8217;а, и про уважение HTTP&#8230; Замечательно написано. Маленькая цитата для затравки:</p>

<blockquote>
  <p>I know I’d feel much more comfortable throwing someone with good markup skills at a project using Django than Rails. For the most part with Django you use the html you’re used to, Rails often wants you to change this to helpers&#8230;</p>
</blockquote>

<p>P.S. И блог, кстати, красивый.</p>

<p>P.P.S. Нет, я не ненавижу Rails. Его выбрал для сравнения не я, а автор той статьи. Про Rails спорьте с ним, пожалуйста :-)</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/04/04/django-from-client-side/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Тикет 2070: лед тронулся!</title>
		<link>http://softwaremaniacs.org/blog/2008/03/21/2070-moves/</link>
		<comments>http://softwaremaniacs.org/blog/2008/03/21/2070-moves/#comments</comments>
		<pubDate>Fri, 21 Mar 2008 00:48:44 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/2008/03/21/2070-moves/</guid>
		<description><![CDATA[&#8220;2070&#8243; &#8212; это магические цифры, которые для джангиста означают номер давнего, многострадального и считавшегося некоторыми обреченным тикета, который отвечает за то, чтобы Джанго загружаемые в нее через веб файлы не грузила целиком в память.

И вот буквально вчера лед тронулся!



Помимо того, что меня это радует в принципе, у меня есть еще и личный мотив. Для злорадных [...]]]></description>
			<content:encoded><![CDATA[<p>&#8220;2070&#8243; &#8212; это магические цифры, которые для джангиста означают номер давнего, многострадального и считавшегося некоторыми обреченным <a href="http://code.djangoproject.com/ticket/2070">тикета</a>, который отвечает за то, чтобы Джанго загружаемые в нее через веб файлы не грузила целиком в память.</p>

<p>И вот буквально вчера лед тронулся!</p>

<p><span id="more-315"></span></p>

<p>Помимо того, что меня это радует в принципе, у меня есть еще и <em>личный мотив</em>. Для злорадных потрясаний пальцем с воплями &#8220;а я же говорил&#8221;! Суть в том, что этот тикет был открыт вместо более раннего тикета с моим патчем, который хоть и был недоработан, но фиксил ядро проблемы. Автор нового тикета сказал, что этого недостаточно и затолкал в один патч аж три совершенно разноуровневые вещи:</p>

<ul>
<li>складывание загружаемого файла на диск</li>
<li>использование этой логики в админке</li>
<li>собственную реализацию upload-счетчика для ajax&#8217;ных прогресс-баров</li>
</ul>

<p>И вот тогда я долго пытался втолковать, что такую патч-бомбу никто не сможет нормально отсмотреть, и следовательно шансов на включение в транк у нее почти нет. Плюс из-за непродуманной архитектуры по ходу 2 лет существования тикета его преследуют постоянные проблемы с туманными багами в разных конфигурациях серверов.</p>

<p>Так вот: &#8220;я же говорил!!!&#8221; Примерно вчера за тикет взялся один человек &#8212; Майк Аксияк &#8212; который начал с того, что <a href="http://groups.google.com/group/django-developers/browse_thread/thread/cf58723582609b9e/c1c402bdb27b80ff#c1c402bdb27b80ff">обсудил в django-developers</a> дизайн, ничем не связанный с предыдущим, а сегодня уже сделал работающие патчи. Майк мой герой!</p>

<p>Кстати, сам дизайн &#8212; сказочный. Джанго не просто будет сваливать большие файлы на диск. Нет, это она будет делать по умолчанию. Но вместе с этим у юзера появится отдельный хук: <code>request.set_upload_handler(&lt;handler&gt;)</code>, в который можно будет передать свой собственный хэндлер и делать там много интересных вещей. В частности в обсуждении родились идеи:</p>

<ul>
<li>подсчет размера для организации того же пресловутого прогресс-бара</li>
<li>раззиповывание архивов на лету, без сохранения их на диск</li>
<li>пересылка файлов, опять же на лету, на другую машину</li>
<li>гибкие квоты для пользователей</li>
</ul>

<p>А теперь &#8212; <a href="http://code.djangoproject.com/ticket/2070">все идем тестировать патч</a>!</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/03/21/2070-moves/feed/</wfw:commentRss>
		</item>
		<item>
		<title>mysql_cluster: репликации для Django</title>
		<link>http://softwaremaniacs.org/blog/2008/03/18/mysql_cluster/</link>
		<comments>http://softwaremaniacs.org/blog/2008/03/18/mysql_cluster/#comments</comments>
		<pubDate>Tue, 18 Mar 2008 13:25:08 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/2008/03/18/mysql_cluster/</guid>
		<description><![CDATA[Mysql_cluster:


  Бэкенд mysql_cluster нужен для использования Django в схеме с master-slave
  репликацией MySQL. Он умеет переключать глобальное соединение Джанго с БД 
  между мастером и slave-репликами, и тем самым позволяет использовать 
  стандартный ORM.


Этот бэкенд мы написали для &#8220;Куда все идут&#8220;, и решили выложить в открытый доступ. Про особенности использования читайте [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://softwaremaniacs.org/soft/mysql_cluster/">Mysql_cluster</a>:</p>

<blockquote>
  <p>Бэкенд mysql_cluster нужен для использования Django в схеме с master-slave
  репликацией MySQL. Он умеет переключать глобальное соединение Джанго с БД 
  между мастером и slave-репликами, и тем самым позволяет использовать 
  стандартный ORM.</p>
</blockquote>

<p>Этот бэкенд мы написали для &#8220;<a href="http://kuda.yandex.ru/">Куда все идут</a>&#8220;, и решили выложить в открытый доступ. Про особенности использования читайте на <a href="http://softwaremaniacs.org/soft/mysql_cluster/">страничке проекта</a>, а в этом посте я изложу подробности реализации и архитектуры.</p>

<p><span id="more-314"></span></p>

<h3>Stateful архитектура</h3>

<p>Считается, что &#8220;Джанго не работает с несколькими БД&#8221;. Но более точно будет сказать, что по умолчанию ORM Джанго использует единый враппер соединения с БД (бэкенд). Это означает, что:</p>

<ul>
<li>в обход ORM можно обращаться куда и как угодно (кстати, именно так и поступает джанговское кеширование при хранении кеша в БД)</li>
<li>бэкенд можно заменить своим и организовывать там пулинг, репликации и все что угодно, и это будет работать для соединений из ORM</li>
</ul>

<p class="note"><small>Еще более точно надо заметить, что &#8220;можно заменить своим&#8221; бэкенд стало можно недавно в транке. Есть ли это в текущей стабильной версии 0.96, я, честно говоря, просто не помню&#8230;</small></p>

<p>Однако проектирование бэкенда натыкается на некоторые трудности. Дело в том, что протокол общения Джанго с бэкендом довольно беден. В частности, ORM не сообщает бэкенду многих вещей, которые могли бы оказаться полезными:</p>

<ul>
<li>нужно ли соединение только для выборки или апдейта</li>
<li>от имени какой модели будет делаться операция</li>
<li>происходит ли она в контексте какого-то веб-запроса</li>
</ul>

<p>Фактически вся суть протокола, опуская всякие служебные &#8220;close&#8221;, &#8220;commit&#8221;, &#8220;rollback&#8221;, состоит в операции &#8220;дай курсор!&#8221; Что потом будет происходит с этим курсором, в общем-то, непредсказуемо.</p>

<p>Mysql_cluster обходит эту таинственность ORM&#8217;а тем, что переключение между мастером и  репликами делается из внешнего кода и хранится в виде <em>состояния</em>. То есть работа строится примерно так:</p>

<pre><code>from django.db import connection # connection -- тот самый бэкенд

# по умолчанию работает мастер-соединение

connection.use_slave()
# все курсоры с этого момента создаются из слейв-соединения

connection.revert()
# вернулись к предыдущему состоянию
</code></pre>

<p>Главный минус этого &#8220;императивного&#8221; решения в том, что оно рушит барьеры абстракции. Вы не можете написать совершенно отвлеченную функцию, которая хочет писать в БД, и быть уверенными, что она работает всегда. Потому что если ее вызовет что-то в то время, когда активно слейв-соединение, она упадет.</p>

<p>За это меня все время ругает <a href="http://elephantum.ya.ru/">Андрей</a>, и даже пророчит, что не пройдет и двух месяцев, как кто-нибудь наткнется на какой-нибудь трудноуловимый баг, связанный с этой императивностью. Теоретически, он прав. Но это единственное решение, которое я смог придумать, которое работает извне Джанго, не трогая ее код. Кроме того, не все так страшно на самом деле :-).</p>

<h3>Почему оно все таки работает</h3>

<p>Поскольку Джанго &#8212; это именно веб-фреймворк, большая часть кода под его управлением работает не произвольно, а внутри веб-запросов, которые обладают полезными для нашей задачи свойствами:</p>

<ul>
<li>выполняют одно законченное логическое действие, часто в транзакции</li>
<li>изолированы друг от друга</li>
</ul>

<p>Первое условие означает, что по ходу запроса не надо метаться между мастером и репликами, а достаточно выбрать что-то одно в начале, исходя из того, что будет делать запрос. Второе условие означает, что состояния соединений в параллельных запросах не будут мешать друг другу. Поэтому в большинстве случаев все решается или middleware или декоратором для view, которые переключают бэкенд в нужное состояние до конца запроса.</p>

<p>Но есть и меньшинство случаев.</p>

<h3>Второстепенные операции</h3>

<p>Бывают операций с БД, которые выполняются независимо от основной логики работы запроса. Например создание сессиий, запись какой-нибудь статистики, прозрачная 
регистрация пользователя где-нибудь внутри системы. Они могут случаться в 
произвольные моменты времени, в том числе и в тех запросах, которые работает через слейв-соединение.</p>

<p>Для таких случаев специально оставлена возможность просто вручную включить мастер-соединение, сделать все что нужно, и вернуть состояние бэкенда к предыдущему. И для вот этого &#8220;вернуться к предыдущему&#8221; бэкенд хранит не одно текущее состояние, а стек состояний. Иначе бы для возврата к предыдущему состоянию клиентскому коду пришлось бы запоминать его в отдельной переменной, что неудобно.</p>

<p>В &#8220;Куда все идут&#8221; такого кода &#8212; ровно два места. Одно &#8212; это создание профиля для каждого нового пользователя, и там все решилось одним перекрытием метода save() модели профиля. Второе &#8212; это работа с сессиями. Сессии, на самом деле, такой классический случай второстепенных операций, что мы даже включили в mysql_cluster замену стандартной джанговской модели сессий, которая работает точно так же, только явно переключается в мастер для всех операций.</p>

<h3>Не-REST архитектуры</h3>

<p>Вторая часть меньшинства случаев  заключается в том, что многие разработчики игнорируют REST-архитектуру HTTP, и воспринимают веб-запросы не как основную схему, управляющую состоянием приложения, а как некоторую деталь реализации. Этот подход часто встречается у GUI-программистов, когда они начинают делать веб, и подходят к этому с вопросами типа: &#8220;как на клик по <code>&lt;select&gt;</code> выполнить мою питоновую функцию&#8221;. В такой схеме одна логическая операция может обслуживаться несколькими запросами, и каждый из них не будет знать, зачем он делает то, что делает. Другими словами, здесь уровень бизнес-логики поднимается с уровня веб-запроса на более высокий и обслуживается комбинацией кук и отслеживания текущего состояния в БД.</p>

<p>Mysql_cluster для таких случаев не работает. Но тут моя совесть чиста. Сама Джанго в явном виде не поощряет такой стиль приложений, и для этого лучше пользоваться другими фреймворками. Кажется <a href="http://www.seaside.st/">Seaside</a> и <a href="http://www.asp.net/">ASP.NET</a> его проповедуют, если я не путаю.</p>

<h3>Почему не MySQL Proxy</h3>

<p>Довольно много меня спрашивали разные люди, почему я не хочу воспользоваться решениями, которые распределяют запросы между мастером и репликой, смотря на тип запроса: select&#8217;ы в одну сторону, все остальное &#8212; в другую. Самый известный (мне) продукт, который это умеет &#8212; <a href="http://forge.mysql.com/wiki/MySQL_Proxy">MySQL Proxy</a>. Самое приятное в нем то, что это совсем внешнее решение, которое не требует изменения кода.</p>

<p>Однако нам это не подходит, потому что мы работаем с транзакциями. И, предвосхищая советы их выкинуть, скажу сразу, что без них невозможно надежно работать с денормализованными даными: сплошь и рядом будут случаться ситуации, когда авторитетные данные будут разъезжаться с предрассчитанными.</p>

<p>С транзакциями же возможны ситуации, когда некий update-запрос меняет данные в базе, а потом <em>в той же транзакции</em> следующие select-запросы эти данные используют. Проксирующее решение, которое про наши транзакции ничего не знает, направит select в другое соединение, и там этот select новых данных не увидит. Я, к сожалению, не могу привести какого-то реального примера, но возможность кажется мне очень реальной, и мне страшно :-).</p>

<p>Другая проблема с внешним прокси, что он не знает, что даже некоторые read-only транзакции <em>иногда</em> хочется выполнять в мастер-базе. Речь идет о ситуациях, когда POST-форма после сабмита возвращает HTTP-редирект на страницу с обновленными данными. А поскольку реплики обновляются не мгновенно, может возникнуть (и возникает) ситуация, когда эта страница считается из еще не обновленной реплики, и пользователь увидит, что его действие не сработало. Для борьбы с этим эффектом в обслуживающих middleware и декораторах mysql_cluster&#8217;а есть специальная логика, которая заворачивает в мастер следующий GET&#8217;а после POST&#8217;а с редиректом.</p>

<h3>Кластеризация реплик</h3>

<p>Последний вопрос, который я хотел осветить &#8212; это работа с несколькими репликами. Сам mysql_cluster про несколько реплик ничего не знает, для него все они &#8212; одно соединение. Потому кластеризацию реплик делается с помощью некого внешнего балансера. Там никакой зависимой от приложения логики не надо.</p>

<p>Что конкретно используется для этого в Яндексе, признаться, не знаю. А поскольку я сейчас в отпуске, то и пойти спросить трудновато :-). Может быть кто из яндексоидов в комментариях подскажет.</p>

<hr />

<p>Ну а в целом &#8212; <a href="http://softwaremaniacs.org/soft/mysql_cluster/">скачивайте</a>, пользуйтесь, делитесь впечатлениями.</p>

<p>P.S. Что-то дернуло посчитать размер mysql_cluster. Оказалось &#8212; всего 9 КБ, даже этот пост занимает больше &#8212; 14 КБ :-)</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/03/18/mysql_cluster/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Второй тизер</title>
		<link>http://softwaremaniacs.org/blog/2008/03/17/second-teaser/</link>
		<comments>http://softwaremaniacs.org/blog/2008/03/17/second-teaser/#comments</comments>
		<pubDate>Mon, 17 Mar 2008 13:48:20 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<category><![CDATA[Яндекс]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/2008/03/17/second-teaser/</guid>
		<description><![CDATA[У меня появилось время написать продолжение истории про то, как &#8220;Куда все идут&#8221; падал под трафиком с тизера (часть 1, часть 2). Сразу скажу, что рушится все обычно гораздо громче, чем не рушится, поэтому этот пост на захватывающую историю не претендует совсем.



Показатели

Итак, второй тизер для нас висел еще аж в позапрошлый четверг, 6 марта. Был [...]]]></description>
			<content:encoded><![CDATA[<p>У меня появилось время написать продолжение истории про то, как &#8220;Куда все идут&#8221; падал под трафиком с тизера (<a href="/blog/2008/02/21/offline-teaser/">часть 1</a>, <a href="/blog/2008/02/22/why-offline-crashed/">часть 2</a>). Сразу скажу, что рушится все обычно гораздо громче, чем <em>не</em> рушится, поэтому этот пост на захватывающую историю не претендует совсем.</p>

<p><span id="more-312"></span></p>

<h3>Показатели</h3>

<p>Итак, второй тизер для нас висел еще аж в позапрошлый четверг, 6 марта. Был он, правда, не постоянный, а появлялся с вероятностью примерно в 20%. Это, понятное дело, была перестраховка, и вылилась она в то, что нагрузка была очень маленькой: 27 запросов/сек в пике.</p>

<p>Тут, кстати, для меня загадака, откуда взялись наши 300 запросов/сек в прошлый раз. Если 27 умножить на 5 (что уже преувеличение), то получится только 135. Думаю, что может быть зависимость между вероятностью показа и трафиком нелинейная, а может быть мы в тот раз что-то лишнее не от-grepили из статистики :-).</p>

<p>Однако, даже на таком небольшом потоке можно сказать, что своего мы достигли: нагрузка на систему весь день валялась в полном нуле, LA не забирался выше десятых долей. И я думаю, что эту нагрузку раз в пять увеличивать можно легко. Можно и больше даже. Правда, практически проверить мы это сможем не очень скоро. Помимо чисто технических показателей тизер дал нам понять, что звать в большом количестве народ к нам пока не очень интересно. Поэтому мы сначала прикрутим несколько фичек повкуснее, и тогда уж &#8212; снова.</p>

<h3>Решения</h3>

<p>Все решения, которые я перечислял в прошлом посте были сделаны и сыграли свою роль.</p>

<p>Если я правильно читаю <del>хрустальный шар</del> <ins>статистику тестового стенда</ins>, то больше всего воздуху сервису дает мягкое протухание кеша. Правда, мы не стали использовать MintCache напрямую, а просто позаимствовали идею в своем коде, так удобней оказалось.</p>

<p>Обещанный мной бэкенд, который сдружил Django с репликацией MySQL я тоже написал, и больше того, мы собираемся его опубликовать в ближайшее время (вот только README на английский переведу :-) ). Но в нашем конкретном случае он, правда, не дал много, потому что у нас реально много тяжелых запросов на сервисе, поэтому кешировать результат существенно эффективней, чем просто размазывать тяжелый SQL по бэкендам.</p>

<p class="note"><small>Интересно, сколько на этот раз найдется людей, которые после этого предложения напишут в <a href="http://rsdn.ru/">RSDN</a> и на <a href="http://habrahabr.ru/">Хабре</a>, что-нибудь типа &#8220;Django плохо работает с репликацией&#8221; :-)</small></p>

<p>Но в любом случае реплики играют свою роль хотя бы просто для надежности. Даже если у нас отключится датацентр, в котором стоит мастер-база, то мы, по идее, должны теперь нормально работать в read-only режиме.</p>
]]></content:encoded>
			<wfw:commentRss>http://softwaremaniacs.org/blog/2008/03/17/second-teaser/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Декларативная инвалидация кеша</title>
		<link>http://softwaremaniacs.org/blog/2008/03/10/%d0%b4%d0%b5%d0%ba%d0%bb%d0%b0%d1%80%d0%b0%d1%82%d0%b8%d0%b2%d0%bd%d0%b0%d1%8f-%d0%b8%d0%bd%d0%b2%d0%b0%d0%bb%d0%b8%d0%b4%d0%b0%d1%86%d0%b8%d1%8f-%d0%ba%d0%b5%d1%88%d0%b0/</link>
		<comments>http://softwaremaniacs.org/blog/2008/03/10/%d0%b4%d0%b5%d0%ba%d0%bb%d0%b0%d1%80%d0%b0%d1%82%d0%b8%d0%b2%d0%bd%d0%b0%d1%8f-%d0%b8%d0%bd%d0%b2%d0%b0%d0%bb%d0%b8%d0%b4%d0%b0%d1%86%d0%b8%d1%8f-%d0%ba%d0%b5%d1%88%d0%b0/#comments</comments>
		<pubDate>Mon, 10 Mar 2008 19:15:42 +0000</pubDate>
		<dc:creator>Иван Сагалаев</dc:creator>
		
		<category><![CDATA[Django]]></category>

		<guid isPermaLink="false">http://softwaremaniacs.org/blog/2008/03/10/%d0%b4%d0%b5%d0%ba%d0%bb%d0%b0%d1%80%d0%b0%d1%82%d0%b8%d0%b2%d0%bd%d0%b0%d1%8f-%d0%b8%d0%bd%d0%b2%d0%b0%d0%bb%d0%b8%d0%b4%d0%b0%d1%86%d0%b8%d1%8f-%d0%ba%d0%b5%d1%88%d0%b0/</guid>
		<description><![CDATA[Александр Кошелев написал подробную статью про инвалидацию кешей. Самое интересное (для меня, по крайней мере) в ней то, что там сделана попытка придумать декларативный синтаксис описания процесса инвалидации. И хотя в общем случае такая задача не решается, было бы очень полезно найти подход, который бы работал в пресловутых 80% случаев.

Пишу &#8220;бы&#8221;, потому что для нас [...]]]></description>
			<c