Coding a forum – Django

Recently coded a very simple forum from scratch in Python, for a Django website. Utilised a lot of recent learnings, as well as having to get through some difficult concepts (with simple solutions). For any die-hard or even intermediate Django programmers, this is unlikely to be anywhere near the best solution, and I will be building on this, but for now this is what works.

The Models

I won’t do a full walk through on the models as it is simple enough any beginner will be able to follow, instead I’ve added some over-the-top in-line commenting.

# The model imports.
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.utils.text import slugify

# Pretty simple top-level forum board that holds the categories, which in turn hold the topics.
class ForumBoard(models.Model):
    board_name = models.CharField(
        max_length=settings.FORUM_TITLE_MAX_LENGTH
    )

    slug = models.SlugField(
        default=board_name,
        editable=False,
        max_length=settings.FORUM_TITLE_MAX_LENGTH
    )

    board_description = models.TextField(
        blank=False,
        null=False,
        help_text="Please insert a description.",
    )

    board_is_open = models.BooleanField(
        default=True,
    )

    # Creating the slug field below, so it can be used in the views.
    def get_absolute_url(self):
        kwargs = {
            'pk': self.id,
            'slug': self.slug
        }
        return reverse('slug-detail/board-pk', kwargs=kwargs)

    def save(self, *args, **kwargs):
        value = self.board_name
        self.slug = slugify(value, allow_unicode=True)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.board_name

# The forum Categories model, again quite simple
class ForumCategory(models.Model):
    category_name = models.CharField(
        max_length=settings.FORUM_TITLE_MAX_LENGTH,
    )

    slug = models.SlugField(
        default=category_name,
        max_length=settings.FORUM_TITLE_MAX_LENGTH,
        editable=False,
    )

    board_parent = models.ForeignKey(
        ForumBoard,
        on_delete=models.CASCADE,
        related_query_name="parent_boards",
        related_name="parent_board",
    )

    category_description = models.TextField(
        blank=False,
        null=False,
        help_text="Please insert a description.",
    )

    def get_absolute_url(self):
        kwargs = {
            'pk': self.id,
            'slug': self.slug
        }
        return reverse('slug-detail/category-pk', kwargs=kwargs)

    def save(self, *args, **kwargs):
        value = self.category_name
        self.slug = slugify(value, allow_unicode=True)
        super().save(*args, **kwargs)

    def __str__(self):
        return "%s" % self.category_name
        # return "%s, %s" % (self.board_parent, self.category_name)

    # The forum topic, using choices, slug fields, foreign keys. Initially the topic model had a foreign key field to the category model to tie the two together. When I was working on the replies I decided this was creating redundant data, so I dropped this field and replaced it with a new model further down to bridge the two.
class ForumTopic(models.Model):
    # Choices preceded with F for forum :).
    F_PUB = 'F_PUB'
    F_DRA = 'F_DRA'
    F_LOC = 'F_LOC'
    TOPIC_STATUS_CHOICES = [
        (F_PUB, 'Published'),
        (F_DRA, 'Draft'),
        (F_LOC, 'Locked'),
    ]

    topic_title = models.CharField(
        max_length=settings.FORUM_TITLE_MAX_LENGTH,
    )

    slug = models.SlugField(
        default=topic_title,
        editable=False,
    )

    topic_author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )

    topic_status = models.CharField(
        max_length=5,
        choices=TOPIC_STATUS_CHOICES,
        default=F_PUB,
    )


    topic_last_edit = models.DateTimeField(
        null=True,
        blank=True,
    )

    topic_text = models.TextField(
        blank=False,
        null=False,
        help_text="Please enter some text."
    )

    # If the topic is a response to another topic, it needs a reply topic id. There were a few ways I could have gone about this ie; isolate the replies in a new model (but the replies are _exactly_ the same as the topics with an exception of category ownership and a reply topic id) - I could also have a seperate model to hold the topic id and the topic type for example topic id = 123, topic type=reply
    reply_topic_id = models.ForeignKey(
        "cm_forums.ForumTopic",
        blank=True,
        null=True,
        on_delete=models.CASCADE,
        related_name='reply_topic',
        related_query_name='reply_topics',
    )

    # The slug code. This is the same in each of the models. (yup, DRY)
    def get_absolute_url(self):
        kwargs = {
            'pk': self.id,
            'slug': self.slug
        }
        return reverse('slug-detail/topic-pk', kwargs=kwargs)

    def save(self, *args, **kwargs):
        value = self.topic_title
        self.slug = slugify(value, allow_unicode=True)
        super().save(*args, **kwargs)

    def __str__(self):
        return "%s, %s" % (self.topic_title, self.id)

# This is the model I simply refer to as a bridge although there's more than likely a different correct term for it. This bridges the topics to the category model using a one to one and, a many to one relationship. You'll note I've properly named all the related name attributes in all the models (but not the related_query_name as they default from related_name). This helps a lot when pulling data into the views.
class ForumTopicCategory(models.Model):
    topic_id = models.OneToOneField(
        ForumTopic,
        on_delete=models.CASCADE,
        related_name='f_topic_id',
        blank=False,
        null=False,
    )

    category_id = models.ForeignKey(
        ForumCategory,
        on_delete=models.CASCADE,
        related_name='f_cat_id',
        blank=False,
        null=False,
    )

    def __str__(self):
        return "%s, %s" % (self.topic_id, self.category_id)

Author: JR

Leave a Reply