Subclass, two separate classes or single class with optional field

Hi,

I’m building a forum app in Django as a learning project.

I’m unsure of how to approach the creation of Post objects. I want users to be able to post threads (i.e. new topic), I want users to be able to post replies to those threads as well as replying to other replies.

I want to include features like reactions to posts, and replies.

I’d appreciate some feedback on the different approaches I’m considering:

My thinking is that, since the two types of posts are so similar, they should inherit common fields/methods from a parent class. I’m unsure how Django would represent that in the DB tbh. Intially this was an abstract class

class Post(models.Model):
    post_body = models.TextField(max_length=2000)
    publish_date = models.DateTimeField("date posted")
    author = models.ForeignKey(User, on_delete=models.CASCADE)

class ThreadPost(Post):
    post_title = models.Charfield(200)
    is_solved = models.BooleanField("is_issue_solved", default=False)

class ReplyPost(Post):
    is_solution = models.BooleanField("is_this_the_solution", default=False)
    thread_post = models.ForeignKey(ThreadPost, on_delete=models.CASCADE)

Later I started using this, based on a stackoverflow response. However, I’m finding it a bit clunky to work with, as I often have to filter by whether a post is a thread or a reply, and have to find the thread which a post replies to. In that case, just having seperate classes seems more efficient. I do need to get better at querying etc.

class Post(models.Model):
    post_title = models.CharField(max_length=200, blank=True, null=True)  
    post_body = models.TextField(max_length=2000)
    publish_date = models.DateTimeField("date posted", auto_now_add=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    parent_post = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
    is_solved = models.BooleanField("is_issue_solved", default=False)
    is_solution = models.BooleanField("is_this_the_solution", default=False)

    def __str__(self):
        return self.post_title or self.post_body[:30]

    @property
    def is_thread(self):
        return self.parent_post is None

    @property
    def is_reply(self):
        return self.parent_post is not None


class Reaction(models.Model):
    REACTION_CHOICES = [
        ('thumbs_up', 'Thumbs Up'),
        ('thumbs_down', 'Thumbs Down'),
        ('laugh', 'Laugh'),
        # ...
    ]
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    reaction_type = models.CharField(max_length=20, choices=REACTION_CHOICES)

    def __str__(self):
        return f"{self.user.username} - {self.reaction_type}"

class Report(models.Model):
    REPORT_CHOICES = [
        ('spam', 'Spam'),
        ('abusive', 'Abusive Content'),
        # ...
    ]
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    reason = models.CharField(max_length=20, choices=REPORT_CHOICES)
    report_date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user.username} reported {self.post.id} for {self.reason}"

Finally, I could just have seperate unrelated Thread and Reply classes. I guess the issue with that is the code repitition and things like how other classes would interact with them. E.g. a Reaction class would need to account for the two unrelated Post types.

class ThreadPost(modesl.Model):
    Title = models.Charfield(200)
    body = models.TextField(max_length=2000)
    publish_date = models.DateTimeField("date posted")
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    is_solved = models.BooleanField("is_issue_solved", default=False)

class ReplyPost(models.Model):
    body = models.TextField(max_length=2000)
    publish_date = models.DateTimeField("date posted")
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    threadpost = models.ForeignKey(ThreadPost, on_delete=models.CASCADE)
    is_solution = models.BooleanField("is_this_the_solution", default=False)