Our social messaging app is coming along quite nicely at this point. We have a custom user model, styling, a complete authorization flow, and our core messaging functionality. In this chapter we’ll add the ability for other users to comment on message posts. This is a good way to introduce one-to-many foreign key relationships: one message can have many comments on it.

Complete source code for this chapter can be found on Github.

Model

To start we can add another table to our existing database called Comment. This model will have a ForeignKey relationship that links it to Post where our messages are. We only need this one field to link the two tables. Traditionally such a field has lower-case of the model name so we’ll call it post.

There will also be two other fields in our model:

  • comment which will have 140 characters like old-school Twitter
  • author linked to the current user

It also has a get_absolute_url method that returns to the main posts/ page and a __str__ method.

Open up posts.model.py and underneath the existing code add the following.

# posts/models.py
...

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    comment = models.CharField(max_length=140)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

    def get_absolute_url(self):
        return reverse('post_list')

    def __str__(self):
        return self.comment

Since we’ve updated our model it’s time to make a new migration file and then apply it. Note that by adding posts at the end of each command–which is optional–we are specifying we want to use just the posts app here. This is a good habit to use. For example, what if we made changes to models in two different apps? If we did not specify an app, then both apps’ changes would be incorporated in the same migrations file which makes it harder, in the future, to debug errors. Keep each migration as small and contained as possible.

(msg) $ python manage.py makemigrations posts
(msg) $ python manage.py migrate posts

Admin

After making a new model it’s good to play around with it in the admin app before displaying it on our actual website. Add Comment to our admin.py file so it will be visible.

# posts/admin.py
from django.contrib import admin

from . import models

admin.site.register(models.Post)
admin.site.register(models.Comment)

Then start up the server with python manage.py runserver and navigate to our main page http://127.0.0.1:8000/admin/

Admin page with Comments

Under our app “Posts” you’ll see our two tables: Comments and Posts. Click on “Comments.”

Admin Comments

There’s nothing here yet. Click on the “Add comment” button. Then enter in a few comments for the same post. For example I’ve added two comments on the “Does this work?” message post and selected by testuser account to do it.

Admin Comment One

Admin Comment Two

Now on the Comment page I can see both comments.

Admin Comment Page

At this point I hope you’re thinking: wouldn’t it be nice to see all Comment models related to a single Post model? It turns out we can with a Django admin feature called inlines which displays foreign key relationships in a nice, visual way.

There are two main inline views used: TabularInline and StackedInline. The only difference between the two is the template for displaying information. In a TabularInline all model fields appear on one line while in a StackedInline each field has its own line. We’ll implement both so you can decide which one you prefer.

Update posts/admin.py as follows in your text editor.

# posts/admin.py
from django.contrib import admin

from . import models


class CommentInline(admin.StackedInline):
    model = models.Comment


class PostAdmin(admin.ModelAdmin):
    inlines = [
        CommentInline,
    ]


admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Comment)

Now go back to the main admin page at http://127.0.0.1:8000/admin/ and click on “Posts.”

Admin posts page

Now select the post that we added comments for, which in my case was “Does this work?”

Admin change page

Better right! We can see and modify our messages and comments in one place.

Personally though I prefer using TabularInline as it shows more information in less space. To switch to to it we only need to change our CommentInline from admin.StackedInline to admin.TabularInline.

# posts/admin.py
from django.contrib import admin

from . import models


class CommentInline(admin.TabularInline):
    model = models.Comment


class PostAdmin(admin.ModelAdmin):
    inlines = [
        CommentInline,
    ]


admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Comment)

Refresh the admin page and you’ll see the new change: all fields for each model are displayed on the same line.

TabularInline page

Much better. Now we need to update our template to display comments.

Template

Since Comment lives within our existing post app we only need to update the template to display our new content. We don’t have to mess around with urls and views.

What we want to do is display all comments related to a specific post message. This is called a “query” as we’re asking the database for a specific bit of information. In our case, working with a foreign key, we want to follow a relationship backward: for each Post look up related Comment models.

Django has a built-in syntax we can use known as FOO_set where FOO is the lowercased source model name. So for our Post model we can use post_set to access all instances of the model. If we want “all” the results we would do post_set.all().

Understanding queries takes some time so don’t be concerned if the previous two paragraphs were confusing. I’ll show you how to implement the code as desired. And once you’ve mastered these basic cases you can explore how to filter your querysets so they return exactly the information you want.

In our post_list.html file we can add our comments to the card-footer. Note that I’ve moved our edit and delete links up into card-body. To access each comment we’re calling post.comment_set.all which means first look at the post model, then comment_set which is the entire Comment model of which we want all included. It can take a little while to become accustomed to this syntax for referencing foreign key data in a template!

<!-- template/post_list.html -->
{% extends 'base.html' %}

{% block title %}Posts{% endblock %}

{% block content %}
  {% for post in object_list %}
    <div class="card">
      <div class="card-header">
        <span class="font-weight-bold">{{ post.author }}</span> &middot; <span class="text-muted">{{ post.date }}</span>
      </div>
      <div class="card-body">
        <p>{{ post.message }}</p>
        <a href="{% url 'post_edit' post.pk %}">Edit</a> | <a href="{% url 'post_delete' post.pk %}">Delete</a>
      </div>
      <div class="card-footer">
        {% for comment in post.comment_set.all %}
          <p>
            <span class="font-weight-bold">{{ comment.author }} &middot;</span>
            {{ comment }}
          </p>
        {% endfor %}
      </div>
    </div>
    <br />
  {% endfor %}
{% endblock content %}

If you refresh the posts page at http://127.0.0.1:8000/posts/ we can see our new comments displayed on the page.

Posts page with comments

Yoohoo! It works. We can see both comments listed underneath the initial message.

Conclusion

With more time we would focus on forms here so that a user could write a new message post directly on the posts/ page as well as add a comment here, too. But the main focus of this chapter is to demonstrate how foreign key relationships work in Django.

In our final chapter we’ll focus on authorization and learn how to restrict access to our site to only logged-in users. We’ll also deploy our application with Heroku.

Continue on to Chapter 14: Authorization.