summaryrefslogtreecommitdiff
path: root/postgresqleu
diff options
context:
space:
mode:
authorMagnus Hagander2010-04-13 20:13:07 +0000
committerMagnus Hagander2010-04-13 20:13:07 +0000
commit08f8c44685d84e2bef2e7a263dcc767da701e334 (patch)
tree1a9be3ec710dbd7fa90818095962780e2d7231a8 /postgresqleu
parent9456315de8c169c9448d05010b7d68d0954b7fa6 (diff)
Initial version of election management software.
Not yet linked in anywhere, so we can test it properly first.
Diffstat (limited to 'postgresqleu')
-rw-r--r--postgresqleu/elections/__init__.py0
-rw-r--r--postgresqleu/elections/admin.py33
-rw-r--r--postgresqleu/elections/forms.py94
-rw-r--r--postgresqleu/elections/models.py28
-rw-r--r--postgresqleu/elections/tests.py23
-rw-r--r--postgresqleu/elections/views.py129
-rw-r--r--postgresqleu/settings.py1
-rw-r--r--postgresqleu/urls.py7
8 files changed, 315 insertions, 0 deletions
diff --git a/postgresqleu/elections/__init__.py b/postgresqleu/elections/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/postgresqleu/elections/__init__.py
diff --git a/postgresqleu/elections/admin.py b/postgresqleu/elections/admin.py
new file mode 100644
index 00000000..4deb6260
--- /dev/null
+++ b/postgresqleu/elections/admin.py
@@ -0,0 +1,33 @@
+from django.contrib import admin
+from django import forms
+from django.forms import ValidationError, ModelForm
+
+from datetime import datetime
+
+from models import *
+
+class VoteAdminForm(ModelForm):
+ class Meta:
+ model = Vote
+
+ def clean(self):
+ raise ValidationError("You really shouldn't edit votes! If you *really* need to fix something broken, do it in the db")
+
+class VoteAdmin(admin.ModelAdmin):
+ list_display = ('election', 'voter', 'candidate', 'score')
+ ordering = ['election', ]
+ form = VoteAdminForm
+
+
+class ElectionAdmin(admin.ModelAdmin):
+ list_display = ['name', 'startdate', 'enddate', ]
+ ordering = ['-startdate', ]
+
+class CandidateAdmin(admin.ModelAdmin):
+ list_display = ['name', 'election', ]
+ list_filter = ['election', ]
+ ordering = ['name', ]
+
+admin.site.register(Election, ElectionAdmin)
+admin.site.register(Candidate, CandidateAdmin)
+admin.site.register(Vote, VoteAdmin)
diff --git a/postgresqleu/elections/forms.py b/postgresqleu/elections/forms.py
new file mode 100644
index 00000000..0235d5cb
--- /dev/null
+++ b/postgresqleu/elections/forms.py
@@ -0,0 +1,94 @@
+from django import forms
+from django.forms.util import ErrorList
+from django.db import transaction, connection
+
+from models import Vote
+from postgresqleu.membership.models import MemberLog
+
+from datetime import datetime
+
+class VoteForm(forms.Form):
+ def __init__(self, election, member, *args, **kwargs):
+ super(VoteForm, self).__init__(*args, **kwargs)
+
+ self.election = election
+ self.member = member
+
+ self.candidates = election.candidate_set.all().order_by('name')
+ self.votes = Vote.objects.filter(election=election, voter=member)
+
+ votemap = {}
+ for vote in self.votes:
+ votemap[vote.candidate_id] = vote.score
+
+ dropdown = [(x,x) for x in range(1,len(self.candidates)+1)]
+ dropdown.insert(0, (-1, '** Please rate this candidate'))
+
+ # Dynamically add a dropdown field for each candidate
+ for candidate in self.candidates:
+ self.fields['cand%i' % candidate.id] = forms.ChoiceField(choices=dropdown,
+ label=candidate.name,
+ required=True,
+ help_text=candidate.id,
+ initial=votemap.has_key(candidate.id) and votemap[candidate.id] or -1)
+
+ def clean(self):
+ # First, make sure all existing fields are actually filled out
+ for (k,v) in self.cleaned_data.items():
+ if k.startswith('cand'):
+ if v == "-1":
+ self._errors[k] = ErrorList(["You need to select a score for this candidate!"])
+ else:
+ raise Exception("Invalid field name found: %s" % k)
+
+ # Second, make sure the fields match the candidates
+ fields = self.cleaned_data.copy()
+ for candidate in self.candidates:
+ if fields.has_key("cand%i" % candidate.id):
+ del fields["cand%i" % candidate.id]
+ else:
+ raise Exception("Data for candidate %i is missing" % candidate.id)
+
+ if len(fields) > 0:
+ raise Exception("Data for candidate not standing for election found!")
+
+ # Finally, verify that all options have been found, and none have been duplicated
+ options = range(1, len(self.candidates)+1)
+ for k,v in self.cleaned_data.items():
+ if int(v) in options:
+ # First use is ok. Take it out of the list, so next attempt generates error
+ del options[options.index(int(v))]
+ else:
+ # Not in the list means it was already used! Bad user!
+ self._errors[k] = ErrorList(["This score has already been given to another candidate"])
+
+ if len(options) != 0:
+ raise forms.ValidationError("One or more scores was not properly assigned!")
+
+ return self.cleaned_data
+
+ @transaction.commit_on_success
+ def save(self):
+ # Let's see if the old votes are here
+ if len(self.votes) == 0:
+ # This is completely new, let's create votes for him
+ for k,v in self.cleaned_data.items():
+ id = int(k[4:])
+ Vote(election=self.election, voter=self.member, candidate_id=id, score=v).save()
+ MemberLog(member=self.member, timestamp=datetime.now(),
+ message="Voted in election '%s'" % self.election.name).save()
+ elif len(self.votes) == len(self.candidates):
+ # Ok, we have one vote for each candidate already, so modify them as necessary
+ changedany = False
+ for vote in self.votes:
+ score = int(self.cleaned_data['cand%i' % vote.candidate_id])
+ if vote.score != score:
+ vote.score = score
+ vote.save()
+ changedany = True
+
+ if changedany:
+ MemberLog(member=self.member, timestamp=datetime.now(),
+ message="Changed votes in election '%s'" % self.election.name).save()
+ else:
+ raise Exception("Invalid number of records found in database, unable to update vote.")
diff --git a/postgresqleu/elections/models.py b/postgresqleu/elections/models.py
new file mode 100644
index 00000000..20a8b8a3
--- /dev/null
+++ b/postgresqleu/elections/models.py
@@ -0,0 +1,28 @@
+from django.db import models
+from postgresqleu.membership.models import Member
+
+class Election(models.Model):
+ name = models.CharField(max_length=100, null=False, blank=False)
+ startdate = models.DateField(null=False, blank=False)
+ enddate = models.DateField(null=False, blank=False)
+ slots = models.IntegerField(null=False, default=1)
+ isopen = models.BooleanField(null=False, default=False)
+ resultspublic = models.BooleanField(null=False, default=False)
+
+ def __unicode__(self):
+ return self.name
+
+class Candidate(models.Model):
+ election = models.ForeignKey(Election, null=False, blank=False)
+ name = models.CharField(max_length=100, null=False, blank=False)
+ email = models.EmailField(max_length=200, null=False, blank=False)
+ presentation = models.TextField(null=False, blank=False)
+
+ def __unicode__(self):
+ return "%s (%s)" % (self.name, self.election)
+
+class Vote(models.Model):
+ election = models.ForeignKey(Election, null=False, blank=False)
+ voter = models.ForeignKey(Member, null=False, blank=False)
+ candidate = models.ForeignKey(Candidate, null=False, blank=False)
+ score = models.IntegerField(null=False, blank=False)
diff --git a/postgresqleu/elections/tests.py b/postgresqleu/elections/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/postgresqleu/elections/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/postgresqleu/elections/views.py b/postgresqleu/elections/views.py
new file mode 100644
index 00000000..066c605a
--- /dev/null
+++ b/postgresqleu/elections/views.py
@@ -0,0 +1,129 @@
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponseRedirect, Http404
+from django.template import RequestContext
+from django.contrib.auth.decorators import login_required, user_passes_test
+from django.conf import settings
+from django.db import connection
+
+from models import *
+from forms import VoteForm
+from datetime import date, timedelta
+
+def home(request):
+ elections = Election.objects.filter(isopen=True).order_by('startdate')
+ open_elections = [e for e in elections if e.startdate<=date.today() and e.enddate>=date.today()]
+ past_elections = [e for e in elections if e.startdate<date.today() and e.enddate<date.today()]
+ upcoming_elections = [e for e in elections if e.startdate>date.today()]
+
+ return render_to_response('elections/home.html', {
+ 'open': open_elections,
+ 'past': past_elections,
+ 'upcoming': upcoming_elections,
+ }, context_instance=RequestContext(request))
+
+def election(request, electionid):
+ if settings.FORCE_SECURE_FORMS and not request.is_secure():
+ return HttpResponseRedirect(request.build_absolute_uri().replace('http://','https://',1))
+
+ election = get_object_or_404(Election, pk=electionid)
+ if not election.isopen:
+ raise Http404("This election is not open (yet)")
+
+ if election.startdate > date.today():
+ raise Http404("This election has not started yet")
+
+ if election.enddate < date.today():
+ # Election is closed, consider publishing the results
+ if not election.resultspublic:
+ # If user is an admin, show anyway, otherwise throw an error
+ if not request.user.is_superuser:
+ raise Http404("The results for this election isn't published yet.")
+
+ # Ok, so we do have the results. Use a custom query to make sure we get decently formatted data
+ # and no client-side ORM aggregation
+ curs = connection.cursor()
+ curs.execute("SELECT c.name, sum(v.score) AS score FROM elections_candidate c INNER JOIN elections_vote v ON c.id=v.candidate_id WHERE v.election_id=%(election)s AND c.election_id=%(election)s GROUP BY c.name ORDER BY 2 DESC", {
+ 'election': election.pk,
+ })
+ res = curs.fetchall()
+ if len(res) == 0:
+ raise Http404('No results found for this election')
+
+ return render_to_response('elections/results.html', {
+ 'election': election,
+ 'topscore': res[0][1],
+ 'scores': [{'name': r[0], 'score': r[1], 'width': 300*r[1]/res[0][1]} for r in res],
+ }, context_instance=RequestContext(request))
+
+ if len(election.candidate_set.all()) <= 0:
+ raise Http404("This election has no candidates!")
+
+ # Otherwise, we show up the form. This part requires login
+ if not request.user.is_authenticated():
+ return HttpResponseRedirect("/login/?next=%s" % request.path)
+
+ try:
+ member = Member.objects.get(user=request.user)
+
+ # Make sure that the membership hasn't expired
+ if member.paiduntil < date.today():
+ return render_to_response('elections/mustbemember.html', {},
+ context_instance=RequestContext(request))
+
+ # Verify that the membership is new enough
+ # Since memberships are valid one year, the date of signup is considered to be
+ # member.paiduntil - '1 year'.
+
+ if member.paiduntil - timedelta(days=365) > election.startdate - timedelta(days=28):
+ return render_to_response('elections/memberfourweeks.html', {
+ 'registered_at': member.paiduntil - timedelta(days=365),
+ 'mustregbefore': election.startdate - timedelta(days=28),
+ 'election': election,
+ }, context_instance=RequestContext(request))
+
+ except Member.DoesNotExist:
+ return render_to_response('elections/mustbemember.html', {},
+ context_instance=RequestContext(request))
+
+ if request.method == "POST":
+ form = VoteForm(election, member, data=request.POST)
+ if form.is_valid():
+ # Save the form
+ form.save()
+ else:
+ # Not a POST, so generate an empty form
+ form = VoteForm(election, member)
+
+
+ return render_to_response('elections/form.html', {
+ 'form': form,
+ 'election': election,
+ }, context_instance=RequestContext(request))
+
+def candidate(request, election, candidate):
+ candidate = get_object_or_404(Candidate, election=election, pk=candidate)
+
+ return render_to_response('elections/candidate.html', {
+ 'candidate': candidate,
+ }, context_instance=RequestContext(request))
+
+@login_required
+def ownvotes(request, electionid):
+ if settings.FORCE_SECURE_FORMS and not request.is_secure():
+ return HttpResponseRedirect(request.build_absolute_uri().replace('http://','https://',1))
+
+ election = get_object_or_404(Election, pk=electionid)
+ if not election.isopen:
+ raise Http404("This election is not open (yet)")
+
+ if election.enddate >= date.today():
+ raise Http404("This election has not ended yet")
+
+ member = get_object_or_404(Member, user=request.user)
+
+ votes = Vote.objects.select_related().filter(voter=member, election=election).order_by('-score')
+
+ return render_to_response('elections/ownvotes.html', {
+ 'election': election,
+ 'votes': votes,
+ }, context_instance=RequestContext(request))
diff --git a/postgresqleu/settings.py b/postgresqleu/settings.py
index ed12bc54..0abe2851 100644
--- a/postgresqleu/settings.py
+++ b/postgresqleu/settings.py
@@ -83,6 +83,7 @@ INSTALLED_APPS = (
'postgresqleu.newsevents',
'postgresqleu.confreg',
'postgresqleu.membership',
+ 'postgresqleu.elections',
)
AUTH_CONNECTION_STRING="override authentication connection string in the local settings file"
diff --git a/postgresqleu/urls.py b/postgresqleu/urls.py
index 24221410..5c8220cb 100644
--- a/postgresqleu/urls.py
+++ b/postgresqleu/urls.py
@@ -9,6 +9,7 @@ import postgresqleu.newsevents.views
import postgresqleu.views
import postgresqleu.confreg.views
import postgresqleu.membership.views
+import postgresqleu.elections.views
from postgresqleu.newsevents.feeds import LatestNews, LatestEvents
@@ -56,6 +57,12 @@ urlpatterns = patterns('',
(r'^membership/$', postgresqleu.membership.views.home),
(r'^community/members/$', postgresqleu.membership.views.userlist),
+ # Elections
+ (r'^elections/$', postgresqleu.elections.views.home),
+ (r'^elections/(\d+)/$', postgresqleu.elections.views.election),
+ (r'^elections/(\d+)/candidate/(\d+)/$', postgresqleu.elections.views.candidate),
+ (r'^elections/(\d+)/ownvotes/$', postgresqleu.elections.views.ownvotes),
+
# This should not happen in production - serve by apache!
url(r'^media/(.*)$', 'django.views.static.serve', {
'document_root': '../media',