0

I want to count number of replies on a particular post in Django

View.py

def forum(request):
    profile = Profile.objects.all()
    if request.method=="POST":   
        user = request.user
        image = request.user.profile.image
        content = request.POST.get('content','')
        post = Post(user1=user, post_content=content, image=image)
        post.save()
        messages.success(request, f'Your Question has been posted successfully!!')
        return redirect('/forum')
    posts = Post.objects.filter().order_by('-timestamp')
    return render(request, "forum.html", {'posts':posts})

Reply code

def discussion(request, myid):
    post = Post.objects.filter(id=myid).first()
    replies = Replie.objects.filter(post=post)
    if request.method=="POST":
        user = request.user
        image = request.user.profile.image
        desc = request.POST.get('desc','')
        post_id =request.POST.get('post_id','')
        reply = Replie(user = user, reply_content = desc, post=post, image=image)
        reply.save()
        messages.success(request, f'Your Reply has been posted successfully!!')
        return redirect('/forum')
    return render(request, "discussion.html", {'post':post, 'replies':replies})    

model.py

class Post(models.Model):
    user1 = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    post_id = models.AutoField
    post_content = models.CharField(max_length=5000)
    timestamp= models.DateTimeField(default=now)
    image = models.ImageField(upload_to="images",default="")
    def __str__(self):
       return f'{self.user1} Post'

class Replie(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    reply_id = models.AutoField
    reply_content = models.CharField(max_length=5000) 
    post = models.ForeignKey(Post, on_delete=models.CASCADE, default='')
    timestamp= models.DateTimeField(default=now)
    image = models.ImageField(upload_to="images",default="")
    def __str__(self):
       return f'{self.user1} Post'

My Forum.html code:

{% for post in posts %}
<div class="container-fluid mt-10">
  <div class="row">
      <div class="col-md-12">
          <div class="card mb-4 forumcardcss">
              <div class="card-header forumcardheader">
                  <div class="media flex-wrap w-100 align-items-center imgcss"> <img src="/media/{{post.image}}" 
                          class="d-block ui-w-40 rounded-circle" alt="profileimage"style="width: 40px;height: 40px;">  <p class="ml-4 usernamecss">  {{post.user1}} </p>
                      <div class="media-body ml-3"> <a href="/discussion/{{post.id}}" data-abc="true"><button class="btn btn-light" style="color:blue; font-size: 13px;">Add or See reply </button>  </a>
                      </div>
                      <div class="text-muted small ml-3">
                        <div class="px-4 pt-3">Nmber of reply   &nbsp;&nbsp;  {{post.timestamp}} </div>
                      </div>
                       {% if user.is_superuser or user.is_staff %}
                        <a href="{% url 'dashboard:delete_post' post.id %}"> <button class="btn btn-danger btn-sm" onclick="window.mytest()">Delete Post</button></a>
                        <script type="text/javascript">window.mytest = function() { var isValid = confirm('If you click ok then its delete this post and related reply on it. Are you sure to delete?');if (!isValid) { event.preventDefault();  alert("It wont delete. Yay!");}}</script>
                        {% endif %}
                     </div>
                   </div>         
               <div class="card-body forumcardbody">
                  <p>{{post.post_content}}</p>
              </div>
              <div class="card-footer d-flex flex-wrap justify-content-between align-items-center px-0 pt-0 pb-3">                  
              </div>
          </div>
      </div>
  </div>
</div>
{% endfor %}

I want to do like this

this

where on the place of Number of reply, I want to display the number of replies of the particular post

Is there any way to find if Question(Post) has been answered(reply) on my post page(forum.py) I want to do it like this If the Question has been answered then it should show "Answered" else "Not answered yet"

Ajay
  • 31
  • 1
  • 6
  • A few comments on the models - you don't need to explicitly create the primary key as this is done by Django automatically (see https://docs.djangoproject.com/en/4.0/topics/db/models/#automatic-primary-key-fields-1). So `post_id = models.AutoField` on the `Post` model and `reply_id = models.AutoField` on the Replie model isn't necessary. Also, if you always want to automatically create the timestand on create the `DateTimeField` has the `auto_now_add` attribute that does that for you (https://docs.djangoproject.com/en/4.0/ref/models/fields/#django.db.models.DateField.auto_now_add). – eega May 08 '22 at 09:01
  • I tried to fix the obvious syntax errors in the broken indentation but I obviously had to guess; please review. Perhaps the easiest fix is to replace it with your actual code. On the desktop version of this site, you can get code marked up for you by pasting your code, selecting the pasted block, and typing ctrl-K. – tripleee May 09 '22 at 10:21
  • Does this answer your question? [Django. How to annotate a object count from a related model](https://stackoverflow.com/questions/33356152/django-how-to-annotate-a-object-count-from-a-related-model) – Abdul Aziz Barkat May 09 '22 at 13:33

2 Answers2

0

To archive this you can use the related name of the Post model (have a look at the documentation). Django will create a field for every foreign key that allows you to access the related model. By default, this will be named replie_set on the Post model.

This field you can then use to get the number of replies to a post by calling the count() method of the replie_set queryset. I would also add a method to the Post model that does that for you as a convenience.

To bring this together, your Post model would look like this:

class Post(models.Model):
    user1 = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    post_id = models.AutoField
    post_content = models.CharField(max_length=5000)
    timestamp= models.DateTimeField(default=now)
    image = models.ImageField(upload_to="images",default="")
    
    @property
    def count_replies():
        return self.replies_set.count()

    def __str__(self):
       return f'{self.user1} Post'

Assuming that your forum.html template iterates over the posts like that:

{% for post in posts %}
    ...
    <p>Posts: {{ post.count_replies }}</p>
    ...
{% endfor %}

You get the number of replies by calling post.count_replies(). Of course, if you don't want to add a dedicated method to the model you can just use do post.replie_set.count() directly.


An alternative - and more efficient method - is to annotate your posts with the reply count as Abdul Aziz Barkat suggests. To do this you have to change your view like that:

from django.db.models import Count

def forum(request):
    profile = Profile.objects.all()
    if request.method=="POST":   
        user = request.user
        image = request.user.profile.image
        content = request.POST.get('content','')
        post = Post(user1=user, post_content=content, image=image)
        post.save()
        messages.success(request, f'Your Question has been posted successfully!!')
        return redirect('/forum')
    posts = Post.objects.annotate(count_replies=Count("replie")).order_by('-timestamp')
    return render(request, "forum.html", {'posts':posts})

I changed only the second to last line here:

   posts = Post.objects.annotate(count_replies=Count("replie")).order_by('-timestamp')

This adds the aggregated count of replies to each post as count_replies.

Then this value is used in the forum.html template like that:

{% for post in posts %}
<div class="container-fluid mt-10">
  <div class="row">
      <div class="col-md-12">
          <div class="card mb-4 forumcardcss">
              <div class="card-header forumcardheader">
                  <div class="media flex-wrap w-100 align-items-center imgcss"> <img src="/media/{{post.image}}" 
                          class="d-block ui-w-40 rounded-circle" alt="profileimage"style="width: 40px;height: 40px;">  <p class="ml-4 usernamecss">  {{post.user1}} </p>
                      <div class="media-body ml-3"> <a href="/discussion/{{post.id}}" data-abc="true"><button class="btn btn-light" style="color:blue; font-size: 13px;">Add or See reply </button>  </a>
                      </div>
                      <div class="text-muted small ml-3">
                        <div class="px-4 pt-3">Number of replies   &nbsp;&nbsp;  {{ post.count_replies }} </div>
                      </div>
                       {% if user.is_superuser or user.is_staff %}
                        <a href="{% url 'dashboard:delete_post' post.id %}"> <button class="btn btn-danger btn-sm" onclick="window.mytest()">Delete Post</button></a>
                        <script type="text/javascript">window.mytest = function() { var isValid = confirm('If you click ok then its delete this post and related reply on it. Are you sure to delete?');if (!isValid) { event.preventDefault();  alert("It wont delete. Yay!");}}</script>
                        {% endif %}
                     </div>
                   </div>         
               <div class="card-body forumcardbody">
                  <p>{{post.post_content}}</p>
              </div>
              <div class="card-footer d-flex flex-wrap justify-content-between align-items-center px-0 pt-0 pb-3">                  
              </div>
          </div>
      </div>
  </div>
</div>
{% endfor %}

So, only a single line changed here either:

<div class="px-4 pt-3">Number of replies   &nbsp;&nbsp;  {{ post.count_replies }} </div>
eega
  • 479
  • 8
  • 20
  • thank you eega! I have a doubts what about view.py I have to make changes in view.py also or not. According to your answer I have try as: I put `def count_replies(): return self.replies_set.count() `in POST model and `post.count_replies()` in forum.py but I got an error **Could not parse the remainder: '()' from 'post.count_replies()' ** – Ajay May 08 '22 at 09:41
  • @Ajay Show your template file where you are using it? – Sunderam Dubey May 09 '22 at 08:08
  • I assume you have used `{{ post.count_replies() }}` in your template, but it should be `{{ post.count_replies }}`. However, this is really reading tea leaves without you providing `forum.html`. – eega May 09 '22 at 10:12
  • Regarding `view.py`- no, nothing has to change here as far as I can tell, as you are already providing the posts as context to the template. – eega May 09 '22 at 10:13
  • But after implementing your solution I got an error message `Could not parse the remainder: '()' from 'post.count_replies()` – Ajay May 09 '22 at 10:19
  • I Have added my forum .html code snippet – Ajay May 09 '22 at 10:26
  • Hi @Ajay change the definition from `def count_replies(self):` to `@property def count_replies(self):` – Deepak Tripathi May 09 '22 at 11:05
  • Right, did forget that. Thanks for clearing that up! I've adapted the answer as well. – eega May 09 '22 at 13:11
  • It should be `replie_set` instead of `replies_set` @Ajay Also this will be inefficient since this would result in the [N+1 selects problem](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping). It is better to annotate the count which would give you just a single query. See the example in the [documentation](https://docs.djangoproject.com/en/4.0/topics/db/aggregation/#generating-aggregates-for-each-item-in-a-queryset) – Abdul Aziz Barkat May 09 '22 at 13:31
  • After changing replies_set to replie_set Error has gone but the reply count is not appearing on the page – Ajay May 09 '22 at 13:44
  • @Deepak Tripathi as your suggested solution I made same changes in view.py `post = Post.objects.get(id=myid).annotate(post_count=Count("replie"))` and in forum.html `{{post.post_count }} ` but I do not get solutions of my problem please help to figure it out – Ajay May 09 '22 at 16:24
  • Can you share the output of `print(posts)` ? before you render the html ? – Deepak Tripathi May 09 '22 at 16:27
  • ok I will send it now but first please tell me what about Count() method it gives me error `Count is not defined` should I have to import count – Ajay May 09 '22 at 16:30
  • I do in my view.py `post = Post.objects.get(id=myid).annotate(post_count=count("replie")) print(post) replies = Replie.objects.filter(post=post) if request.method=="POST": user = request.user image = request.user.profile.image desc = request.POST.get('desc','') post_id =request.POST.get('post_id','') reply = Replie(user = user, reply_content = desc, post=post, image=image) reply.save() ` but `print(post)` does not print anything in my console – Ajay May 09 '22 at 16:38
  • The reason you get `Count is not defined` is most likely that you need to import it first: `from django.db.models import Count`. – eega May 10 '22 at 19:30
0

@Eega suggested the right answer just some changes in the code will help you

class Post(models.Model):
    user1 = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    post_id = models.AutoField
    post_content = models.CharField(max_length=5000)
    timestamp= models.DateTimeField(default=now)
    image = models.ImageField(upload_to="images",default="")
    
    @property
    def count_replies(self):
        return self.replie_set.count()

    def __str__(self):
       return f'{self.user1} Post'

post = Post.objects.filter(id=myid).first() to post = Post.objects.filter(id=myid).first().prefetch_related('replies_set') This will make your query optimized Also accept @Eega answer only, I have just showed you the edited code

Now I am suggesting one good method here

Post.objects.get(id=myid).annotate(post_count=Count("replie"))

Simply use this in your views without changing #models.py and access it in your template as post.post_count in for loop.

Deepak Tripathi
  • 3,175
  • 1
  • 8
  • 21
  • after implementing both solutions I am getting `AttributeError at /forum 'Post' object has no attribute 'replies_set'` error – Ajay May 09 '22 at 13:20
  • I try to do like this in my forum.html `
    {{ post.count_replies }}    {{post.timestamp}}
    `
    – Ajay May 09 '22 at 13:24
  • Adding a `prefetch_related` will not optimize anything here, the `count` method is a _separate_ query regardless. Also if you feel that you just need to edit a bit of the code you can always propose an edit... – Abdul Aziz Barkat May 09 '22 at 13:28
  • I add code in model.py like this `def count_replies(self): return self.replies_set.count()` – Ajay May 09 '22 at 13:29
  • If I am not writing self in `def count_replies()` function then in VS code It shows me " self " is not defined – Ajay May 09 '22 at 13:31
  • @AbdulAzizBarkat Obviously I have tried to edit but the editing queue is filled – Deepak Tripathi May 09 '22 at 13:51
  • @Ajay I have edited the answer now check – Deepak Tripathi May 09 '22 at 13:53
  • what about Count() method in this line `Post.objects.get(id=myid).annotate(post_count=Count("replie"))` when i use this line in my view.py i got an error `Count is not defined ` To remove this error I import `from itertools import count` but I do not get any reply count in my post – Ajay May 09 '22 at 16:22
  • If I use `Post.objects.get(id=myid).annotate(post_count=Count("replie"))` in view.py to the discussion view and `{{post.post_count }}` in discussion.html then I got this error `AttributeError at /discussion/4 'Post' object has no attribute 'annotate'` Note I use `{{post.post_count }}` in discussion.html only for trail to see if number of reply comes their I am getting nothing their also. I want to print number of reply in forum.html page – Ajay May 10 '22 at 04:23
  • I have a suggestion All of you provide solution like this: make changes in the Post model and discussion view and print the number of replies in forum.html. I think if I want to print the number of replies on forum.html page then I should make changes in the forum view. My suggestion maybe wrong please provide a accurate solution for my problem – Ajay May 10 '22 at 04:30
  • I have update my question please see that – Ajay May 10 '22 at 05:26
  • I have updated the answer to include the solution using aggregation. Hope this solves your issues now. – eega May 10 '22 at 20:10