Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 24, 2022 02:32 pm GMT

Create a Modern Application with Django and Vue Part Five

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.

You can download the source code for this tutorial here:

Download the Source Code

Comment

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

Not logged in

Logged in

Logged in

Backend

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

models.py

# Comment modelclass 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.

python manage.py makemigrationspython manage.py migrate

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

types.py

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.

mutations.py

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)
class Mutation(graphene.ObjectType):    ...    create_comment = CreateComment.Field()

Remember to add theCreateCommentinside theMutationclass.

Frontend

As for the frontend, let's go toPost.vue, this is where the comments are shown. Please note that I removed some unrelated code in the following examples, if you want the complete code, you can download the source codehere.

Post.vue

<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>
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 thecreated()hook, we retrieve the requested article as well as the comments using thePOST_BY_SLUGquery, which is shown above. Next, in thecomputedproperty, 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 theCommentSectionComponent.

<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.

<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 usepropsto pass information between different components, so I'll skip this part, and let's focus on thesubmitComment()method.

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

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 ofCommentSection.vuefile. Notice that at the end of this code, we used another componentCommentSingle.vueto display one single comment.

<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 theCommentSingle.vuefile.

<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>
<script>export default {  name: "CommentSingleComponent",  data() {    return {      ...    };  },  props: {    comment: {      type: Object,      required: true,    },    userID: {      type: String,      required: true,    },  },};</script>

Like

https://www.ericsdevblog.com/wp-content/uploads/2022/05/image-1024x72.png

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.

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 amany-to-many relationshipbetweenPostandUser.

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.

# Post modelclass 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.

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 theget_number_of_likes()function we defined in the model.

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.

Frontend

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

<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>
<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, thePOST_BY_SLUGquery 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:

export const POST_BY_SLUG = gql`  query ($slug: String!) {    postBySlug(slug: $slug) {      ...      numberOfLikes      likes {        id      }      ...    }  }`;

Next, in thecreated()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 theupdateLike()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 theUPDATE_POST_LIKEmutation:

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?


Original Link: https://dev.to/ericnanhu/create-a-modern-application-with-django-and-vue-part-five-4lpl

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To