Comments#
Introduction#
Now that we have articles, we need comments. Gotta give our users a voice, right?
Model#
Comments are a whole new object for our app, so we need to create a model.
A comment will need a related article, an author, a body, and a date.
Let's create a Comment model in articles/models.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
We've been through this before, so we can be quick:
- Our
articlefield is a many-to-one relationship that relates theCommentmodel to theArticlemodel. By default, Django uses thepkof the related object, but we're working withslug_uuidfields in this tutorial, henceto_field="slug_uuid". - The
authoris a many-to-one relationship that relatesCommenttoProfile. - The string representation of our model (
__str__) is the truncated comment body. - The canonical URL for a
Commentinstance is the same as the URL for theArticleinstance attached to the comment: we don't need to navigate to specific comments in Conduit, so we might as well have a simplifiedget_absolute_url.
Time to makemigrations and migrate. You should get the following error:
SystemCheckError: System check identified some issues:
ERRORS: articles.Comment.article: (fields.E311) ‘Article.slug_uuid’ must be unique because it is referenced by a foreign key. HINT: Add unique=True to this field or add a UniqueConstraint (without condition) in the model Meta.constraints.
That's because we're using articles' slug_uuid fields as ForeignKeys for the comments (so that we can filter our comments by the attached articles' slug_uuid fields instead of their UUIDs). As the error message indicates, this error is easily corrected by adding unique=True as an argument to the slug_uuid field in the Article model in articles/models.py.
1 2 3 4 5 6 7 | |
You should be able to makemigrations and migrate after that.
1 2 3 4 5 6 7 8 9 10 | |
Now, we need to register our model in articles/admin.py, so that we can access that object in the Django admin:
1 2 3 4 5 | |
Viewing comments#
In order to view comments, we need to have some comments to view first: go to the Django admin and create a few comments for a couple articles by hand.
We want to be able to view the comments on each article's detail page. Consequently, we need to modify templates/article_detail.html (based on Svelte implementation's [[https://github.com/sveltejs/realworld/blob/master/src/routes/article/%5Bslug%5D/index.svelte][article/[slug]/index.svelte]]):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Now create templates/comment_container.html and add the following (based on Svelte implementation's [[https://github.com/sveltejs/realworld/blob/master/src/routes/article/\slug\/_CommentContainer.svelte][_CommentContainer.svelte]]):
1 2 3 4 5 | |
We want to view all the comments for the article we're viewing, from most to least recent (which we achieve with the dictsortreversed template filter).
We will implement the rendering logic for our comments in templates/comment.html (based on Svelte implementation's [[https://github.com/sveltejs/realworld/blob/master/src/routes/article/\slug\/_Comment.svelte][_Comment.svelte]]):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Nothing new here: this template displays the comment's body, author's username, and creation date.
Try navigating to one of the articles that you created comments for through the Django admin: you should see them displayed under the article (though they made do with some decoration, which we will get to later).
Creating comments#
We will now start allowing our users to leave comments on the website.
We could do this like in the Django Girls tutorial: the ArticleDetailView would include a button that would direct to CommentCreateView on a separate page, and saving the comment would bring the user back to the ArticleDetailView. However, the RealWorldApp allows users to create and save their comments directly below the article, on the same page, so that's what we're going to try.
Surprisingly, this is not straightforward to implement in Django, because it implies mixing DetailView and CreateView functionalities in a single page, which is made difficult by the fact that the DetailView doesn't have a POST method, while the CreateView requires it.
Create CommentCreateView#
Fortunately, our use case is covered in the Django documentation, which greatly simplifies the task at hand.
First, we'll create a CommentCreateView in users/views.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
We have seen a lot of this before, but a lot is new:
- We subclass a
CreateViewand specify the object this view will be related to (Comment), which fields we want to be able to modify when we createCommentinstances (we only want to modify the comment body), and which template our view is going to be using. - We override the
form_validmethod because we need to specify theauthor(the currently logged-in user) andarticle(the article that the comment is being attached to) fields required by theCommentmodel. - We also override the
get_success_urlbecause we want the user to be redirected to theArticleDetailViewupon saving the comment (we need to import thereverseURL resolver for that).
Adapt ArticleDetailView#
Now, we need to modify the ArticleDetailView to make the CommentCreateView's form available to templates/article_detail.html through the get_context_data method:
1 2 3 4 5 6 7 8 9 10 11 | |
Let's explain our override of get_context_data:
- the
get_context_datamethod populates the dictionary that will be used as the template context super().get_context_data(**kwargs)is boilerplate that populates ourcontextwith the data the view would come with by default- we then pass the newly created
CommentCreateView's form (which we get through itsget_form_classmethod) to theArticleDetailView'scontext(DetailViewgeneric views do not have aform, so we're not overwriting anything here) - we now have access to the relevant context of both our views.
Combine ArticleDetailView and CommentCreateView#
Finally, we create a view that combines ArticleDetailView and CommentCreateView:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
We now have a new hybrid view that, depending on whether the method is GET or POST, will return the ArticleDetailView or the CommentCreateView, respectively.
Adapt the urlpattern#
In order for our new hybrid view to be able to act as intended, we need to specify it as the view that deals with requests to the article/<slug:slug_uuid> path in articles/urls.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Create a template#
Now that articles/views.py and articles/urls.py are ready, we need to create the templates.
Create comment_input.html (based on Svelte implementation's [[https://github.com/sveltejs/realworld/blob/master/src/routes/article/\slug\/_CommentInput.svelte][_CommentInput.svelte]]):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
This POST form corresponds to CommentCreateView's form: when we send submit this form in our app, the ArticleCommentView will return the CommentCreateView, which will process the request and create the requested Comment instance.
In templates/comment_container.html, we include the comment_input.html template:
1 2 3 4 5 6 | |
Everything should be working now. Try to create some comments on an article.
Deleting comments#
We now want to be able to delete comments.
In articles/views.py, add CommentDeleteView:
1 2 3 4 5 6 7 8 9 10 | |
In articles/urls.py, we add a urlpattern:
1 2 3 4 5 6 7 8 | |
We require pk as an argument because CommentDeleteView needs this information to identify the comment to delete. The <slug:slug_uuid> part is unnecessary, but it makes the path more logical, I find.
Create templates/comment_delete.html (based on Svelte implementation's [[https://github.com/sveltejs/realworld/blob/master/src/routes/article/\slug\/_Comment.svelte][_Comment.svelte]]):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
In templates/comment.html:
1 2 3 4 5 6 7 8 9 10 11 | |