# HG changeset patch # User Brian Neal # Date 1306007748 0 # Node ID 1f139de929c4a12c4eaabfbf9497a3d7e0dc27c9 # Parent 524fd1b3919af94a75b50513fdb22c1b1856ef3b Fixing #216; added anti-ballot stuffing feature to the polls application. diff -r 524fd1b3919a -r 1f139de929c4 gpp/polls/admin.py --- a/gpp/polls/admin.py Wed May 18 03:04:25 2011 +0000 +++ b/gpp/polls/admin.py Sat May 21 19:55:48 2011 +0000 @@ -10,13 +10,14 @@ class ChoiceInline(admin.TabularInline): model = Choice extra = 3 + raw_id_fields = ['voters'] class PollAdmin(admin.ModelAdmin): - list_display = ('question', 'start_date', 'end_date', 'is_enabled') - inlines = (ChoiceInline, ) - list_filter = ('start_date', 'end_date') - search_fields = ('question', ) + list_display = ['question', 'start_date', 'end_date', 'is_enabled'] + inlines = [ChoiceInline] + list_filter = ['start_date', 'end_date'] + search_fields = ['question'] date_hierarchy = 'start_date' diff -r 524fd1b3919a -r 1f139de929c4 gpp/polls/forms.py --- a/gpp/polls/forms.py Wed May 18 03:04:25 2011 +0000 +++ b/gpp/polls/forms.py Sat May 21 19:55:48 2011 +0000 @@ -1,16 +1,28 @@ """Forms for the Polls application.""" from django import forms +from django.db.models import F from polls.models import Choice class VoteForm(forms.Form): - """Form for voting in a poll.""" - choices = forms.ModelChoiceField(label='', empty_label=None, - queryset=Choice.objects.none(), widget=forms.RadioSelect) + """Form for voting in a poll.""" + choices = forms.ModelChoiceField(label='', empty_label=None, + queryset=Choice.objects.none(), widget=forms.RadioSelect) - def __init__(self, poll, *args, **kwargs): - super(VoteForm, self).__init__(*args, **kwargs) - self.fields['choices'].queryset = poll.choice_set.all() + def __init__(self, poll, *args, **kwargs): + self.user = kwargs.pop('user', None) + self.user_choice = kwargs.pop('user_choice', None) + super(VoteForm, self).__init__(*args, **kwargs) + self.fields['choices'].queryset = poll.choice_set.all() + def clean(self): + if self.user_choice: + raise forms.ValidationError("You've already voted in this poll!") + return self.cleaned_data + + def save(self): + choice = self.cleaned_data['choices'] + Choice.objects.filter(id=choice.id).update(votes=F('votes') + 1) + choice.voters.add(self.user) diff -r 524fd1b3919a -r 1f139de929c4 gpp/polls/models.py --- a/gpp/polls/models.py Wed May 18 03:04:25 2011 +0000 +++ b/gpp/polls/models.py Sat May 21 19:55:48 2011 +0000 @@ -3,82 +3,92 @@ """ import datetime + +from django.contrib.auth.models import User from django.db import models from django.db.models import Q class PollManager(models.Manager): - """Manager for the Poll model""" + """Manager for the Poll model""" - def get_current_polls(self): - now = datetime.datetime.now() - return self.filter( - Q(is_enabled=True), - Q(start_date__lte=now), - Q(end_date__isnull=True) | Q(end_date__gte=now)) + def get_current_polls(self): + now = datetime.datetime.now() + return self.filter( + Q(is_enabled=True), + Q(start_date__lte=now), + Q(end_date__isnull=True) | Q(end_date__gte=now)) - def get_old_polls(self): - now = datetime.datetime.now() - return self.filter( - Q(is_enabled=True), - Q(end_date__isnull=False), - Q(end_date__lt=now)) + def get_old_polls(self): + now = datetime.datetime.now() + return self.filter( + is_enabled=True, + end_date__isnull=False, + end_date__lt=now) class Poll(models.Model): - """Model to represent polls""" - start_date = models.DateTimeField(db_index=True, - help_text='Date/time the poll will be eligible for voting.',) - end_date = models.DateTimeField(blank=True, null=True, db_index=True, - help_text='Date/time the poll will be ineligible for voting. ' - 'Leave blank for an open ended poll.') - is_enabled = models.BooleanField(default=True, db_index=True, - help_text='Check to allow the poll to be viewed on the site.') - question = models.CharField(max_length=200) + """Model to represent polls""" + start_date = models.DateTimeField(db_index=True, + help_text='Date/time the poll will be eligible for voting.',) + end_date = models.DateTimeField(blank=True, null=True, db_index=True, + help_text='Date/time the poll will be ineligible for voting. ' + 'Leave blank for an open ended poll.') + is_enabled = models.BooleanField(default=True, db_index=True, + help_text='Check to allow the poll to be viewed on the site.') + question = models.CharField(max_length=200) - objects = PollManager() + objects = PollManager() - def __unicode__(self): - return self.question + def __unicode__(self): + return self.question - class Meta: - ordering = ('-start_date', ) - get_latest_by = 'start_date' + class Meta: + ordering = ('-start_date', ) + get_latest_by = 'start_date' - @models.permalink - def get_absolute_url(self): - return ('polls.views.poll_detail', [str(self.id)]) + @models.permalink + def get_absolute_url(self): + return ('polls-detail', [], {'poll_id': str(self.id)}) - def results(self): - """ - Returns a tuple; element 0 is the total votes, element 1 is a list of - {choice, votes, pct} - """ - choices = [] - total_votes = 0 - for choice in self.choice_set.all(): - total_votes += choice.votes - choices.append({'choice': choice.choice, 'votes': choice.votes, 'pct': 0.0}) + def results(self): + """ + Returns a tuple; element 0 is the total votes, element 1 is a list of + {choice, votes, pct} + """ + choices = [] + total_votes = 0 + for choice in self.choice_set.all(): + total_votes += choice.votes + choices.append({'choice': choice.choice, 'votes': choice.votes, 'pct': 0.0}) - if total_votes > 0: - for choice in choices: - choice['pct'] = float(choice['votes']) / total_votes * 100.0 + if total_votes > 0: + for choice in choices: + choice['pct'] = float(choice['votes']) / total_votes * 100.0 - return (total_votes, choices) + return (total_votes, choices) - def is_open(self): - now = datetime.datetime.now() - return self.start_date <= now and (not self.end_date or now <= self.end_date) + def total_votes(self): + """ + Returns the number of votes cast in this poll to date. - def can_comment_on(self): - return self.is_open() + """ + return sum(choice.votes for choice in self.choice_set.all()) + + def is_open(self): + now = datetime.datetime.now() + return self.start_date <= now and (not self.end_date or now <= self.end_date) + + def can_comment_on(self): + return self.is_open() class Choice(models.Model): - """Model for poll choices""" - poll = models.ForeignKey(Poll) - choice = models.CharField(max_length = 200) - votes = models.IntegerField(default = 0) + """Model for poll choices""" + poll = models.ForeignKey(Poll) + choice = models.CharField(max_length=200) + votes = models.IntegerField(default=0) + voters = models.ManyToManyField(User, blank=True) - def __unicode__(self): - return self.choice + def __unicode__(self): + return self.choice diff -r 524fd1b3919a -r 1f139de929c4 gpp/polls/urls.py --- a/gpp/polls/urls.py Wed May 18 03:04:25 2011 +0000 +++ b/gpp/polls/urls.py Sat May 21 19:55:48 2011 +0000 @@ -6,4 +6,5 @@ url(r'^(?P\d+)/$', 'poll_detail', name='polls-detail'), url(r'^(?P\d+)/results/$', 'poll_results', name='polls-results'), url(r'^(?P\d+)/vote/$', 'poll_vote', name='polls-vote'), + url(r'^delete_vote/$', 'poll_delete_vote', name='polls-delete_vote'), ) diff -r 524fd1b3919a -r 1f139de929c4 gpp/polls/views.py --- a/gpp/polls/views.py Wed May 18 03:04:25 2011 +0000 +++ b/gpp/polls/views.py Sat May 21 19:55:48 2011 +0000 @@ -1,12 +1,15 @@ -"""Views for the polls application""" +""" +Views for the polls application. -from django.shortcuts import render_to_response -from django.template import RequestContext +""" +from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404 from django.http import Http404 from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse +from django.views.decorators.http import require_POST +from django.db.models import F from polls.models import Poll from polls.models import Choice @@ -14,64 +17,89 @@ ####################################################################### +def get_user_choice(user, poll): + """ + Return the Choice object the give user voted for from the given poll, + or None of no vote has been recorded (or the user is not authenticated. + + """ + user_choice = None + if user.is_authenticated(): + user_choices = user.choice_set.filter(poll=poll) + if user_choices: + user_choice = user_choices[0] + + return user_choice + +####################################################################### + def poll_index(request): - current_polls = Poll.objects.get_current_polls() - old_polls = Poll.objects.get_old_polls() - return render_to_response('polls/index.html', { - 'current_polls': current_polls, - 'old_polls': old_polls, - }, - context_instance = RequestContext(request)) + current_polls = Poll.objects.get_current_polls() + old_polls = Poll.objects.get_old_polls() + return render(request, 'polls/index.html', { + 'current_polls': current_polls, + 'old_polls': old_polls, + }) ####################################################################### def poll_detail(request, poll_id): - poll = get_object_or_404(Poll, pk = poll_id) - if not poll.is_enabled: - raise Http404 + poll = get_object_or_404(Poll, pk=poll_id) + if not poll.is_enabled: + raise Http404 - return render_to_response('polls/poll.html', { - 'poll': poll, - }, - context_instance = RequestContext(request)) + return render(request, 'polls/poll.html', { + 'poll': poll, + 'user_choice': get_user_choice(request.user, poll), + }) ####################################################################### @login_required def poll_vote(request, poll_id): - poll = get_object_or_404(Poll, pk = poll_id) - if not poll.is_enabled: - raise Http404 - if not poll.is_open(): - return HttpResponseRedirect(reverse('polls.views.poll_results', args=[poll_id])) + poll = get_object_or_404(Poll, pk=poll_id) + if not poll.is_enabled: + raise Http404 + if not poll.is_open(): + return HttpResponseRedirect(reverse('polls-results', args=[poll_id])) - if request.method == "POST": - vote_form = VoteForm(poll, request.POST) - if vote_form.is_valid(): - choice_id = request.POST.get('choices', None) - choice = get_object_or_404(Choice, pk = choice_id) - choice.votes += 1 - choice.save() - return HttpResponseRedirect(reverse('polls.views.poll_results', args=[poll_id])) - - vote_form = VoteForm(poll) + user_choice = get_user_choice(request.user, poll) - return render_to_response('polls/poll_vote.html', { - 'poll': poll, - 'vote_form': vote_form, - }, - context_instance = RequestContext(request)) + if request.method == "POST": + vote_form = VoteForm(poll, request.POST, user=request.user, user_choice=user_choice) + if vote_form.is_valid(): + vote_form.save() + return HttpResponseRedirect(reverse('polls-results', args=[poll_id])) + else: + vote_form = VoteForm(poll) + + return render(request, 'polls/poll_vote.html', { + 'poll': poll, + 'vote_form': vote_form, + 'user_choice': user_choice, + }) ####################################################################### def poll_results(request, poll_id): - poll = get_object_or_404(Poll, pk = poll_id) - total_votes, choices = poll.results() - return render_to_response('polls/poll_results.html', { - 'poll': poll, - 'total_votes': total_votes, - 'choices': choices, - }, - context_instance = RequestContext(request)) + poll = get_object_or_404(Poll, pk=poll_id) + total_votes, choices = poll.results() + return render(request, 'polls/poll_results.html', { + 'poll': poll, + 'total_votes': total_votes, + 'choices': choices, + 'user_choice': get_user_choice(request.user, poll), + }) ####################################################################### + +@require_POST +@login_required +def poll_delete_vote(request): + poll = get_object_or_404(Poll, pk=request.POST.get('poll_id')) + user_choice = get_user_choice(request.user, poll) + if user_choice: + Choice.objects.filter(id=user_choice.id).update(votes=F('votes') - 1) + user_choice.voters.remove(request.user) + + return HttpResponseRedirect(reverse('polls-results', args=[poll.id])) diff -r 524fd1b3919a -r 1f139de929c4 gpp/templates/polls/poll.html --- a/gpp/templates/polls/poll.html Wed May 18 03:04:25 2011 +0000 +++ b/gpp/templates/polls/poll.html Sat May 21 19:55:48 2011 +0000 @@ -10,6 +10,9 @@
  • {{ choice.choice }}
  • {% endfor %} +{% if user_choice %} +

    You voted for "{{ user_choice.choice }}".

    +{% endif %} {% get_comment_count for poll as comment_count %}

    This poll has {{ comment_count }} comment{{ comment_count|pluralize }}. @@ -23,10 +26,10 @@ {% endif %}

    -View Results & Comments +View Results & Comments {% if poll.is_open and user.is_authenticated %} -| Vote +| Vote {% endif %} -| Poll Index +| All Polls

    {% endblock %} diff -r 524fd1b3919a -r 1f139de929c4 gpp/templates/polls/poll_results.html --- a/gpp/templates/polls/poll_results.html Wed May 18 03:04:25 2011 +0000 +++ b/gpp/templates/polls/poll_results.html Sat May 21 19:55:48 2011 +0000 @@ -15,7 +15,7 @@ {% endblock %} {% block content %}

    Polls

    -

    {{ poll.question }}

    +

    Results for: {{ poll.question }}

    {% for choice in choices %}
    {{ choice.choice }} - {{ choice.pct|floatformat }}% ({{ choice.votes }} vote{{ choice.votes|pluralize }})
    @@ -26,12 +26,17 @@ {% endfor %}

    {{ total_votes }} total vote{{ total_votes|pluralize }}.

    + +{% if user_choice %} +

    You voted for "{{ user_choice.choice }}".

    +{% endif %} +

    Poll Details {% if poll.is_open and user.is_authenticated %} -| Vote +| Vote {% endif %} -| Poll Index +| All Polls

    {% get_comment_count for poll as comment_count %} diff -r 524fd1b3919a -r 1f139de929c4 gpp/templates/polls/poll_vote.html --- a/gpp/templates/polls/poll_vote.html Wed May 18 03:04:25 2011 +0000 +++ b/gpp/templates/polls/poll_vote.html Sat May 21 19:55:48 2011 +0000 @@ -6,17 +6,27 @@ {% endblock %} {% block content %}

    Poll

    -

    {{ poll.question }}

    -
    {% csrf_token %} +

    Voting Booth: {{ poll.question }}

    +{% if user_choice %} +

    You voted for "{{ user_choice.choice }}".

    +{% csrf_token %}
    - {{ vote_form.as_p }} - + +
    +{% else %} +
    {% csrf_token %} +
    + {{ vote_form.as_p }} + +
    +
    +{% endif %}

    Poll Details -| View Results -| Poll Index +| View Results +| All Polls

    This poll was published on {{ poll.start_date|date:"F d, Y" }}.

    {% endblock %}