Mercurial > public > sg101
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 "{{ user_choice.choice }}".</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 & Comments</a> +<a href="{% url 'polls-results' poll.id %}">View Results & 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 "{{ user_choice.choice }}".</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 "{{ user_choice.choice }}".</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 %}