Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions helios/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,22 @@ def can_send_voter_emails(self):

return (True, None)

def can_add_voters_file(self):
"""
Check if voter file uploads are allowed for this election.
Returns a tuple: (can_add: bool, reason: str|None)

Voter uploads are blocked once tallying has started,
as adding voters after vote counting would compromise election integrity.
"""
if self.encrypted_tally:
return (False, "Election has been tallied")

if self.tallying_started_at:
return (False, "Tallying has started")

return (True, None)

def ready_for_tallying(self):
return datetime.datetime.utcnow() >= self.tallying_starts_at

Expand Down
4 changes: 4 additions & 0 deletions helios/templates/voters_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ <h3 class="title">{{election.name}} &mdash; Voters and Ballot Tracking Center <s
<!-- Add a Voter: WORK HERE-->
{% if upload_p and not election.openreg %}
<p>
{% if can_add_voters_file %}
<a class="button" href="{% url "election@voters@upload" election_uuid=election.uuid %}">bulk upload voters</a>
{% else %}
<button class="button" disabled style="opacity: 0.5; cursor: not-allowed;" title="{{upload_disabled_reason}}">bulk upload voters (disabled: {{upload_disabled_reason}})</button>
{% endif %}
</p>

{% if voter_files %}
Expand Down
63 changes: 63 additions & 0 deletions helios/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2821,3 +2821,66 @@ def test_user_search_shows_admin_status(self):
self.assertContains(response, 'Site Admin')
self.assertContains(response, 'No')


class VoterUploadRestrictionTests(WebTest):
"""Tests for voter upload restrictions when election is tallied (issue #455)"""
fixtures = ['users.json']
allow_database_queries = True

def setUp(self):
self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
self.election, _ = models.Election.get_or_create(
short_name='test-upload-restriction',
name='Test Upload Restriction',
description='Test',
admin=self.user
)
if not self.election.uuid:
self.election.uuid = str(uuid.uuid4())
self.election.save()

def setup_login(self):
self.client.get("/")
session = self.client.session
session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
session.save()

def test_can_add_voters_file_allowed_by_default(self):
can_add, reason = self.election.can_add_voters_file()
self.assertTrue(can_add)
self.assertIsNone(reason)

def test_can_add_voters_file_blocked_when_encrypted_tally_exists(self):
# Set in memory only - the method just checks truthiness
self.election.encrypted_tally = True

can_add, reason = self.election.can_add_voters_file()
self.assertFalse(can_add)
self.assertEqual(reason, "Election has been tallied")

def test_can_add_voters_file_blocked_when_tallying_started(self):
self.election.tallying_started_at = datetime.datetime.utcnow()
self.election.save()

can_add, reason = self.election.can_add_voters_file()
self.assertFalse(can_add)
self.assertEqual(reason, "Tallying has started")

def test_voter_upload_view_returns_403_when_blocked(self):
self.setup_login()
self.election.tallying_started_at = datetime.datetime.utcnow()
self.election.save()

response = self.client.get("/helios/elections/%s/voters/upload" % self.election.uuid)
self.assertStatusCode(response, 403)

def test_voters_list_shows_disabled_button_when_blocked(self):
self.setup_login()
self.election.tallying_started_at = datetime.datetime.utcnow()
self.election.save()

response = self.client.get("/helios/elections/%s/voters/list" % self.election.uuid)
self.assertStatusCode(response, 200)
self.assertContains(response, 'disabled')
self.assertContains(response, 'Tallying has started')

15 changes: 11 additions & 4 deletions helios/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1457,14 +1457,20 @@ def voters_list_pretty(request, election):
# Check if voter emails can be sent
can_send_emails, email_disabled_reason = election.can_send_voter_emails()

# Check if voter uploads are allowed
can_add_voters_file, upload_disabled_reason = election.can_add_voters_file()

return render_template(request, 'voters_list',
{'election': election, 'voters_page': voters_page,
'voters': voters_page.object_list, 'admin_p': admin_p,
'email_voters': VOTERS_EMAIL,
'can_send_emails': can_send_emails,
'email_disabled_reason': email_disabled_reason,
'limit': limit, 'total_voters': total_voters,
'upload_p': VOTERS_UPLOAD, 'q' : q,
'upload_p': VOTERS_UPLOAD,
'can_add_voters_file': can_add_voters_file,
'upload_disabled_reason': upload_disabled_reason,
'q' : q,
'voter_files': voter_files,
'categories': categories,
'eligibility_category_id' : eligibility_category_id})
Expand Down Expand Up @@ -1618,9 +1624,10 @@ def voters_upload(request, election):
voter_type, voter_id, optional_additional_params (e.g. email, name)
"""

## TRYING this: allowing voters upload by admin when election is frozen
#if election.frozen_at and not election.openreg:
# raise PermissionDenied()
# don't allow voter upload when election is tallied
can_upload, reason = election.can_add_voters_file()
if not can_upload:
raise PermissionDenied()

if request.method == "GET":
return render_template(request, 'voters_upload', {'election': election, 'error': request.GET.get('e',None)})
Expand Down