1

I'm new to django, so please be patient :)

Like many before, I'm trying to build a reddit clone. I've got it all working, but one part is missing. Without producing too many database requests, I would like to indicate whether the current user has voted for a specific question.

This is what my models look like:

from django.db import models
from django.conf import settings
from mptt.models import MPTTModel, TreeForeignKey

max_post_length = 2000


class Thread(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField(max_length=max_post_length)
    created = models.DateField()
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    userUpVotes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='threadUpVotes')
    userDownVotes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='threadDownVotes')

    def __str__(self):
        return self.title


class Comment(MPTTModel):
    title = models.CharField(max_length=200)
    text = models.TextField(max_length=max_post_length)
    created = models.DateField()
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    thread = models.ForeignKey(Thread)
    parent = TreeForeignKey('self', related_name='children', blank=True, null=True)
    vote_count = models.IntegerField(default=0)
    userUpVotes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='commentUpVotes')
    userDownVotes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='commentDownVotes')

    class MPTTMeta:
        order_insertion_by = ['created']

    def save(self, *args, **kwargs):
        self.vote_count = self.userUpVotes.count() - self.userDownVotes.count()
        super(Comment, self).save(*args, **kwargs)

    def __str__(self):
        return self.title

This is my view:

from django.shortcuts import get_object_or_404, render
from community.models import Thread, Comment
from django.http import HttpResponse


def detail(request, thread_id):
    thread = get_object_or_404(Thread, pk=thread_id)
    comments = thread.comment_set.all()

    return render(request, 'threads/detail.html', {
        'thread': thread,
        'comments': comments
    })

And this is my template:

{% extends "base.html" %}
{% load mptt_tags %}
{% block content %}
    <h1>{{ thread.title }}</h1>
    <p>{{ thread.text }}</p>
    <ul class="comments">
        {% recursetree comments %}
            <li>
                <div class="comment-block clearfix">
                    <vote-up-down up="{{ node.up_vote_by_user }}"
                                  down="{{ node.user_down_vote }}"
                                  url="/community/comment/{{ node.id }}/vote/"
                                  count="{{ node.vote_count }}"></vote-up-down>
                    <div class="comment">
                        <h4>{{ node.title }}</h4>

                        <div class="text">{{ node.text }}</div>
                    </div>
                </div>
                {% if not node.is_leaf_node %}
                    <ul class="children">
                        {{ children }}
                    </ul>
                {% endif %}
            </li>
        {% endrecursetree %}
    </ul>
{% endblock content %}

What would be the best way to populate node.up_vote_by_user and node.down_vote_by_user? I tried using a user middleware and model methods. I also tried pre-iterating the comments, but that doesn't work together with the recursion used for the list.

hugo der hungrige
  • 12,382
  • 9
  • 57
  • 84

1 Answers1

1

First of all, it looks like Comment is superset of Thread, in other words, they both are using same fields like title, text, user, created etc, so if I were in your shoes, I would create an abstract base class named Node and put those fields into base model:

class Node(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField(max_length=max_post_length)
    created = models.DateField()
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    up_votes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='%(class)s_upvotes')
    down_votes = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='%(class)s_downvotes')

    class Meta:
        abstract = True

    def __str__(self):
        return self.title

class Thread(Node):
    pass

class Comment(Node, MPTTModel):
    thread = models.ForeignKey(Thread)
    parent = TreeForeignKey('self', related_name='children', blank=True, null=True)
    vote_count = models.IntegerField(default=0)

    class MPTTMeta(Node.Meta):
        order_insertion_by = ['created']

For your actual question about how to find whether a user voted a specific question or not, assuming you don't want to replace your own User model with the existing one, I would definitely go with iterating the users: (I don't know why it didn't work for you)

class Node(models.Model):
    ...

   def upvoted_by(self, user):
       return self.up_votes.filter(user=user).exists()

   def downvoted_by(self, user):
       return self.down_votes.filter(user=user).exists()

By this way, you will even be able to search not only for comments but also for threads voted by a specific user. However, in order to use those methods in a template, you will have to write your own template filter that will take request.user as a parameter.

@register.filter
def up_voted_by(obj, user):
    return obj.upvoted_by(user)

I don't think you will be satisfied with the performance of the above solution since it hits the database for each node instance, so you may end up having a webpage taking too long to load if thread has so many comments. Thus, the better option would be iterating user's up-votes and filter the ones matching with the thread.

(There is another solution which I think is faster and performant for getting all descendants of comment objects in here, but I've found it difficult to understand.)

def get_all_descendants(nodes, include_self=True):
     result = set()
     for node in nodes:
        descendants = node.get_descendants(include_self=include_self)
        result.update(set(descendats.values_list('pk', flat=True)))
     return result

def detail(request, thread_id):
    thread = get_object_or_404(Thread, pk=thread_id)

    comments = thread.comment_set.all()
    descendants = get_all_descendants(comments, include_self=True)

    upvoted_comments = request.user.comment_upvotes.filter(
        id__in=descendants).values_list('pk', flat=True)

    downvoted_comments = request.user.comment_downvotes.filter(
        id__in=descendants).exclude(
        id__in=upvoted_comments).values_list('pk', flat=True)

    return render(request, 'threads/detail.html', {
        'thread': thread,
        'comments': comments,
        'upvoted_comments': set(upvoted_comments),
        'downvoted_comments': set(downvoted_comments)
    })

Then, in your template you can easily check whether a comment is up-voted or down-voted in 0(1):

{% recursetree comments %}
 <li>
   <div class="comment-block clearfix">
     <vote-up-down up="{%if node.pk in upvoted_comments %}{% endif %}"
                 down="{%if node.pk in downvoted_comments %}{% endif %}"

  ...
Community
  • 1
  • 1
Ozgur Vatansever
  • 49,246
  • 17
  • 84
  • 119
  • Thank you very much. That is already very helpful. I was becoming frustrated with this one. ```upvoted_comments = request.user.comment_upvotes.filter( comment__in=descendants).values_list('pk', flat=True)``` and ```downvoted_comments = request.user.comment_downvotes.filter(comment__in=comment_ids).exclude(id__in=upvoted_comments).values_list('pk', flat=True)``` dont seem to work though. – hugo der hungrige Feb 21 '15 at 15:14