Все ж я был прав, когда думал, что описание процесса написания форума будет занимать куда больше времени, чем собственно его написание :-). Отщепление постов в новый топик было готово практически месяц назад, а руки дошли до блога только сейчас! Но лучше поздно, чем никогда.

Bzr-репозиторий http://softwaremaniacs.org/code/cicero/
Работающий форум http://softwaremaniacs.org/forum/

Мотивация

Штука, в общем-то, простая. У модератора должна быть удобная возможность взять некий пост внутри одного топика, вынести его из этого топика, приписать к нему новое название темы и оформить отдельным новым топиком. И я считаю, что важность этой операции, выражаясь штампами, очень трудно переоценить.

Хотя это может показаться странным. Ведь в большинстве существующих форумов модераторы стараются свести дискуссию внутрь одного длинного топика — прямо обратное усилие. Но на самом деле все объяснимо. Как мне кажется, ноги у желания свести дискуссию в одно место растут из того, что большинство форумов являются фактически чатами, где довольно постоянная аудитория постоянно сидит на форуме и разговаривает друг с другом. Общение это практически непрерывно и в нем трудно найти отдельные части, которые были бы ценны сами по себе. И в таком форуме один топик обычно составляет некую заданную обширную тему, а не конкретный частный вопрос. Если какой-то человек создает новый топик с темой, похожей на существующую, он ставит перед остальным сообществом странную задачу выбора, в какой же из нескольких постоянно текущих дискуссий участвовать. Что приводит обычно к тому, что все дискуссии распыляются и умирают. Поэтому модераторы с эти явлением борются.

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

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

Q: Как в Джанго работать с form_for_model, если мне нужны не все поля?

A: Используйте параметр fields.

Q: Спасибо! А вот еще... Почему у меня ImageField не работает?

Вот в этом-то месте модератор и должен посмотреть на пост, нажать кнопочку "Отщепить" и приписать сабжект "Не работает ImageField". При этом на старом месте, где был пост, должна появиться ссылка "пост перенесен в новый топик".

Сейчас на практике это выглядит так: http://softwaremaniacs.org/cicero/test/143/ (третий пост там).

Реализация

Я перепробовал несколько вариантов модели хранения связи поста с отщепленным от него топиком и остановился в итоге на таком:

class Article(models.Model):
  # ...
  spawned_to = models.ForeignKey(Topic, null=True, related_name='spawned_from')

Если у статьи поле spawned_to не NULL, значит она была перенесена в новый топик. При выводе постов в шаблоне добавляется условие о том, что если статья перенесена, то она не рисуется, а вместо нее рисуется ссылка на топик:

{% if not article.spawned %}
...
{% else %}
<div class="spawned">
Отщеплен новый топик "<a href="{% url cicero.views.topic article.spawned_to.forum.slug,article.spawned_to.id %}">{{ article.spawned_to.subject|escape }}</a>".
</div>
{% endif %}

Кстати, хочу обратить внимание на саму проверку на "spawned". Джанговские шаблоны не поощряют программирования. В частности, там нельзя написать что-то в духе {% if article.spawned_id is not None %}, и специально для этой проверки в модели статьи написан коротенький метод spawned:

def spawned(self):
  '''
  Перенесена ли статья в новый топик.
  '''
  return self.spawned_to_id is not None

Именно он вызывается в шаблоне, и именно из-за этого шаблон лучше читается и понимается. Совершенно не лукавя, я считаю это одной из самых важных черт джанговских шаблонов.

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

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

  1. Майк

    Когда мне в такой же ситуации нужна переменная для шаблона, я прямо пишу во view:
    if item.spawned_to_id is not None:
    item.spawned = 1
    Это решение хуже, чем через метод?

  2. Юревич Юрий

    @Майк
    Во-первых, дополнительный атрибут .spawned, который нужно инициализировать, а то забудешь и где-нибудь в другом месте попытаешься использовать item.spawned и получишь AttributeError. Во-вторых, получается что одно состояние "расщепимости" топика несут два атрибута: .spawned_to_id и .spawned, что потенциально может привести к ошибке, когда .spawned не синхронизировано с .spawned_to_id.

    @Иван
    Я бы не использовал название .spawned для метода, а использовал .is_spawned; либо использовал декоратор @property, чтобы метод был как атрибут.

  3. Alexander Solovyov

    Естественно, хуже. Так надо заранее заботиться об этом spawned. Заранее перебирать все элементы.

  4. Роман Чепляка

    отсутствие возможности ссылаться на отдельные посты

    Настораживает.

  5. dark-demon

    топик 1:

    • Как в Джанго работать с form_for_model, если мне нужны не все поля?
    • Используйте параметр fieldz.
      [сообщение отщеплено]

    топик 2:

    • Спасибо! только не fieldz, а fields. А вот еще… Почему у меня ImageField не работает?
    • это ты к чему про fields вначале написал?
  6. Иван Сагалаев

    Мне решение с методом нравится больше просто потому, что это не отдельная часть бизнес-логики, которую надо иметь в какой-то одной конкретной view, а просто еще одно представление состояния объекта, которое удобно иметь для шаблона.

  7. Тинитилов Дмитрий

    Привет. Чем больше я тебя читаю, тем больше убеждаюсь, что твои начальные мотивы совпадают с моими.

    Начать с того, что наравне с постоянной аудиторией для меня очень ценным является то, чтобы любой человек “с улицы” мог прийти и что-нибудь спросить или, наоборот, ответить.

    Я эту идею реализую, через виртуальных экспертов, которые могут отвечать на ранее встретившиеся вопросы. У меня конечно все не так технически круто как у тебя, но можешь глянуть http://ji.od.ua

  8. Давид Мзареулян

    В данном случае метод is_spawned вполне оправдан, но вообще, тут надо держать баланс. Я в одном своём классе уже подошёл к границе нечитаемости, потому что по каждому чиху шаблона создавал новый чек-метод в модели. Теперь буду рефакторить, потому что результат мне не нравится.

    По теме постинга: меня немного смущает то, что создаётся постинг-копия (данные дублируются непонятно зачем, не очень понятно, что делать при удалении родительской/дочерней копий...), но как это сделать правильно, сказать не могу:(

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

    Теперь буду рефакторить, потому что результат мне не нравится.

    Вполне обычный процесс ведь :-). Когда код становится плохом, он рефакторится. Заранее ведь не узнаешь, будет он плохим в каком-то конкретном месте или нет...

    По теме постинга: меня немного смущает то, что создаётся постинг-копия

    А, это интересный вопрос :-). Я в итоге пришел именно к таком решению, потому что если старую статью не удалять, то тогда вывод в шаблон ссылок "отсюда статья убрана" становится проблемой. Надо, получается, обязательно делать лишние запросы. А когда статья остается и сама может служить такой заглушкой, то весь вывод остается очень простым.

    Вопросы с удалением актуальны мало, потому что на практике вряд ли встретятся. Удалять саму ссылку просто странно. А удаление самого отщепленного топика маловероятно, потому что его создает ответственный модератор. Но даже если понадобится — всегда можно в админке удалить и топик, и ссылку на него.

    То есть я согласен, что структура избыточна. Но она реляционная, особой гибкости от нее ждать не приходится, а "practicality beats purity" :-)

  10. Майк

    2 Юревич Юрий, Alexander Solovyov
    Хорошо, с простым примером понятно. Теперь предположим такую ситуацию: из базы по сложному алгоритму выбираются элементы (ORM Django не справляется). Элементы после выборки из базы надо проиницализировать и подготовить к выводу в шаблон. То есть от цикла по элеменам никуда не уйти. Скажите, в такой ситуации тоже надо стремиться максимум информации для шаблона поместить в модель в виде методов? Или нагляднее будет проиницализировать все нужные флаги в одном цикле?

  11. Maximbo

    Выходит, cicero уже не станет форумом в привычном понимании этого слова. Скорее, у вас получится некая модерируемая база знаний вида "конкретный вопрос/множество ответов", наполняемая пользователями.

    Иван, какие-нибудь другие отличительные особенности планируются?

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

    Да в общем-то все по спецификации изначальной. У меня еще плавает в голове идея, что можно сделать некий автоматически показываемый FAQ, набираемых из рельно часто задаваемых вопросов, но это пока совсем не оформлено.

  13. Maximbo

    Было бы очень здорово! Только вот как собирать вопросы и ответы автоматом?

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

    Самый простой автоматический критерий, который мне виделся — это просто количество ссылок на топик изнутри самого форума. Раз на топик часто ссылаются, значит он популярный.

    Интерфейсно я предполагал это как небольшой блок из десяти самых популярных топиков на индексе, а также рядом с окном редактирования. Последнее — для того, чтобы на частый вопрос можно было в качестве ответа просто перетащить в textarea ссылку на топик из FAQ'а.

    Но это, повторюсь, очень отдаленная перспектива. Пока что надо дописать то, что сейчас придумано. А то вот многие уже просят его водрузить вместо старого :-)

  15. Андрей Хаврюченко

    Оффтопный вопрос, но я не нашёл у тебя в коде ссылки на лицензию под которой ты раздаёшь форум. Не мог бы прояснить?

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

    Я просто не думал об этом. Код же ни разу не релизнутый, поэтому можно считать, что форума как такового нет :-). Но в итоге он будет под BSD.

  17. Андрей Хаврюченко

    Ссылка же на скачивание (даже на этой странице) есть? Значит "опубликованый" в юридическом понимании этого слова.

    Кстати, нелишне будет в INSTALL упомянуть о необходимости добавить django.middleware.transaction.TransactionMiddleware.

    А за BSD - спасибо, могу со спокойной совестью одолжить...

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

    Кстати, нелишне будет в INSTALL упомянуть о необходимости добавить django.middleware.transaction.TransactionMiddleware.

    По идее, это не необходимость (хотя я не проверял просто). Он будет работать и без транзакций. Хотя я изо всех сил рекомендую их использовать, конечно :-)

  19. Андрей Хаврюченко

    Если бы работало, я бы не заметил :)

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