From 872147d0ea326ea5e48a46f6cefbc88824a548be Mon Sep 17 00:00:00 2001
From: Ben Adida <ben@adida.net>
Date: Mon, 30 May 2011 16:16:06 -0700
Subject: [PATCH] a bunch of clarifying tweaks, made private elections work for
 real

---
 helios/forms.py                       |  7 +++---
 helios/models.py                      | 20 +++++++++++++----
 helios/templates/election_freeze.html | 14 ++++++------
 helios/templates/election_view.html   | 20 +++++++++--------
 helios/templates/list_trustees.html   | 14 +++++++++---
 helios/templates/voters_list.html     | 12 +++++++---
 helios/tests.py                       | 11 +++++++++
 helios/views.py                       | 32 ++++++++++++++++-----------
 server_ui/templates/base.html         |  7 +++++-
 9 files changed, 94 insertions(+), 43 deletions(-)

diff --git a/helios/forms.py b/helios/forms.py
index 967525d..4fcf514 100644
--- a/helios/forms.py
+++ b/helios/forms.py
@@ -10,11 +10,12 @@ from fields import *
 class ElectionForm(forms.Form):
   short_name = forms.SlugField(max_length=25, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010')
   name = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'size':60}), help_text='the pretty name for your election, e.g. My Club 2010 Election')
-  description = forms.CharField(max_length=2000, widget=forms.Textarea(attrs={'cols': 70, 'wrap': 'soft'}))
+  description = forms.CharField(max_length=2000, widget=forms.Textarea(attrs={'cols': 70, 'wrap': 'soft'}), required=False)
   election_type = forms.ChoiceField(label="type", choices = Election.ELECTION_TYPES)
-  use_voter_aliases = forms.BooleanField(required=False, initial=False, help_text='if selected, voter identities will be replaced with aliases, e.g. "V12", in the ballot tracking center')
+  use_voter_aliases = forms.BooleanField(required=False, initial=False, help_text='If selected, voter identities will be replaced with aliases, e.g. "V12", in the ballot tracking center')
   #use_advanced_audit_features = forms.BooleanField(required=False, initial=True, help_text='disable this only if you want a simple election with reduced security but a simpler user interface')
-  private_p = forms.BooleanField(required=False, initial=False, label="Private?", help_text='a private election is only visible to registered/eligible voters', widget=forms.HiddenInput)
+  #private_p = forms.BooleanField(required=False, initial=False, label="Private?", help_text='a private election is only visible to registered/eligible voters', widget=forms.HiddenInput)
+  private_p = forms.BooleanField(required=False, initial=False, label="Private?", help_text='A private election is only visible to registered voters.')
   
 
 class ElectionTimesForm(forms.Form):
diff --git a/helios/models.py b/helios/models.py
index 2ddc440..7e8f166 100644
--- a/helios/models.py
+++ b/helios/models.py
@@ -262,18 +262,30 @@ class Election(HeliosModel):
   def issues_before_freeze(self):
     issues = []
     if self.questions == None or len(self.questions) == 0:
-      issues.append("no questions")
+      issues.append(
+        {'type': 'questions',
+         'action': "add questions to the ballot"}
+        )
   
     trustees = Trustee.get_by_election(self)
     if len(trustees) == 0:
-      issues.append("no trustees")
+      issues.append({
+          'type': 'trustees',
+          'action': "add at least one trustee"
+          })
 
     for t in trustees:
       if t.public_key == None:
-        issues.append("trustee %s hasn't generated a key yet" % t.name)
+        issues.append({
+            'type': 'trustee keypairs',
+            'action': 'have trustee %s generate a keypair' % t.name
+            })
 
     if self.voter_set.count() == 0 and not self.openreg:
-      issues.append("no voters and closed registration")
+      issues.append({
+          "type" : "voters",
+          "action" : 'enter your voter list (or open registration to the public)'
+          })
 
     return issues    
 
diff --git a/helios/templates/election_freeze.html b/helios/templates/election_freeze.html
index 9b770c8..1fe2bf6 100644
--- a/helios/templates/election_freeze.html
+++ b/helios/templates/election_freeze.html
@@ -3,15 +3,15 @@
 {% block content %}
   <h2 class="title">{{election.name}} &mdash; Freeze Ballot</h2>
 <p>
-Once the ballot is frozen, the questions and available choices can no longer be modified.<br />
+Once the ballot is frozen, the questions and options can no longer be modified.<br />
 The list of trustees and their public keys will also be frozen.
 </p>
 
 <p>
 {% if election.openreg %}
-Your election currently has <b>open registration</b>. After you freeze the ballot, you will be able to continue to manage the voter list while the election runs. You will <em>not</em> be able to switch back to a closed-registration setting.
+Registration for your election is currently <b>open</b>, which means anyone can vote, even after you freeze the ballot.
 {% else %}
-Your election currently has <b>closed registration</b>.<br />After you freeze the ballot, you also will <em>not</em> be able to modify the voter list, nor will you be able to re-open registration.
+Registration for your election is currently <b>closed</b>, which means only the voters you designate will be able to cast a ballot. As the administrator, you will still be able to modify that voter list as the election progresses.
 {% endif %}
 </p>    
 
@@ -23,10 +23,10 @@ You must freeze the ballot before you can contact voters.
 
 {% if issues_p %}
 <p>
-    There are <b>problems</b> that prevent you from freezing the election:
+    Before you can freeze the election, you will need to:
     <ul>
         {% for issue in issues %}
-        <li>{{issue}}</li>
+        <li>{{issue.action}}</li>
         {% endfor %}
     </ul>
     <a href="{% url helios.views.one_election_view election.uuid %}">go back to the election</a>
@@ -35,10 +35,10 @@ You must freeze the ballot before you can contact voters.
 <form method="post" action="">
 <input type="hidden" name="csrf_token" value="{{csrf_token}}" />
     
-<input class="pretty" type="submit" value="freeze!" />
+<input class="pretty" type="submit" value="Freeze the ballot" />
 <button onclick="document.location='./view'; return false;">never mind</button>
 </form>
 {% endif %}
 
 <br /><br />
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/helios/templates/election_view.html b/helios/templates/election_view.html
index 6b8587b..f0d856e 100644
--- a/helios/templates/election_view.html
+++ b/helios/templates/election_view.html
@@ -33,6 +33,7 @@ if (!navigator.javaEnabled()) {
 {% endif %}
 <br />
 {% if admin_p %}
+{% if not election.private_p %}
 {% if election.featured_p %}
 this {{election.election_type}} is featured on the front page.
 {% if can_feature_p %}
@@ -45,6 +46,7 @@ this {{election.election_type}} is <u>not</u> featured on the front page.
 {% endif %}
 {% endif %}
 {% endif %}
+{% endif %}
 </p>
 
 </div>
@@ -72,7 +74,7 @@ this {{election.election_type}} is <u>not</u> featured on the front page.
 </p>
 
 {% if admin_p %}
-{% if not election.private_p %}
+{% if election.frozen_p %}
 <div style="background: lightyellow; padding:5px; padding-left: 10px; margin-top: 15px; border: 1px solid #aaa; width: 720px;" class="round">
 <a href="#" onclick="$('#badgebody').slideToggle(250);">Embed an Election Badge</a>
 <div id="badgebody" style="display:none;">
@@ -85,7 +87,6 @@ this {{election.election_type}} is <u>not</u> featured on the front page.
 </div>
 </div>
 {% endif %}
-
 <p>
 
 {% if election.result %}
@@ -97,7 +98,9 @@ this {{election.election_type}} is <u>not</u> featured on the front page.
 <span style="font-size: 1.3em;">
 {% if not election.frozen_at %}
 {% if election.issues_before_freeze %}
-add questions, voters, and trustees.
+{% for issue in election.issues_before_freeze %}
+{{issue.action}}{% if forloop.last %}{% else %}, and{% endif %}<br />
+{% endfor %}
 {% else %}
 <a href="{% url helios.views.one_election_freeze election.uuid %}">freeze ballot and open election.</a>
 <br />
@@ -182,13 +185,8 @@ all voters will be notified that the tally is ready.
 <span class="highlight-box round" style="font-size: 1.6em; margin-right: 10px;" id="votelink">
 <a href="{{test_cookie_url}}">Vote in this {{election.election_type}} </a>
 </span><br />
-{% if not user %}
 <br />
-<br /><br />
-For your privacy, you'll be asked to log in only once your ballot is encrypted.
-{% endif %}
 {% if election.voting_extended_until %}
-<br />
 This {{election.election_type}} was initially scheduled to end at {{election.voting_ends_at}} (UTC),<br />
 but has been extended until {{ election.voting_extended_until }} (UTC).
 {% else %}
@@ -196,12 +194,16 @@ but has been extended until {{ election.voting_extended_until }} (UTC).
 <br />
 This {{election.election_type}} is scheduled to end at {{election.voting_ends_at}} (UTC).
 {% else %}
-<br />
 This {{election.election_type}} ends at the administrator's discretion.
 {% endif %}
 <br />
 {% endif %}
 
+{% if election.private_p and voter %}
+<br />
+This election is <em>private</em>. You are signed in as eligible voter <em>{{voter.name}}</em>.
+{% endif %}
+
 <div class="highlight-box round" style="font-size: 1.2em; margin-right: 400px; display:none;" id="nojava_message">
   You do not have Java installed in your browser.<br />At this time, Helios requires Java.<br />
   Visit <a target="_new" href="http://java.sun.com">java.sun.com</a> to install it.
diff --git a/helios/templates/list_trustees.html b/helios/templates/list_trustees.html
index 94d4158..aa7ace9 100644
--- a/helios/templates/list_trustees.html
+++ b/helios/templates/list_trustees.html
@@ -6,13 +6,21 @@
   <h2 class="title">{{election.name}} &mdash; Trustees <span style="font-size:0.7em;">[<a href="{% url helios.views.one_election_view election.uuid %}">back to election</a>]</span></h2>
 
 <p>
-    Trustees are responsible for decrypting the election result.
+    Trustees are responsible for decrypting the election result.<br />
+    Each trustee generates a keypair and submits the public portion to Helios.<br />
+    When it's time to decrypt, each trustee needs to provide his secret key.
 </p>
 
-
 {% if not election.frozen_at %}
+
+<p>
+  Helios is automatically your first trustee and will handle its keypair generation and decryption automatically.<br />
+  You may add additional trustees if you want, and you can even remove the Helios trustee.<br />
+  However, we recommend you do this only if you have a solid understanding of the trustee's role.
+</p>
+
 <p>
-    <a href="{% url helios.views.new_trustee election.uuid %}">new trustee</a>
+    [ <a onclick="return(confirm('Are you sure you want to add a trustee?\n\nThis is an advanced feature that requires a good bit more owrk to tally the election.\nThe simplest option is to let Helios tally the election for you.'));" href="{% url helios.views.new_trustee election.uuid %}">add a trustee</a> ]
 </p>
 {% if not election.has_helios_trustee %}
 <p>
diff --git a/helios/templates/voters_list.html b/helios/templates/voters_list.html
index e8f4072..588fb04 100644
--- a/helios/templates/voters_list.html
+++ b/helios/templates/voters_list.html
@@ -10,16 +10,21 @@
 {% if election.openreg %}
 [<a href="{% url helios.views.one_election_set_reg election.uuid %}?open_p=0">switch to closed</a>]
 {% else %}
+{% if election.private_p %}
+<br />
+Your election is marked private: registration can be opened to the public only for public elections.
+{% else %}
 [<a href="{% url helios.views.one_election_set_reg election.uuid %}?open_p=1">switch to open</a>]
 {% endif %}
 {% endif %}
+{% endif %}
 </p>
 
 {% if email_voters and election.frozen_at and admin_p %}
 <p><a href="{% url helios.views.voters_email election.uuid %}">email voters</a></p>
 {% endif %}
 
-
+{% if election.num_voters > 20 %}
 <p>
 {% if q %}
 <p><em>searching for <u>{{q}}</u>.</em> [<a href="?">clear search</a>]</p>
@@ -27,9 +32,10 @@
 <form method="get" action="{% url helios.views.voters_list_pretty election.uuid %}"><b>search</b>: <input type="text" name="q" /> <input type="submit" value="search" /></form>
 {% endif %}
 </p>
-<br />
+{% endif %}
+
 {% if admin_p %}
-Add a Voter: WORK HERE
+<!-- Add a Voter: WORK HERE-->
 {% if upload_p %}
 <p>
 <a href="{% url helios.views.voters_upload election_uuid=election.uuid %}">bulk upload voters</a>
diff --git a/helios/tests.py b/helios/tests.py
index b017290..fd973d7 100644
--- a/helios/tests.py
+++ b/helios/tests.py
@@ -553,6 +553,17 @@ class ElectionBlackboxTests(TestCase):
         url = re.search('http://[^/]+(/[^ \n]*)', email_message.body).group(1)
 
         # check that we can get at that URL
+        if not need_login:
+            # confusing piece: if need_login is True, that means it was a public election
+            # that required login before casting a ballot.
+            # so if need_login is False, it was a private election, and we do need to re-login here
+            # we need to re-login if it's a private election, because all data, including ballots
+            # is otherwise private
+            response = self.client.post("/helios/elections/%s/password_voter_login" % election_id, {
+                    'voter_id' : username,
+                    'password' : password
+                    })
+            
         response = self.client.get(url)
         self.assertContains(response, ballot.hash)
         self.assertContains(response, html_escape(encrypted_vote))
diff --git a/helios/views.py b/helios/views.py
index 01d8257..47921fb 100644
--- a/helios/views.py
+++ b/helios/views.py
@@ -166,14 +166,17 @@ def election_vote_shortcut(request, election_short_name):
   else:
     raise Http404
 
+@election_view()
+def _castvote_shortcut_by_election(request, election, cast_vote):
+  return render_template(request, 'castvote', {'cast_vote' : cast_vote, 'vote_content': cast_vote.vote.toJSON(), 'voter': cast_vote.voter, 'election': election})
+  
 def castvote_shortcut(request, vote_tinyhash):
   try:
     cast_vote = CastVote.objects.get(vote_tinyhash = vote_tinyhash)
   except CastVote.DoesNotExist:
     raise Http404
 
-  # FIXME: consider privacy of election
-  return render_template(request, 'castvote', {'cast_vote' : cast_vote, 'vote_content': cast_vote.vote.toJSON(), 'voter': cast_vote.voter, 'election': cast_vote.voter.election})
+  return _castvote_shortcut_by_election(request, election_uuid = cast_vote.voter.election.uuid, cast_vote=cast_vote)
 
 @trustee_check
 def trustee_keygenerator(request, election, trustee):
@@ -247,8 +250,8 @@ def election_new(request):
 def one_election_edit(request, election):
 
   error = None
-  RELEVANT_FIELDS = ['short_name', 'name', 'description', 'use_voter_aliases', 'election_type']
-  # RELEVANT_FIELDS += ['use_advanced_audit_features', 'private_p']
+  RELEVANT_FIELDS = ['short_name', 'name', 'description', 'use_voter_aliases', 'election_type', 'private_p']
+  # RELEVANT_FIELDS += ['use_advanced_audit_features']
   
   if request.method == "GET":
     values = {}
@@ -308,15 +311,16 @@ def one_election_view(request, election):
   if user:
     voter = Voter.get_by_election_and_user(election, user)
     
-    if voter:
-      # cast any votes?
-      votes = CastVote.get_by_voter(voter)
-    else:
+    if not voter:
       eligible_p = _check_eligibility(election, user)
-      votes = None
       notregistered = True
   else:
-    voter = None
+    voter = get_voter(request, user, election)
+
+  if voter:
+    # cast any votes?
+    votes = CastVote.get_by_voter(voter)
+  else:
     votes = None
 
   # status update message?
@@ -829,9 +833,11 @@ def one_election_set_reg(request, election):
   """
   Set whether this is open registration or not
   """
-  open_p = bool(int(request.GET['open_p']))
-  election.openreg = open_p
-  election.save()
+  # only allow this for public elections
+  if not election.private_p:
+    open_p = bool(int(request.GET['open_p']))
+    election.openreg = open_p
+    election.save()
   
   return HttpResponseRedirect(reverse(voters_list_pretty, args=[election.uuid]))
 
diff --git a/server_ui/templates/base.html b/server_ui/templates/base.html
index 000a36e..99f4eb5 100644
--- a/server_ui/templates/base.html
+++ b/server_ui/templates/base.html
@@ -49,7 +49,12 @@
 logged in as {{user.display_html_small|safe}}
 [<a href="{% url auth.views.logout %}?return_url={{CURRENT_URL}}">logout</a>]<br />
 {% else %}
-not logged in. [<a href="{{settings.SECURE_URL_HOST}}{% url auth.views.index %}?return_url={{CURRENT_URL}}">log in</a>]<br />
+{% if voter %}
+You are signed in as voter <u>{{voter.name}}</u> in election <u>{{voter.election.name}}</u>. [<a href="{{settings.SECURE_URL_HOST}}{% url auth.views.index %}?return_url={{CURRENT_URL}}">sign out and back in</a>]
+{% else %}
+not logged in. [<a href="{{settings.SECURE_URL_HOST}}{% url auth.views.index %}?return_url={{CURRENT_URL}}">log in</a>]
+{% endif %}
+<br />
 {% endif %}
 <a href="http://heliosvoting.org">About Helios</a>
 {% for footer_link in settings.FOOTER_LINKS %}
-- 
GitLab