В пятницу нарисовал самую базу форума и сделал начальный коммит.

Предварительные замечания

Свою версию Django я периодически обновляю из их репозитория, и сейчас она ушла уже достаточно далеко от последней официальной 0.95. Однако на днях джанговцы собираются оформить текущие сборки в не менее официальную 0.96, и если вы предпочитаете фиксированные версии, то для форума нужна будет именно она.

Все файлы пишутся в кодировке utf-8. Очень, очень настоятельно рекомендую всем, кто еще боится этой кодировки, и кому windows-1251 кажется привычней, отказаться от этой пагубной привычки (только увольте от подробностей! :-) ). Сейчас это, в общем-то, совсем не проблема: наверное все редакторы уже научились utf-8 читать. То же относится к кодировке в базе и на веб-страницах.

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

Проекты и приложения

В Django разделяются понятия проекта, которое означает, грубо, "весь сайт", и приложения — логически самостоятельной части сайта. Сам проект у меня уже есть, он олицетворяет собственно сайт SoftwareManiacs.Org, и сейчас в нем есть одно простенькое приложение — каталог моих программ. Форум будет еще одним приложением, подключенным к этому проекту. Но поскольку я хочу это приложение сделать открытым для людей и отдельным от проекта, я не включил его внутрь директории проекта, а создал в совершенно отдельном месте.

Все это я описываю для того, что если кто-то захочет реально использовать этот форум как своеобразный тьюториал к Django, то вам надо будет выгрузить к себе исходник из репозитория форума (svn://softwaremaniacs.org/cicero/trunk) и создать для него маленький проект. Как создавать проект, написано в официальном тьюториале Django. Единственное отличие будет в том, что не надо создавать свое приложение 'polls', а надо подключить уже готовое приложение cicero, директорию которого проще всего выгрузить внутрь директории проекта.

Еще одна вещь, которую надо сделать — это включить настройки Cicero в настройки проекта. Можно переписать их руками из settings.py, который лежит в директории форума, в ваш основной settings.py, а можно просто в нем их импортировать:

from cicero.settings import *

Модель

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

Первая модель очень простая — это отдельные форумы. Все, что у форума есть — это название, группировка и slug (то, как он будет называться в URL):

class Forum(models.Model):
  slug = models.SlugField()
  name = models.CharField(maxlength=255)
  group = models.CharField(maxlength=255, blank=True)

  class Meta:
    ordering = ['group']

  def __str__(self):
    return self.name

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

Дальше идут топики и статьи:

class Topic(models.Model):
  forum = models.ForeignKey(Forum)
  subject = models.CharField(maxlength=255)
  created = models.DateTimeField(auto_now_add=True)

  class Meta:
    ordering = ['id']

  def __str__(self):
    return self.subject

class Article(models.Model):
  topic = models.ForeignKey(Topic)
  text = models.TextField()
  filter = models.CharField(maxlength=50)
  created = models.DateTimeField(auto_now_add=True)
  author = models.ForeignKey(User)

  class Meta:
    ordering = ['id']

  def __str__(self):
    return '(%s, %s, %s)' % (self.topic, self.author, self.created.replace(microsecond=0))

  def html(self):
    '''
    Возвращает HTML-текст статьи, полученный фильтрацией содержимого
    через указанный фильтр.
    '''
    # implement filtering
    return self.text

Вот здесь уже поинтересней. В своем предыдущем опыте написания форума я делал другие модели: Article, которая была статьей "верхнего уровня" и Reply, которые были ответами, привязанными к Article. В итоге, пользовательский текст лежал и в Article'ах, и в Reply'ях. Теперь же в Topic попадает только название (subject), а весь пользовательский текст находится только в моделях Article.

Я не готов до крови драться именно за такое решение, но оно мне сейчас кажется более удачным. У первой статьи и остальных очень много общих черт: наличие автора, текста, фильтра; одинаковое отображение в шаблоне, индексирование поиском и т.д. А отличий — только subject.

Еще, как видно, я сделал у статьи метод html, который будет выдавать готовый HTML с пользовательским текстом, прогнанным через фильтр (bbcode, markdown и пр.). Но реализовывать пока не стал, потому что чтобы статьям назначать фильтр, нужно читать его из пользовательского профайла, а он появится только после того, как приделаю авторизацию.

Теперь осталось только включить это все в базу, и здесь Django сделает все "CREATE TABLE ..." за нас:

./manage.py syncdb

URLы и view

Для логики вывода на веб я предполагаю максимально использовать generic views. Это, на самом деле, некий сдвиг относительно того, как я писал на Django два первых приложения, когда мне generic'и казались слишком ограниченными, и я забил на них совсем, и везде писал собственные view. В итоге получил большое количество дублирующегося кода, особенно связанного с обработкой форм. Поэтому теперь моя философия такова, что generic views — отличный базис, и даже собственные функции имеет смысл строить на них.

В итоге, вот такой у меня получился urlconf (опуская незначительные детали):

from django.views.generic.list_detail import object_list
from cicero import views
from cicero.models import Forum, Topic, Article

info = {
  'paginate_by': settings.PAGINATE_BY,
  'allow_empty': True,
}

urlpatterns = patterns('',
  (r'^$', object_list, {'queryset': Forum.objects.all()}),
  (r'^([a-z0-9-]+)/$', views.forum, info),
  (r'^([a-z0-9-]+)/(\d+)/$', views.topic, info),
)

Пара замечаний:

А URL'ы, в итоге, будут выглядеть так:

Теперь две функции-обертки в views:

def forum(request, slug, **kwargs):
  forum = get_object_or_404(Forum, slug=slug)
  kwargs['queryset'] = forum.topic_set.all()
  kwargs['extra_context'] = {'forum': forum}
  return object_list(request, **kwargs)

def topic(request, slug, id, **kwargs):
  topic = get_object_or_404(Topic, forum__slug=slug, pk=id)
  kwargs['queryset'] = topic.article_set.all()
  kwargs['extra_context'] = {'topic': topic, 'forum': topic.forum}
  return object_list(request, **kwargs)

Это, на самом деле, очень стандартный джанговский паттерн решения очень стандартного джанговского FAQ'а. Generic'овый object_list изначально был создан для отображения списка всех объектов какой-то модели. Потом его немножко переделали, и ему стало можно подсунуть выборку с фильтрацией (queryset). Но вот выбирать значения для фильтрации из URL'а он так и не научился.

Поэтому для того, чтобы вывести список топиков, принадлежащих форуму, и список статей, принадлежащих топику, понадобились две такие функции. Все, что они делают — это берут переданные в них из URL'а параметры (slug форума и id топика), фильтруют по ним соответствующие модели (Forum и Topic) и передают это в стандартный object_list.

Да, ну и еще передают в шаблоны сами родительские объекты, чтобы их топиков можно было сослаться на форум, а из статей — на топик и форум.

Так здесь через обертки незамеченными проходят параметры allow_empty и paginate_by, которые туда передает Django, читая их из urlconf.

Шаблоны

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

Итак, шаблоны лежат в директории templates/cicero внутри директории приложения и называются forum_list.html, topic_list.html, article_list.html. И это — отличный пример работы принципа DRY, который проповедуется в философии Django. Проявляется он в том, что мне нигде не потребовалось отдельно указывать Django, что объекты "Forum" нужно выводить в шаблон "forum_list", объекты "Topic" — в шаблон "topic_list", а объекты "Article" — в "article_list". Эту нехитрую логику обеспечивает стандартная функция "object_list", которую я использую для всех view.

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

<!DOCTYPE html>
<head>
  <title>Форумы Cicero</title>

<body>

<h1>Форумы Cicero</h1>

{% regroup object_list by group as groups %}

{% for group in groups %}

{% if group.grouper %}<h2>{{ group.grouper|escape }}</h2>{% endif %}
<ul>
{% for forum in group.list %}
  <li><a href="{{ forum.slug }}/">{{ forum|escape }}</a>
{% endfor %}
</ul>

{% endfor %}

Особого внимания тут заслуживает разве что часто забываемый тег {% regroup %}, который из плоского списка форумов ("object_list"), который передает в шаблон view-функция, легко и непринужденно делает группированный список форумов ("groups"), который потом двойным for'ом уже выводится. Очень часто почему-то такую группировку хотят делать вручную в view, что гораздо менее приятно.

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


Вот, пока все так быстро и мало, получилось урвать часок в пятницу... Жду ваших комментариев!

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

  1. absurd

    а у меня вот так..
    как правило список форумов статичен..

    class Post(models.Model):

    SECTIONS=( ('main', 'Talks'), ('bugs', 'Bug reports'), )
    section=models.CharField(default='main',maxlength=10,choices=SECTIONS,db_index=True) author=models.ForeignKey(editor.models.User,editable=False)

  2. Maximbo

    2 absurd: это менее удобно, так как в дальнейшем не позволит изменить список форумов редактору (не программисту).

    2 Иван Сагалаев: спасибо, жутко интересно :) Непонятно только как потом работать с шаблонами без блоков. Может "обрамить" всё содержимое после в {% block ciceroforum %}{% endblock %} ?

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

    Блоки, безусловно, появятся... Например, наверняка на всех страницах форума в правом верхнем углу будет отмечено состояние залогиненности посетителя, и это будет вынесено в базовый шаблон.

  4. Alexander Solovyov

    Многие давние джанговские пользователи могут не знать, что теперь вместо строковых названию view-функций, в urlconf можно импортировать их самих, и это удобней.

    Чем? :)

  5. Alex Lebedev
        <p>В Python, при наличии такой возможности, следует <em>всегда</em> использовать в качестве агрументов функции, а не строковые переменные с их названиями.  Основных причин две:</p>
    

    1. При попытке передать несуществующую функцию интерпретатор выдаст ошибку сразу, а не при открытии соответствующей страницы.

    2. Есть возможность "на лету" модифицировать функцию перед передачей в качестве параметра. В Python 2.5 есть для этих целей очень удобная функция functools.partial

  6. Иван Сагалаев
        <p>Есть более "приземленная" причина, из-за которой я этим пользуюсь. Этот способ позволяет легко и просто перечислять URL'ы, которые обращаются и в собственные view, и в generic, как раз как у меня в статье и есть. А при использовании строк возникают проблемы с префиксом:</p>
    

    • если не указывать строковый префикс в начале patterns, то получаются очень длинные названия у каждой view

    • если указать префикс, то тогда в одной patterns можно перечислить только view из одного источника, и тогда приходится делать несколько patterns:

      urlpatterns = patterns('myapp.views',
        ...
      ) + patterns('django.views.generic',
        ...
      )
      

      а это трудно читать, потому что url'ы сгруппированы не по логике, а по деталям реализации, совершенно произвольно

  7. igorekk

    Иван, спасибо за пост.

    PS. Люди, до svn'а на работы не достучаться - порты закрыты. Положите кто-нибудь gzip в инет :)

  8. hidded

    Свежестянутая 2-я ревизия:
    http://hidded.pp.ru/files/uploads/2007/03/cicero-rev2.tar.gz

  9. Max Ischenko

    Еще, как видно, я сделал у статьи метод html, который
    будет выдавать готовый HTML с пользовательским
    текстом, прогнанным через фильтр (bbcode, markdown и
    пр.). Но реализовывать пока не стал, потому что чтобы
    статьям назначать фильтр, нужно читать его из
    пользовательского профайла, а он появится только
    после того, как приделаю авторизацию.

    Ты ведь кажется говорил, что пользователь сможет в любой момент изменить свои настройки фильтра? Тогда нужно хранить значение фильтра для каждого конкретного поста, т.е. как атрибут Article. Где None == "дефолтный системный фильтр". Нет?

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

    Ты ведь кажется говорил, что пользователь сможет в любой момент изменить свои настройки фильтра? Тогда нужно хранить значение фильтра для каждого конкретного поста

    Так оно там и есть: filter = models.CharField(maxlength=50)

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

  11. Max Ischenko

    Упс, просмотрел. ;)

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