Skip to content

Commit 3cbbccc

Browse files
authored
#754 Multiple Uploads (#763)
* Improve UX by replacing buttons with menu popover * Perf improvement only watch for changes if a decryption is in progress * Rename decryption to tally in UI * Exclude duplicate ballots * Show sum of ballots uploaded * Show ballots uploaded and ballot count on decryption page * Fix bug with ballot sum * Some unit tests * Added "Upload More Ballots" button * Fix decryption reference
1 parent ebc7828 commit 3cbbccc

15 files changed

Lines changed: 220 additions & 43 deletions

src/electionguard_gui/components/create_decryption_component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def get_suggested_decryption_name(self, election_id: str) -> dict[str, Any]:
3131
db, election_id
3232
)
3333
return eel_success(
34-
f"{election.election_name} Decryption #{existing_decryptions + 1}"
34+
f"{election.election_name} Tally #{existing_decryptions + 1}"
3535
)
3636

3737
def create_decryption(

src/electionguard_gui/components/upload_ballots_component.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def create_ballot_upload(
2929
election_id: str,
3030
device_file_name: str,
3131
device_file_contents: str,
32-
ballot_count: int,
3332
) -> dict[str, Any]:
3433
try:
3534
db = self._db_service.get_db()
@@ -43,15 +42,13 @@ def create_ballot_upload(
4342
election_id,
4443
device_file_name,
4544
device_file_contents,
46-
ballot_count,
4745
created_at,
4846
)
4947
self._election_service.append_ballot_upload(
5048
db,
5149
election_id,
5250
ballot_upload_id,
5351
device_file_contents,
54-
ballot_count,
5552
created_at,
5653
)
5754
return eel_success(ballot_upload_id)
@@ -69,10 +66,15 @@ def upload_ballot(
6966
try:
7067
db = self._db_service.get_db()
7168
self._log.debug(f"adding ballot {file_name} to {ballot_upload_id}")
72-
self._ballot_upload_service.add_ballot(
69+
success = self._ballot_upload_service.add_ballot(
7370
db, ballot_upload_id, election_id, file_name, file_contents
7471
)
75-
return eel_success()
72+
if success:
73+
self._ballot_upload_service.increment_ballot_count(db, ballot_upload_id)
74+
self._election_service.increment_ballot_upload_ballot_count(
75+
db, election_id, ballot_upload_id
76+
)
77+
return eel_success({"is_duplicate": not success})
7678
# pylint: disable=broad-except
7779
except Exception as e:
7880
return self.handle_error(e)

src/electionguard_gui/models/decryption_dto.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class DecryptionDto:
4141
decryption_id: str
4242
election_id: str
4343
election_name: Optional[str]
44+
ballot_upload_count: int
45+
ballot_count: int
4446
guardians: int
4547
quorum: int
4648
decryption_name: Optional[str]
@@ -63,6 +65,8 @@ def __init__(self, decryption: dict[str, Any]):
6365
self.election_id = str(decryption.get("election_id"))
6466
self.key_ceremony_id = decryption.get("key_ceremony_id")
6567
self.election_name = decryption.get("election_name")
68+
self.ballot_upload_count = _get_int(decryption, "ballot_upload_count", 0)
69+
self.ballot_count = _get_int(decryption, "ballot_count", 0)
6670
self.guardians = _get_int(decryption, "guardians", 0)
6771
self.quorum = _get_int(decryption, "quorum", 0)
6872
self.decryption_name = decryption.get("decryption_name")
@@ -99,6 +103,8 @@ def to_dict(self) -> dict[str, Any]:
99103
"decryption_id": self.decryption_id,
100104
"election_id": self.election_id,
101105
"election_name": self.election_name,
106+
"ballot_upload_count": self.ballot_upload_count,
107+
"ballot_count": self.ballot_count,
102108
"decryption_name": self.decryption_name,
103109
"guardians_joined": self.guardians_joined,
104110
"status": self.get_status(),

src/electionguard_gui/models/election_dto.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ def get_guardian_sequence_order(self, guardian_id: str) -> int:
111111
return record.sequence_order
112112
raise Exception("Guardian not found")
113113

114+
def sum_ballots(self) -> int:
115+
return sum(ballot["ballot_count"] for ballot in self.ballot_uploads)
116+
114117

115118
def _get_list(election: dict[str, Any], name: str) -> list:
116119
value = election.get(name)

src/electionguard_gui/services/ballot_upload_service.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ def create(
2525
election_id: str,
2626
device_file_name: str,
2727
device_file_contents: str,
28-
ballot_count: int,
2928
created_at: datetime,
3029
) -> str:
3130
ballot_upload = {
3231
"election_id": election_id,
3332
"device_file_name": device_file_name,
3433
"device_file_contents": device_file_contents,
35-
"ballot_count": ballot_count,
34+
"ballot_count": 0,
3635
"created_by": self._auth_service.get_user_id(),
3736
"created_at": created_at,
3837
}
@@ -47,16 +46,41 @@ def add_ballot(
4746
election_id: str,
4847
file_name: str,
4948
file_contents: str,
50-
) -> None:
49+
) -> bool:
5150
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
5258
db.ballot_uploads.insert_one(
5359
{
5460
"ballot_upload_id": ballot_upload_id,
5561
"election_id": election_id,
5662
"file_name": file_name,
63+
"object_id": ballot_object_id,
5764
"file_contents": file_contents,
5865
}
5966
)
67+
return True
68+
69+
def increment_ballot_count(self, db: Database, ballot_upload_id: str) -> None:
70+
self._log.trace(f"incrementing ballot count for {ballot_upload_id}")
71+
db.ballot_uploads.update_one(
72+
{"_id": ballot_upload_id, "ballot_count": {"$exists": True}},
73+
{"$inc": {"ballot_count": 1}},
74+
)
75+
76+
def any_ballot_exists(self, db: Database, election_id: str, object_id: str) -> bool:
77+
self._log.trace("checking if ballot exists for {election_id}")
78+
return (
79+
db.ballot_uploads.count_documents(
80+
{"election_id": election_id, "object_id": object_id}
81+
)
82+
> 0
83+
)
6084

6185
def get_ballots(self, db: Database, election_id: str) -> list[SubmittedBallot]:
6286
self._log.debug(f"getting ballots for {election_id}")

src/electionguard_gui/services/decryption_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ def create(
3939
election: ElectionDto,
4040
decryption_name: str,
4141
) -> str:
42+
ballot_count = election.sum_ballots()
43+
ballot_upload_count = len(election.ballot_uploads)
4244
decryption: dict[str, Any] = {
4345
"election_id": election.id,
4446
"election_name": election.election_name,
47+
"ballot_count": ballot_count,
48+
"ballot_upload_count": ballot_upload_count,
4549
"key_ceremony_id": election.key_ceremony_id,
4650
"guardians": election.guardians,
4751
"quorum": election.quorum,

src/electionguard_gui/services/election_service.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ def append_ballot_upload(
8989
election_id: str,
9090
ballot_upload_id: str,
9191
device_file_contents: str,
92-
ballot_count: int,
9392
created_at: datetime,
9493
) -> None:
9594
self._log.trace(
@@ -107,7 +106,7 @@ def append_ballot_upload(
107106
"launch_code": device_file_json["launch_code"],
108107
"location": device_file_json["location"],
109108
"session_id": device_file_json["session_id"],
110-
"ballot_count": ballot_count,
109+
"ballot_count": 0,
111110
"created_at": created_at,
112111
}
113112
}
@@ -124,3 +123,17 @@ def append_decryption(
124123
{"_id": ObjectId(election_id)},
125124
{"$push": {"decryptions": {"decryption_id": decryption_id, "name": name}}},
126125
)
126+
127+
def increment_ballot_upload_ballot_count(
128+
self, db: Database, election_id: str, ballot_upload_id: str
129+
) -> None:
130+
self._log.trace(
131+
f"incrementing ballot upload {ballot_upload_id} ballot count in election {election_id}"
132+
)
133+
db.elections.update_one(
134+
{
135+
"_id": ObjectId(election_id),
136+
"ballot_uploads.ballot_upload_id": ballot_upload_id,
137+
},
138+
{"$inc": {"ballot_uploads.$.ballot_count": 1}},
139+
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export default {
4343
<form id="mainForm" class="needs-validation" novalidate @submit.prevent="createDecryption">
4444
<div class="row g-3 text-center col-6 mx-auto">
4545
<div class="col-12">
46-
<h1>Create Decryption</h1>
46+
<h1>Create Tally</h1>
4747
</div>
4848
<div class="col-12">
49-
<label for="name" class="form-label">Decryption Name</label>
49+
<label for="name" class="form-label">Name</label>
5050
<input type="text" id="name" class="form-control" v-model="name" required>
5151
</div>
5252
<div class="col-12 mt-4">

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

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default {
1414
ballotsProcessed: null,
1515
ballotsTotal: null,
1616
success: false,
17+
duplicateCount: 0,
1718
};
1819
},
1920
methods: {
@@ -27,7 +28,7 @@ export default {
2728
const ballotFiles = document.getElementById("ballotsFolder").files;
2829
this.ballotsTotal = ballotFiles.length;
2930

30-
const uploadId = await this.uploadDeviceFile(this.ballotsTotal);
31+
const uploadId = await this.uploadDeviceFile();
3132
await this.uploadBallotFiles(uploadId, ballotFiles);
3233
this.success = true;
3334
}
@@ -39,15 +40,14 @@ export default {
3940
this.loading = false;
4041
}
4142
},
42-
async uploadDeviceFile(ballotCount) {
43+
async uploadDeviceFile() {
4344
const [deviceFile] = document.getElementById("deviceFile").files;
4445
const deviceContents = await deviceFile.text();
4546
console.log("Creating election", deviceFile.name);
4647
const result = await eel.create_ballot_upload(
4748
this.electionId,
4849
deviceFile.name,
49-
deviceContents,
50-
ballotCount
50+
deviceContents
5151
)();
5252
if (!result.success) {
5353
throw new Error(result.message);
@@ -69,17 +69,44 @@ export default {
6969
if (!result.success) {
7070
throw new Error(result.message);
7171
}
72+
if (result.result.is_duplicate) {
73+
this.duplicateCount++;
74+
}
7275
this.ballotsProcessed++;
7376
}
7477
},
7578
getElectionUrl: function () {
7679
return RouterService.getElectionUrl(this.electionId);
7780
},
81+
uploadMore: function () {
82+
this.success = false;
83+
this.duplicateCount = 0;
84+
this.election = null;
85+
this.loading = false;
86+
this.alert = null;
87+
this.ballotsProcessed = null;
88+
this.ballotsTotal = null;
89+
this.success = false;
90+
this.duplicateCount = 0;
91+
this.$nextTick(() => {
92+
this.resetFiles();
93+
});
94+
},
95+
resetFiles: function () {
96+
document.getElementById("deviceFile").value = null;
97+
document.getElementById("ballotsFolder").value = null;
98+
},
99+
},
100+
mounted() {
101+
this.resetFiles();
78102
},
79103
template: /*html*/ `
80104
<div v-if="alert" class="alert alert-danger" role="alert">
81105
{{ alert }}
82106
</div>
107+
<div v-if="duplicateCount" class="alert alert-warning" role="alert">
108+
{{ duplicateCount }} ballots were skipped because their object_ids had already been uploaded for this election.
109+
</div>
83110
<form id="mainForm" class="needs-validation" novalidate @submit.prevent="uploadBallots" v-if="!success">
84111
<div class="row g-3 align-items-center">
85112
<div class="col-12">
@@ -107,15 +134,17 @@ export default {
107134
<div class="invalid-feedback">Please provide a ballot folder.</div>
108135
</div>
109136
<div class="col-12 mt-4">
110-
<button type="submit" :disabled="loading" class="btn btn-primary">Upload Ballots</button>
137+
<button type="submit" :disabled="loading" class="btn btn-primary me-2">Upload Ballots</button>
138+
<a :href="getElectionUrl()" class="btn btn-secondary me-2">Cancel</a>
111139
<spinner :visible="loading"></spinner>
112140
<p v-if="loading && ballotsProcessed">{{ ballotsProcessed }} of {{ ballotsTotal }} files processed.</p>
113141
</div>
114142
</form>
115143
<div v-if="success" class="text-center">
116144
<img src="/images/check.svg" width="200" height="200" class="mt-4 mb-2"></img>
117-
<p>Successfully uploaded {{ballotsTotal}} ballots.</p>
118-
<a :href="getElectionUrl()" class="btn btn-primary">Continue</a>
145+
<p>Successfully uploaded {{ballotsTotal-duplicateCount}} ballots.</p>
146+
<a :href="getElectionUrl()" class="btn btn-primary me-2">Done Uploading</a>
147+
<button type="button" @click="uploadMore()" class="btn btn-secondary">Upload More Ballots</button>
119148
</div>
120149
`,
121150
};

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export default {
4747
await this.get_decryption(false);
4848
eel.expose(this.refresh_decryption, "refresh_decryption");
4949
console.log("watching decryption");
50-
eel.watch_decryption(this.decryptionId);
50+
// only watch for changes if the decryption is in-progress
51+
if (!this.decryption.completed_at_str) {
52+
eel.watch_decryption(this.decryptionId);
53+
}
5154
},
5255
unmounted() {
5356
console.log("stop watching decryption");
@@ -59,21 +62,45 @@ export default {
5962
</div>
6063
<div v-if="decryption">
6164
<div class="row mb-4">
62-
<div class="col col-11">
65+
<div class="col-11">
6366
<h1>{{decryption.decryption_name}}</h1>
6467
</div>
65-
<div class="col col-xs-2 text-end" v-if="decryption.completed_at_str">
66-
<a :href="getExportElectionRecordUrl()" class="btn btn-sm btn-primary" title="Download election record">
67-
<i class="bi-download"></i>
68-
</a>
68+
<div class="col-1 text-end" v-if="decryption.completed_at_str">
69+
<div class="dropdown">
70+
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
71+
<i class="bi-gear-fill me-1"></i>
72+
</button>
73+
<ul class="dropdown-menu">
74+
<li>
75+
<a :href="getExportElectionRecordUrl()" class="dropdown-item">
76+
<i class="bi-download me-1"></i> Download election record
77+
</a>
78+
</li>
79+
<li>
80+
<a :href="getViewTallyUrl()" class="dropdown-item" v-if="decryption.completed_at_str">
81+
<i class="bi-card-text me-1"></i> View Tally
82+
</a>
83+
</li>
84+
</ul>
85+
</div>
6986
</div>
7087
</div>
7188
<div class="row">
7289
<div class="col col-12 col-md-6 col-lg-5">
73-
<div class="col-12">
90+
<div class="col-md-8">
7491
<dt>Election</dt>
7592
<dd><a :href="getElectionUrl(decryption.election_id)">{{decryption.election_name}}</a></dd>
7693
</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>
102+
</div>
103+
</div>
77104
<dl class="col-12">
78105
<dt>Created</dt>
79106
<dd>by {{decryption.created_by}} on {{decryption.created_at}}</dd>

0 commit comments

Comments
 (0)