changeset 439:1f139de929c4

Fixing #216; added anti-ballot stuffing feature to the polls application.
author Brian Neal <bgneal@gmail.com>
date Sat, 21 May 2011 19:55:48 +0000
parents 524fd1b3919a
children ac9217eef610
files gpp/polls/admin.py gpp/polls/forms.py gpp/polls/models.py gpp/polls/urls.py gpp/polls/views.py gpp/templates/polls/poll.html gpp/templates/polls/poll_results.html gpp/templates/polls/poll_vote.html
diffstat 8 files changed, 193 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- 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'
 
 
--- 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)
--- 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
--- 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<poll_id>\d+)/$', 'poll_detail', name='polls-detail'),
     url(r'^(?P<poll_id>\d+)/results/$', 'poll_results', name='polls-results'),
     url(r'^(?P<poll_id>\d+)/vote/$', 'poll_vote', name='polls-vote'),
+    url(r'^delete_vote/$', 'poll_delete_vote', name='polls-delete_vote'),
 )
--- 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]))
--- 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 @@
    <li>{{ choice.choice }}</li>
 {% endfor %}
 </ul>
+{% if user_choice %}
+<p>You voted for &quot;{{ user_choice.choice }}&quot;.</p>
+{% endif %}
 {% get_comment_count for poll as comment_count %}
 <p>
 This poll has <a href="{% url 'polls.views.poll_results' poll.id %}">{{ comment_count }} comment{{ comment_count|pluralize }}</a>.
@@ -23,10 +26,10 @@
 {% endif %}
 </p>
 <p class="poll-nav">
-<a href="{% url 'polls.views.poll_results' poll.id %}">View Results &amp; Comments</a> 
+<a href="{% url 'polls-results' poll.id %}">View Results &amp; Comments</a> 
 {% if poll.is_open and user.is_authenticated %}
-| <a href="{% url 'polls.views.poll_vote' poll_id=poll.id %}">Vote</a>
+| <a href="{% url 'polls-vote' poll_id=poll.id %}">Vote</a>
 {% endif %}
-| <a href="{% url 'polls.views.poll_index' %}">Poll Index</a>
+| <a href="{% url 'polls-main' %}">All Polls</a>
 </p>
 {% endblock %}
--- 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 %}
 <h2>Polls</h2>
-<h3>{{ poll.question }}</h3>
+<h3>Results for: {{ poll.question }}</h3>
 <dl class="poll-result">
 {% for choice in choices %}
    <dt>{{ choice.choice }} - {{ choice.pct|floatformat }}% ({{ choice.votes }} vote{{ choice.votes|pluralize }})</dt>
@@ -26,12 +26,17 @@
 {% endfor %}
 </dl>
 <p><strong>{{ total_votes }} total vote{{ total_votes|pluralize }}.</strong></p>
+
+{% if user_choice %}
+<p>You voted for &quot;{{ user_choice.choice }}&quot;.</p>
+{% endif %}
+
 <p class="poll-nav">
 <a href="{{ poll.get_absolute_url }}">Poll Details</a>
 {% if poll.is_open and user.is_authenticated %}
-| <a href="{% url 'polls.views.poll_vote' poll_id=poll.id %}">Vote</a> 
+| <a href="{% url 'polls-vote' poll_id=poll.id %}">Vote</a> 
 {% endif %}
-| <a href="{% url 'polls.views.poll_index' %}">Poll Index</a>
+| <a href="{% url 'polls-main' %}">All Polls</a>
 </p>
 
 {% get_comment_count for poll as comment_count %}
--- 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 %}
 <h2>Poll</h2>
-<h3>{{ poll.question }}</h3>
-<form action="." method="post">{% csrf_token %}
+<h3>Voting Booth: {{ poll.question }}</h3>
+{% if user_choice %}
+<p>You voted for &quot;{{ user_choice.choice }}&quot;.</p>
+<form action="{% url 'polls-delete_vote' %}" method="post">{% csrf_token %}
    <div class="poll-form">
-   {{ vote_form.as_p }}
-   <input type="submit" value="Vote" />
+      <input type="hidden" name="poll_id" value="{{ poll.id }}" />
+      <input type="submit" value="Delete My Vote!" />
    </div>
 </form>
+{% else %}
+   <form action="." method="post">{% csrf_token %}
+      <div class="poll-form">
+      {{ vote_form.as_p }}
+      <input type="submit" value="Vote" />
+      </div>
+   </form>
+{% endif %}
 <p class="poll-nav">
 <a href="{{ poll.get_absolute_url }}">Poll Details</a>
-| <a href="{% url 'polls.views.poll_results' poll.id %}">View Results</a>
-| <a href="{% url 'polls.views.poll_index' %}">Poll Index</a>
+| <a href="{% url 'polls-results' poll.id %}">View Results</a>
+| <a href="{% url 'polls-main' %}">All Polls</a>
 </p>
 <p>This poll was published on {{ poll.start_date|date:"F d, Y" }}.</p>
 {% endblock %}