Traversing Model Relationships
Updated Requirements for the Blog App
- Each blog post will have multiple comments
- Comment author will provide comment, optional name, and optional email
- Comments will be displayed along with author name in order below the post
- Date/Time of comment will be displayed next to it
- Number of comments and date/time of latest comment will be shown next to each post in post list
Updated ERD
erDiagram
POST ||--o{ COMMENT : Has
POST {
string title
string slug
string body
datetime created_on
datetime updated_on
int status
}
COMMENT{
string comment
string author
string email
datetime created_on
}
Implementing The Changes
Let’s update the our /blog/models.py
first:
class Comment(models.Model):
comment = models.TextField()
author = models.CharField(max_length=100, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
created_on = models.DateTimeField(auto_now_add=True)
post = models.ForeignKey('Post', on_delete=models.CASCADE)
Since we made a change to our models and to apply the changes to the database, we must run in shell:
python manage.py makemigrations
then:
python manage.py migrate
Now let’s update the admin so we can add comments to our posts using an InlinePostAdmin in blog/admin.py
:
# Import Comment as well
from .models import Post, Comment
# add the following
class CommentInline(admin.TabularInline):
model = Comment
# update the following admin
class PostAdmin(admin.ModelAdmin):
# other configurations here ...
# add the following to PostAdmin
inlines = [CommentInline,]
Updating the Detailed Post View to Show Comments
Remember, comments are related in a 1-to-many relationship with Posts. This is illustrated in the ERD as well as the ForeignKey
field in the Comment model that links it to the Post model.
The ForeignKey is always placed on the many side of the relationship. This means the following:
- Any single Comment must be related to a single Post (becasue the foregnkey field is in Comment)
- Any single Post could has multiple comments associated to it
In our show_post
view we created in the previous section, we fetched a single Post object:
def show_post(request, id):
obj = Post.objects.get(slug=id)
context = {
'post': obj, # The post slot has a single post
}
return render(request, "post_detail.html", context)
Let’s updated it to fetch the comments related to that post that we fetched:
def show_post(request, id):
obj = Post.objects.get(slug=id)
comment_list = obj.comment_set.all() #1
context = {
'post': obj,
'comments': comment_list, #2
}
return render(request, "post_detail.html", context)
Code explanation:
- Line #1: Notice here that we did the query on the variable containing the post object
obj
. Instead of usingobjects
we usedcomment_set
model manager. This is a model manager that is automatically created on any model that has a ForeignKey pointing to it from another model and would take the name of the model pointing to it. So, becauseComment
model has the ForeignKey pointing to Post, this manager is automatically namedcomment_set
. This is called the reverse relationship manager. Soobj.comment_set
will allow us to perform query operations on all the comments associated with that specific post object. Everything you have learned so far on database queries applies here. You can useall
orfilter
. Here we are usingall
to fetch all the comments associated with the post and stored them in thecomment_list
variable. - Line #2: We are storing the contents of
comment_list
variable (which is a list of comments) into thecomments
slot in the context. Now our template will have a variable namedcomments
that has a list of comments that we can loop over and display.
Let’s update the post_detail.html
template to loop over and dipsplay our comments:
<h1>{{ post.title}}</h1>
<ul>
<li>Posted on: {{ post.created_on }} </li>
<li>Last updated: {{ post.updated_on }} </li>
</ul>
<p>
{{ post.body }}
</p>
<h2>comments:</h2>
<ul>
{% for c in comments %}
<li>{{ c.author }}: {{ c.comment }}</li>
{% endfor %}
</ul>
Pay attention specifically to this part:
<ul>
{% for c in comments %}
<li>{{ c.author }}: {{ c.comment }}</li>
{% endfor %}
</ul>
As we did in post lists, we looped over the comments template variable and displayed each comment as a list item. This is the end result:
Final Thoughts
- If you have a Post object called
obj
, you can easily get to the associated Post directly using the ForeignKey field. Soobj.post
would give you the post object. - The reverse relationship manager is always named
model_set
, just replace the model with the name of the model that has the ForeignKey. - You can change the name of the reverse relationship manager using the option
related_field
in the ForeignKey and tell Django if you want to name the manager a name other than themodel_set
name.
Review Questions and Challenges
- What is a reverse model manager?
- Try to filter the comments related to a post such that you would only show the comments in which the name is not empty