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}} — 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}} — 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