Skip to content

Commit da4fd4b

Browse files
authored
#766 Ballot Enhancements (#774)
* validate the manifest_hash on ballot upload * Better error display on decryption * Ballot uploading UI enhancements * UI Enhancements to Election Page Move buttons on menu to appropriate sections where possible Display ballots and tallies even when there isn't information to display Move the title down and separate the sections more clearly * Update decryption page to match style of election page Button color -> secondary Move title lower on page Decryption Results -> Tally Results Show a nice message if no spoiled ballots existed * Add tally date to tallies list on election page * Include ballot upload date in UI * fix linting issues
1 parent 510bc87 commit da4fd4b

7 files changed

Lines changed: 170 additions & 92 deletions

File tree

src/electionguard_gui/components/upload_ballots_component.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import Any
22
from datetime import datetime
33
import eel
4+
from electionguard.serialize import from_raw
5+
from electionguard.ballot import SubmittedBallot
46
from electionguard_gui.components.component_base import ComponentBase
57
from electionguard_gui.eel_utils import eel_fail, eel_success
68
from electionguard_gui.services import ElectionService, BallotUploadService
@@ -66,15 +68,41 @@ def upload_ballot(
6668
try:
6769
db = self._db_service.get_db()
6870
self._log.trace(f"adding ballot {file_name} to {ballot_upload_id}")
71+
ballot = from_raw(SubmittedBallot, file_contents)
72+
election = self._election_service.get(db, election_id)
73+
context = election.get_context()
74+
if context.manifest_hash != ballot.manifest_hash:
75+
self._log.warn(
76+
f"ballot '{ballot.object_id}' had a mismatched manifest hash. "
77+
+ f"Expected {context.manifest_hash}, got {ballot.manifest_hash}."
78+
)
79+
return eel_fail(
80+
"The uploaded ballot didn't match the encryption package for this election. "
81+
+ "Please try a different ballot."
82+
)
83+
is_duplicate = self._ballot_upload_service.any_ballot_exists(
84+
db, election_id, ballot.object_id
85+
)
86+
if is_duplicate:
87+
self._log.warn(
88+
"ballot '{ballot.object_id}' already exists in election '{election_id}'"
89+
)
90+
return eel_success({"is_duplicate": True})
91+
6992
success = self._ballot_upload_service.add_ballot(
70-
db, ballot_upload_id, election_id, file_name, file_contents
93+
db,
94+
ballot_upload_id,
95+
election_id,
96+
file_name,
97+
file_contents,
98+
ballot.object_id,
7199
)
72100
if success:
73101
self._ballot_upload_service.increment_ballot_count(db, ballot_upload_id)
74102
self._election_service.increment_ballot_upload_ballot_count(
75103
db, election_id, ballot_upload_id
76104
)
77-
return eel_success({"is_duplicate": not success})
105+
return eel_success({"is_duplicate": False})
78106
# pylint: disable=broad-except
79107
except Exception as e:
80108
return self.handle_error(e)

src/electionguard_gui/models/election_dto.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,21 @@ def to_dict(self) -> dict[str, Any]:
7373
"contests": self._get_manifest_field("contests"),
7474
"ballot_styles": self._get_manifest_field("ballot_styles"),
7575
},
76-
"ballot_uploads": self.ballot_uploads,
77-
"decryptions": self.decryptions,
76+
"ballot_uploads": [
77+
{
78+
"location": ballot_upload["location"],
79+
"ballot_count": ballot_upload["ballot_count"],
80+
"created_at": utc_to_str(ballot_upload.get("created_at")),
81+
}
82+
for ballot_upload in self.ballot_uploads
83+
],
84+
"decryptions": [
85+
{
86+
"name": decryption["name"],
87+
"created_at": utc_to_str(decryption.get("created_at")),
88+
}
89+
for decryption in self.decryptions
90+
],
7891
"created_by": self.created_by,
7992
"created_at": self.created_at_str,
8093
}

src/electionguard_gui/services/ballot_upload_service.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,9 @@ def add_ballot(
4646
election_id: str,
4747
file_name: str,
4848
file_contents: str,
49+
ballot_object_id: str,
4950
) -> bool:
5051
self._log.trace(f"adding ballot {file_name} to {ballot_upload_id}")
51-
ballot = from_raw(SubmittedBallot, file_contents)
52-
ballot_object_id = ballot.object_id
53-
if self.any_ballot_exists(db, election_id, ballot_object_id):
54-
self._log.warn(
55-
"ballot '{ballot_object_id}' already exists in election '{election_id}'"
56-
)
57-
return False
5852
db.ballot_uploads.insert_one(
5953
{
6054
"ballot_upload_id": ballot_upload_id,

src/electionguard_gui/services/election_service.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,15 @@ def append_decryption(
121121
)
122122
db.elections.update_one(
123123
{"_id": ObjectId(election_id)},
124-
{"$push": {"decryptions": {"decryption_id": decryption_id, "name": name}}},
124+
{
125+
"$push": {
126+
"decryptions": {
127+
"decryption_id": decryption_id,
128+
"name": name,
129+
"created_at": datetime.utcnow(),
130+
}
131+
}
132+
},
125133
)
126134

127135
def increment_ballot_upload_ballot_count(

src/electionguard_gui/web/components/admin/upload-ballots-component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default {
134134
<div class="invalid-feedback">Please provide a ballot folder.</div>
135135
</div>
136136
<div class="col-12 mt-4">
137-
<button type="submit" :disabled="loading" class="btn btn-primary me-2">Upload Ballots</button>
137+
<button type="submit" :disabled="loading" class="btn btn-primary me-2">Upload</button>
138138
<a :href="getElectionUrl()" class="btn btn-secondary me-2">Cancel</a>
139139
<spinner :visible="loading"></spinner>
140140
<p v-if="loading && ballotsProcessed">{{ ballotsProcessed }} of {{ ballotsTotal }} files processed.</p>

src/electionguard_gui/web/components/admin/view-decryption-admin-component.js

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,37 @@ export default {
2929
spoiledBallotId: spoiledBallotId,
3030
});
3131
},
32-
refresh_decryption: async function () {
33-
await this.get_decryption(true);
32+
refresh_decryption: async function (result) {
33+
if (result.success) {
34+
await this.get_decryption(true);
35+
} else {
36+
console.error(result.message);
37+
this.error = true;
38+
}
3439
},
3540
get_decryption: async function (is_refresh) {
3641
console.log("getting decryption");
3742
this.loading = true;
38-
const result = await eel.get_decryption(this.decryptionId, is_refresh)();
39-
this.error = !result.success;
40-
if (result.success) {
41-
this.decryption = result.result;
43+
try {
44+
const result = await eel.get_decryption(
45+
this.decryptionId,
46+
is_refresh
47+
)();
48+
this.error = !result.success;
49+
if (result.success) {
50+
this.decryption = result.result;
51+
}
52+
} finally {
53+
this.loading = false;
4254
}
43-
this.loading = false;
4455
},
4556
},
4657
async mounted() {
47-
await this.get_decryption(false);
4858
eel.expose(this.refresh_decryption, "refresh_decryption");
59+
await this.get_decryption(false);
4960
console.log("watching decryption");
5061
// only watch for changes if the decryption is in-progress
51-
if (!this.decryption.completed_at_str) {
62+
if (this.decryption && !this.decryption.completed_at_str) {
5263
eel.watch_decryption(this.decryptionId);
5364
}
5465
},
@@ -61,13 +72,10 @@ export default {
6172
<p class="alert alert-danger" role="alert">An error occurred. Check the logs and try again.</p>
6273
</div>
6374
<div v-if="decryption">
64-
<div class="row mb-4">
65-
<div class="col-11">
66-
<h1>{{decryption.decryption_name}}</h1>
67-
</div>
68-
<div class="col-1 text-end" v-if="decryption.completed_at_str">
75+
<div class="text-end">
76+
<div v-if="decryption.completed_at_str">
6977
<div class="dropdown">
70-
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
78+
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
7179
<i class="bi-gear-fill me-1"></i>
7280
</button>
7381
<ul class="dropdown-menu">
@@ -85,31 +93,36 @@ export default {
8593
</div>
8694
</div>
8795
</div>
96+
<div class="row">
97+
<h1>{{decryption.decryption_name}}</h1>
98+
</div>
8899
<div class="row">
89100
<div class="col col-12 col-md-6 col-lg-5">
90101
<div class="col-md-8">
91102
<dt>Election</dt>
92103
<dd><a :href="getElectionUrl(decryption.election_id)">{{decryption.election_name}}</a></dd>
93104
</div>
94-
<div class="row">
95-
<div class="col-md-6">
96-
<dt>Ballot Uploads</dt>
97-
<dd>{{decryption.ballot_upload_count}}</dd>
98-
</div>
99-
<div class="col-md-6">
100-
<dt>Total Ballots</dt>
101-
<dd>{{decryption.ballot_count}}</dd>
105+
<div class="mb-4">
106+
<div class="row">
107+
<div class="col-md-6">
108+
<dt>Ballot Uploads</dt>
109+
<dd>{{decryption.ballot_upload_count}}</dd>
110+
</div>
111+
<div class="col-md-6">
112+
<dt>Total Ballots</dt>
113+
<dd>{{decryption.ballot_count}}</dd>
114+
</div>
102115
</div>
116+
<dl class="col-12">
117+
<dt>Created</dt>
118+
<dd>by {{decryption.created_by}} on {{decryption.created_at}}</dd>
119+
</dl>
120+
<dl class="col-12" v-if="decryption.completed_at_str">
121+
<dt>Completed</dt>
122+
<dd>{{decryption.completed_at_str}}</dd>
123+
</dl>
103124
</div>
104-
<dl class="col-12">
105-
<dt>Created</dt>
106-
<dd>by {{decryption.created_by}} on {{decryption.created_at}}</dd>
107-
</dl>
108-
<dl class="col-12" v-if="decryption.completed_at_str">
109-
<dt>Completed</dt>
110-
<dd>{{decryption.completed_at_str}}</dd>
111-
</dl>
112-
<div class="col-12">
125+
<div class="col-12 mb-4">
113126
<h3>Joined Guardians</h3>
114127
<ul v-if="decryption.guardians_joined.length">
115128
<li v-for="guardian in decryption.guardians_joined">{{guardian}}</li>
@@ -118,11 +131,27 @@ export default {
118131
<p>No guardians have joined yet</p>
119132
</div>
120133
</div>
134+
<div v-if="decryption.completed_at_str" class="mb-4">
135+
<h3>Tally Results</h3>
136+
<a :href="getViewTallyUrl()" class="btn btn-sm btn-secondary m-2"><i class="bi bi-binoculars-fill me-1"></i> View Tally</a>
137+
</div>
121138
<div v-if="decryption.completed_at_str">
122-
<h3>Decryption Results</h3>
123-
<a :href="getViewTallyUrl()" class="btn btn-sm btn-primary m-2">View Tally</a>
124-
<h4>Spoiled Ballots</h4>
125-
<a :href="getSpoiledBallotUrl(spoiled_ballot)" class="btn btn-sm btn-primary m-2" v-for="spoiled_ballot in decryption.spoiled_ballots">{{spoiled_ballot}}</a>
139+
<h3>Spoiled Ballots</h3>
140+
<table class="table table-striped" v-if="decryption.spoiled_ballots.length">
141+
<thead>
142+
<tr>
143+
<th>Ballot ID</th>
144+
</tr>
145+
</thead>
146+
<tbody class="table-group-divider">
147+
<tr v-for="spoiledBallot in decryption.spoiled_ballots">
148+
<td><a :href="getSpoiledBallotUrl(spoiledBallot)">{{spoiledBallot}}</a></td>
149+
</tr>
150+
</tbody>
151+
</table>
152+
<div v-else>
153+
<p>No spoiled ballots existed at the time this tally was run.</p>
154+
</div>
126155
</div>
127156
</div>
128157
<div class="col col-12 col-md-6 col-lg-7 text-center">
@@ -132,5 +161,8 @@ export default {
132161
</div>
133162
</div>
134163
</div>
164+
<div v-else>
165+
<spinner :visible="loading"></spinner>
166+
</div>
135167
`,
136168
};

0 commit comments

Comments
 (0)