views:

76

answers:

1

I have a long list of links that I spit out using the below code, total votes, submitted by, the usual stuff but I am not 100% on how to determine if the currently logged in user has voted on a link or not. I know how to do this from within my view but do I need to alter my below view code or can I make use of the way templates work to determine it?

I have read http://stackoverflow.com/questions/1528583/django-vote-up-down-method but I don't quite understand what's going on ( and don't need any ofjavascriptery).

Models (snippet):

class Link(models.Model):
    category = models.ForeignKey(Category, blank=False, default=1)
    user = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    url = models.URLField(max_length=1024, unique=True, verify_exists=True)
    name = models.CharField(max_length=512)

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.url)

class Vote(models.Model):
    link = models.ForeignKey(Link)
    user = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return u'%s vote for %s' % (self.user, self.link)

Views (snippet):

def hot(request):
    links = Link.objects.select_related().annotate(votes=Count('vote')).order_by('-created')
    for link in links:
        delta_in_hours = (int(datetime.now().strftime("%s")) - int(link.created.strftime("%s"))) / 3600
        link.popularity = ((link.votes - 1) / (delta_in_hours + 2)**1.5)
        if request.user.is_authenticated():
            try:
                link.voted = Vote.objects.get(link=link, user=request.user)
            except Vote.DoesNotExist:
                link.voted = None

    links = sorted(links, key=lambda x: x.popularity, reverse=True)

    links = paginate(request, links, 15)

    return direct_to_template(
        request,
        template = 'links/link_list.html',
        extra_context = {
            'links': links,
        })

The above view actually accomplishes what I need but in what I believe to be a horribly inefficient way. This causes the dreaded n+1 queries, as it stands that's 33 queries for a page containing just 29 links while originally I got away with just 4 queries. I would really prefer to do this using Django's ORM or at least .extra().

Any advice?

EDIT

@Gabriel Hurley

I am trying to recreate your answer and I'm have mixed results, let me show ya what I got.

views.py

links = Link.objects.select_related().extra(
    select={
        'votes': 'COUNT(links_vote.id)',
        'voted': 'SELECT COUNT(links_vote.id) FROM links_vote WHERE links_vote.user_id = 1 AND links_vote.link_id = links_link.id',
    },
    tables = ['links_vote']
)

models.py

class Vote(models.Model):
    link = models.ForeignKey(Link)
    user = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('link', 'user')

    def __unicode__(self):
        return u'%s vote for %s' % (self.user, self.link)

But it is returning an error:

subquery uses ungrouped column "links_link.id" from outer query

LINE 1: ...E links_vote.user_id = 1 AND links_vote.link_id = links_link...

The query that is generated looks something (exactly) like this:

SELECT (SELECT COUNT(links_vote.id) FROM links_vote WHERE links_vote.user_id = 1 AND links_vote.link_id = links_link.id) AS "voted", "links_link"."id", "links_link"."category_id", "links_link"."user_id", "links_link"."created", "links_link"."modified", "links_link"."url", "links_link"."name", "links_category"."id", "links_category"."name", "auth_user"."id", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."password", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."is_superuser", "auth_user"."last_login", "auth_user"."date_joined" FROM "links_link" INNER JOIN "links_category" ON ("links_link"."category_id" = "links_category"."id") INNER JOIN "auth_user" ON ("links_link"."user_id" = "auth_user"."id") , "links_vote"

I am using PostgreSQL which I know loves GROUP BY but I am not 100% on how to correct this.

EDIT 2 (Major Progress)

links = Link.objects.select_related().annotate(votes=Count('vote')).extra( select={ #'voted': 'SELECT COUNT(*) FROM links_vote WHERE links_vote.user_id = %s AND links_vote.link_id = links_link.id' % (request.user.id), #'voted': '' % (request.user.id), #'voted': 'SELECT CASE WHEN links_vote.user_id = %s THEN 1 ELSE 0 END' % (request.user.id), #'voted': 'SELECT COUNT(*) FROM links_vote WHERE links_vote.link_id = links_link.id AND links_vote.user_id = %s' % (request.user.id), }, where=['links_link.id = links_vote.link_id'], ).order_by('-created')

*This only works after applying a patch for a bug from here (http://code.djangoproject.com/ticket/11916)

I am so close to finding that last piece I need to determine if a user has voted...

+2  A: 

I've dealt with this before and solved it with extra more or less like so:

# annotate whether you've already voted on this item
table = Vote._meta.db_table
select = 'SELECT COUNT(id) FROM %s' %table
where1 = 'WHERE ' + table + '.user_id = %s'
where2 = 'AND ' + table + '.item_id = appname_item.id'
items = items.extra(
        select={'votes':" ".join((select, where1, where2,))},
        select_params=(request.user.id,)
    )

Effectively this takes a QuerySet of items and annotates each one with either a 0 or some number of votes. In my system I use unique_together = ('link', 'user') on Vote to make sure each user can only vote once, so the annotated data is either 0 or 1 (effectively boolean). It works quite well and avoids the n+1 problem.

Gabriel Hurley
I have a few questions that won't fit here, see my edit above. Thanks though! I feel this is putting me on the right track.
TheLizardKing