Create a Modern Application with Django and Vue #3

You can download the source code for this tutorial here.

Now that we know how to retrieve data using queries and how to send data using mutations, we can try something a little bit more challenging. In this article, we are going to create a comment and a like system for our blog project.

Creating a comment system with Django and Vue.js

Let’s start with the comment section. There are a few things we need to remember before diving into the code. First, for security reasons, only users that are logged in can leave comments. Second, each user can leave multiple comments, and each comment only belongs to one user. Third, each article can have multiple comments, and each comment only belongs to one article. Last but not least, the comment has to be approved by the admin before showing up on the article’s page.

Not logged in

Comment section not logged in

Logged in

Comment logged in

Setting up the backend

With that in mind, let’s start by creating the model for the comments. This part should be fairly easy to understand.

1
2
3
4
5
6
7
8
9
# Comment model
class Comment(models.Model):
    content = models.TextField(max_length=1000)
    created_at = models.DateField(auto_now_add=True)
    is_approved = models.BooleanField(default=False)

    # Each comment belongs to one user and one post
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True) 

Next, we need to apply the changes we’ve made to the models. Go to the terminal and run the following command.

1
2
python manage.py makemigrations
python manage.py migrate

We also need to set up GraphQL at the backend. Let’s add a type for the comment model.

1
2
3
class CommentType(DjangoObjectType):
    class Meta:
        model = models.Comment

And then the mutation. Note that there are three things we need to know to add a comment, the content of the comment, the user that wants to create this comment, and the article that the user is commenting on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class CreateComment(graphene.Mutation):
    comment = graphene.Field(types.CommentType)

    class Arguments:
        content = graphene.String(required=True)
        user_id = graphene.ID(required=True)
        post_id = graphene.ID(required=True)

    def mutate(self, info, content, user_id, post_id):
        comment = models.Comment(
            content=content,
            user_id=user_id,
            post_id=post_id,
        )
        comment.save()

        return CreateComment(comment=comment)
1
2
3
class Mutation(graphene.ObjectType):
    ...
    create_comment = CreateComment.Field()

Remember to add the CreateComment inside the Mutation class.

Setting up the frontend

As for the frontend, let’s go to Post.vue, this is where the comments are shown. Please note that I removed some unrelated code in the following examples, so that the code snippets won’t be too long, but if you wish to have the complete code, you can download the source code here.

Post.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script>
import { POST_BY_SLUG } from "@/queries";
import CommentSectionComponent from "@/components/CommentSection.vue";


export default {
  name: "PostView",

  components: { CommentSectionComponent },

  data() {
    return {
      postBySlug: null,
      comments: null,
      userID: null,
    };
  },

  computed: {
    // Filters out the unapproved comments
    approvedComments() {
      return this.comments.filter((comment) => comment.isApproved);
    },
  },

  async created() {
    // Get the post before the instance is mounted
    const post = await this.$apollo.query({
      query: POST_BY_SLUG,
      variables: {
        slug: this.$route.params.slug,
      },
    });
    this.postBySlug = post.data.postBySlug;
    this.comments = post.data.postBySlug.commentSet;
  },
};
</script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const POST_BY_SLUG = gql`
  query ($slug: String!) {
    postBySlug(slug: $slug) {
      ...
      commentSet {
        id
        content
        createdAt
        isApproved
        user {
          username
          avatar
        }
        numberOfLikes
        likes {
          id
        }
      }
    }
  }
`;

First, in the created() hook, we retrieve the requested article as well as the comments using the POST_BY_SLUG query, which is shown above. Next, in the computed property, we filter out the comments that are not approved by the admin. And finally, we pass the comment, the post ID and the user ID to the CommentSectionComponent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
  <div class="home">
    ...
    <!-- Comment Section -->
    <!-- Pass the approved comments, the user id and the post id to the comment section component -->
    <comment-section-component
      v-if="this.approvedComments"
      :comments="this.approvedComments"
      :postID="this.postBySlug.id"
      :userID="this.userID"
    ></comment-section-component>
  </div>
</template>

CommentSection.vue

Next, let’s take a closer look at the comment section component. This component contains two sections, a form that allows the user to leave comments, which is only shown when the user is logged in, and a list of existing comments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<script>
import { SUBMIT_COMMENT } from "@/mutations";
import CommentSingle from "@/components/CommentSingle.vue";
import { useUserStore } from "@/stores/user";

export default {
  components: { CommentSingle },
  name: "CommentSectionComponent",

  setup() {
    const userStore = useUserStore();
    return { userStore };
  },

  data() {
    return {
      commentContent: "",
      commentSubmitSuccess: false,
      user: {
        isAuthenticated: false,
        token: this.userStore.getToken || "",
        info: this.userStore.getUser || {},
      },
    };
  },
  props: {
    comments: {
      type: Array,
      required: true,
    },
    postID: {
      type: String,
      required: true,
    },
    userID: {
      type: String,
      required: true,
    },
  },
  async created() {
    if (this.user.token) {
      this.user.isAuthenticated = true;
    }
  },
  methods: {
    submitComment() {
      if (this.commentContent !== "") {
        this.$apollo
          .mutate({
            mutation: SUBMIT_COMMENT,
            variables: {
              content: this.commentContent,
              userID: this.userID,
              postID: this.postID,
            },
          })
          .then(() => (this.commentSubmitSuccess = true));
      }
    },
  },
};
</script>

I assume you already know how to use Pinia to verify if the user is logged in, and how to use props to pass information between different components, I’ll skip this part, and let’s focus on the submitComment() method.

When this method is invoked, it will test if the comment is empty, and if not, it will use the SUBMIT_COMMENT mutation to create a new comment. The SUBMIT_COMMENT mutation is defined as follows:

1
2
3
4
5
6
7
8
9
export const SUBMIT_COMMENT = gql`
  mutation ($content: String!, $userID: ID!, $postID: ID!) {
    createComment(content: $content, userId: $userID, postId: $postID) {
      comment {
        content
      }
    }
  }
`;

The following code is the HTML section of CommentSection.vue file. Notice that at the end of this code, we used another component CommentSingle.vue to display one single comment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
  <div class="...">
    <p class="font-bold text-2xl">Comments:</p>

    <!-- If the user is not authenticated -->
    <div v-if="!this.user.isAuthenticated">
      You need to
      <router-link to="/account" >sign in</router-link>
      before you can leave your comment.
    </div>

    <!-- If the user is authenticated -->
    <div v-else>
      <div v-if="this.commentSubmitSuccess" class="">
        Your comment will show up here after is has been approved.
      </div>
      <form action="POST" @submit.prevent="submitComment">
        <textarea
          type="text"
          class="..."
          rows="5"
          v-model="commentContent"
        />

        <button class="...">
          Submit Comment
        </button>
      </form>
    </div>

    <!-- List all comments -->
    <comment-single
      v-for="comment in comments"
      :key="comment.id"
      :comment="comment"
      :userID="this.userID"
    >
    </comment-single>
  </div>
</template>

CommentSingle.vue

Finally, let’s take a closer look at the CommentSingle.vue file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <div class="border-2 p-4">
    <div
      class="flex flex-row justify-start content-center items-center space-x-2 mb-2"
    >
      <img
        :src="`http://127.0.0.1:8000/media/${this.comment.user.avatar}`"
        alt=""
        class="w-10"
      />
      <p class="text-lg font-sans font-bold">
        {{ this.comment.user.username }}
      </p>
    </div>

    <p>
      {{ this.comment.content }}
    </p>
  </div>
</template>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
  name: "CommentSingleComponent",
  data() {
    return {
      ...
    };
  },
  props: {
    comment: {
      type: Object,
      required: true,
    },
    userID: {
      type: String,
      required: true,
    },
  },
};
</script> 

Creating a like system using Django and Vue.js

Like system

As for the like system, there are also a few things we need to keep in mind. First, the user has to be logged in to add a like. Unverified users can only see the number of likes. Second, each user can only send one like to one article, and clicking the like button again would remove the like. Lastly, each article can receive likes from multiple users.

Setting up the backend

Again, let’s start with the models.

Since each article can have many likes from many users, and each user can give many likes to many articles, this should be a many-to-many relationship between Post and User.

Also notice that this time we created a function that returns the total number of likes. Remember to apply these changes to the database using the commands we’ve talked about before.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

# Post model

class Post(models.Model):
    ...

    # Each post can receive likes from multiple users, and each user can like multiple posts
    likes = models.ManyToManyField(User, related_name='post_like')
    
    ...
    
    def get_number_of_likes(self):
        return self.likes.count()

Next, we add the types and mutations.

1
2
3
4
5
6
7
8
9

class PostType(DjangoObjectType):
    class Meta:
        model = models.Post

    number_of_likes = graphene.String()
    
    def resolve_number_of_likes(self, info):
        return self.get_number_of_likes()

Notice that in line 8, self.get_number_of_likes() invokes the get_number_of_likes() function we defined in the model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

class UpdatePostLike(graphene.Mutation):
    post = graphene.Field(types.PostType)

    class Arguments:
        post_id = graphene.ID(required=True)
        user_id = graphene.ID(required=True)
    
    def mutate(self, info, post_id, user_id):
        post = models.Post.objects.get(pk=post_id)
    
        if post.likes.filter(pk=user_id).exists():
            post.likes.remove(user_id)
        else:
            post.likes.add(user_id)
    
        post.save()
    
        return UpdatePostLike(post=post)

To add a like to a post, we need to know the id of the article, and the id of the user that likes this article.

From line 11 to 14, if the post already has a like from the current user, the like will be removed, and if not, a like will be added.

Setting up the frontend

Next, we need to add a like button to our post page. Go back to Post.vue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
  <div class="home">
    ...


    <!-- Like, Comment and Share -->
    <div
      class="..."
    >
      <div v-if="this.liked === true" @click="this.updateLike()">
        <i class="fa-solid fa-thumbs-up">
          <span class="font-sans font-semibold ml-1">{{
            this.numberOfLikes
          }}</span>
        </i>
      </div>
      <div v-else @click="this.updateLike()">
        <i class="fa-regular fa-thumbs-up">
          <span class="font-sans font-semibold ml-1">{{
            this.numberOfLikes
          }}</span>
        </i>
      </div>
      ...
    </div>
    
    ...

  </div>
</template>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script>
import { POST_BY_SLUG } from "@/queries";
import { UPDATE_POST_LIKE } from "@/mutations";
...

export default {
  ...
  async created() {
    ...
    // Find if the current user has liked the post
    let likedUsers = this.postBySlug.likes;

    for (let likedUser in likedUsers) {
      if (likedUsers[likedUser].id === this.userID) {
        this.liked = true;
      }
    }

    // Get the number of likes
    this.numberOfLikes = parseInt(this.postBySlug.numberOfLikes);
  },

  methods: {
    updateLike() {
      if (this.liked === true) {
        this.numberOfLikes = this.numberOfLikes - 1;
      } else {
        this.numberOfLikes = this.numberOfLikes + 1;
      }
      this.liked = !this.liked;

      this.$apollo.mutate({
        mutation: UPDATE_POST_LIKE,
        variables: {
          postID: this.postBySlug.id,
          userID: this.userID,
        },
      });
    },
  },
};
</script> 

I deleted some code to make this example shorter, but there are still four things we need to talk about in this example. First, the POST_BY_SLUG query that we use to retrieve the article, we need to make sure that it returns the number of likes and the users that already liked the article:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export const POST_BY_SLUG = gql`
  query ($slug: String!) {
    postBySlug(slug: $slug) {
      ...
      numberOfLikes
      likes {
        id
      }
      ...
    }
  }
`;

Next, in the created() hook, after we’ve retrieved the post, we determine if the current user is in the list of users that already liked the post.

Then, in the updateLike() method, when this method is invoked, it will change the number of likes based on whether or not the user has liked the post.

Finally, the method updates the post’s likes in the backend using the UPDATE_POST_LIKE mutation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const UPDATE_POST_LIKE = gql`
  mutation ($postID: ID!, $userID: ID!) {
    updatePostLike(postId: $postID, userId: $userID) {
      post {
        id
        title
        likes {
          id
        }
      }
    }
  }
`;

A Challenge

After learning how to create a comment and a like system, let’s consider a more challenging task. What if we want to create a nested commenting system, where users can comment on another comment? How can we change our code to make this possible? And how can we create a like system for the comment as well?

The implementation of these functionalities are included in the source code of this tutorial.


comments powered by Disqus