Skip to content

Commit c79b021

Browse files
authored
#764 Stop Removing Duplicate Contests in Report (#767)
* Add test reproducing error with duplicate section names * Stop removing duplicate contests from report * Test to ensure duplicates aren't removed in selections * fix linting issues
1 parent 3cbbccc commit c79b021

3 files changed

Lines changed: 189 additions & 11 deletions

File tree

src/electionguard_gui/services/plaintext_ballot_service.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,39 @@
77

88
def get_plaintext_ballot_report(
99
election: ElectionDto, plaintext_ballot: PlaintextTally
10-
) -> dict[str, Any]:
10+
) -> list:
1111
manifest = election.get_manifest()
1212
selection_names = manifest.get_selection_names("en")
1313
contest_names = manifest.get_contest_names()
1414
selection_write_ins = _get_candidate_write_ins(manifest)
1515
parties = _get_selection_parties(manifest)
16-
tally_report = {}
16+
tally_report = _get_tally_report(
17+
plaintext_ballot, selection_names, contest_names, selection_write_ins, parties
18+
)
19+
return tally_report
20+
21+
22+
def _get_tally_report(
23+
plaintext_ballot: PlaintextTally,
24+
selection_names: dict[str, str],
25+
contest_names: dict[str, str],
26+
selection_write_ins: dict[str, bool],
27+
parties: dict[str, str],
28+
) -> list:
29+
tally_report = []
1730
contests = plaintext_ballot.contests.values()
1831
for tally_contest in contests:
1932
selections = list(tally_contest.selections.values())
2033
contest_details = _get_contest_details(
2134
selections, selection_names, selection_write_ins, parties
2235
)
2336
contest_name = contest_names.get(tally_contest.object_id, "n/a")
24-
tally_report[contest_name] = contest_details
37+
tally_report.append(
38+
{
39+
"name": contest_name,
40+
"details": contest_details,
41+
}
42+
)
2543
return tally_report
2644

2745

src/electionguard_gui/web/components/shared/view-plaintext-ballot-component.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ export default {
33
ballot: Object,
44
},
55
template: /*html*/ `
6-
<div v-for="(contestContents, contestName) in ballot" class="mb-5">
7-
<h2>{{contestName}}</h2>
6+
<div v-for="contest in ballot" class="mb-5">
7+
<h2>{{contest.name}}</h2>
88
<table class="table table-striped">
99
<thead>
1010
<tr>
@@ -15,7 +15,7 @@ export default {
1515
</tr>
1616
</thead>
1717
<tbody>
18-
<tr v-for="contestInfo in contestContents.selections">
18+
<tr v-for="contestInfo in contest.details.selections">
1919
<td>{{contestInfo.name}}</td>
2020
<td>{{contestInfo.party}}</td>
2121
<td class="text-end">{{contestInfo.tally}}</td>
@@ -24,13 +24,13 @@ export default {
2424
<tr class="table-secondary">
2525
<td></td>
2626
<td></td>
27-
<td class="text-end"><strong>{{contestContents.nonWriteInTotal}}</strong></td>
27+
<td class="text-end"><strong>{{contest.details.nonWriteInTotal}}</strong></td>
2828
<td class="text-end"><strong>100.00%</strong></td>
2929
</tr>
30-
<tr v-if="contestContents.writeInTotal !== null">
30+
<tr v-if="contest.details.writeInTotal !== null">
3131
<td></td>
3232
<td class="text-end">Write-Ins</td>
33-
<td class="text-end">{{contestContents.writeInTotal}}</td>
33+
<td class="text-end">{{contest.details.writeInTotal}}</td>
3434
<td class="text-end"></td>
3535
</tr>
3636
</tbody>

tests/unit/electionguard_gui/test_plaintext_ballot_service.py

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,121 @@
11
from unittest.mock import MagicMock, patch
2-
from electionguard.tally import PlaintextTallySelection
3-
from electionguard_gui.services.plaintext_ballot_service import _get_contest_details
2+
from electionguard.tally import PlaintextTally, PlaintextTallySelection
3+
from electionguard_gui.services.plaintext_ballot_service import (
4+
_get_contest_details,
5+
_get_tally_report,
6+
)
47
from tests.base_test_case import BaseTestCase
58

69

710
class TestPlaintextBallotService(BaseTestCase):
811
"""Test the ElectionDto class"""
912

13+
def test_get_tally_report_with_no_contests(self) -> None:
14+
# ARRANGE
15+
plaintext_ballot = PlaintextTally("tally", {})
16+
selection_names: dict[str, str] = {}
17+
selection_write_ins: dict[str, bool] = {}
18+
parties: dict[str, str] = {}
19+
contest_names: dict[str, str] = {}
20+
21+
# ACT
22+
result = _get_tally_report(
23+
plaintext_ballot,
24+
selection_names,
25+
contest_names,
26+
selection_write_ins,
27+
parties,
28+
)
29+
30+
# ASSERT
31+
self.assertEqual(0, len(result))
32+
33+
@patch("electionguard.tally.PlaintextTallySelection")
34+
def test_given_one_contest_with_valid_name_when_get_tally_report_then_name_returned(
35+
self, plaintext_tally_selection: MagicMock
36+
) -> None:
37+
# ARRANGE
38+
plaintext_tally_selection.object_id = "c-1"
39+
plaintext_ballot = PlaintextTally("tally", {"c-1": plaintext_tally_selection})
40+
selection_names: dict[str, str] = {}
41+
selection_write_ins: dict[str, bool] = {}
42+
parties: dict[str, str] = {}
43+
contest_names: dict[str, str] = {"c-1": "Contest 1"}
44+
45+
# ACT
46+
result = _get_tally_report(
47+
plaintext_ballot,
48+
selection_names,
49+
contest_names,
50+
selection_write_ins,
51+
parties,
52+
)
53+
54+
# ASSERT
55+
self.assertEqual(1, len(result))
56+
self.assertEqual("Contest 1", result[0]["name"])
57+
58+
@patch("electionguard.tally.PlaintextTallySelection")
59+
def test_given_one_contest_with_invalid_name_when_get_tally_report_then_name_is_na(
60+
self, plaintext_tally_selection: MagicMock
61+
) -> None:
62+
# ARRANGE
63+
plaintext_tally_selection.object_id = "c-1"
64+
plaintext_ballot = PlaintextTally("tally", {"c-1": plaintext_tally_selection})
65+
selection_names: dict[str, str] = {}
66+
selection_write_ins: dict[str, bool] = {}
67+
parties: dict[str, str] = {}
68+
contest_names: dict[str, str] = {}
69+
70+
# ACT
71+
result = _get_tally_report(
72+
plaintext_ballot,
73+
selection_names,
74+
contest_names,
75+
selection_write_ins,
76+
parties,
77+
)
78+
79+
# ASSERT
80+
self.assertEqual(1, len(result))
81+
self.assertEqual("n/a", list(result)[0]["name"])
82+
83+
@patch("electionguard.tally.PlaintextTallySelection")
84+
@patch("electionguard.tally.PlaintextTallySelection")
85+
def test_given_two_contests_with_duplicate_names_when_get_tally_report_then_both_names_returned(
86+
self,
87+
plaintext_tally_selection1: MagicMock,
88+
plaintext_tally_selection2: MagicMock,
89+
) -> None:
90+
# ARRANGE
91+
plaintext_tally_selection1.object_id = "c-1"
92+
plaintext_tally_selection2.object_id = "c-2"
93+
plaintext_ballot = PlaintextTally(
94+
"tally",
95+
{
96+
"c-1": plaintext_tally_selection1,
97+
"c-2": plaintext_tally_selection2,
98+
},
99+
)
100+
selection_names: dict[str, str] = {}
101+
selection_write_ins: dict[str, bool] = {}
102+
parties: dict[str, str] = {}
103+
contest_names: dict[str, str] = {"c-1": "My Contest", "c-2": "My Contest"}
104+
105+
# ACT
106+
result = _get_tally_report(
107+
plaintext_ballot,
108+
selection_names,
109+
contest_names,
110+
selection_write_ins,
111+
parties,
112+
)
113+
114+
# ASSERT
115+
self.assertEqual(2, len(result))
116+
self.assertEqual("My Contest", list(result)[0]["name"])
117+
self.assertEqual("My Contest", list(result)[1]["name"])
118+
10119
def test_zero_sections(self) -> None:
11120
# ARRANGE
12121
selections: list[PlaintextTallySelection] = []
@@ -55,6 +164,57 @@ def test_one_non_write_in(self, plaintext_tally_selection: MagicMock) -> None:
55164
self.assertEqual("National Union Party", selection["party"])
56165
self.assertEqual(1, selection["percent"])
57166

167+
@patch("electionguard.tally.PlaintextTallySelection")
168+
@patch("electionguard.tally.PlaintextTallySelection")
169+
def test_duplicate_section_names(
170+
self,
171+
plaintext_tally_selection1: MagicMock,
172+
plaintext_tally_selection2: MagicMock,
173+
) -> None:
174+
# ARRANGE
175+
plaintext_tally_selection1.object_id = "S1"
176+
plaintext_tally_selection1.tally = 1
177+
plaintext_tally_selection2.object_id = "S2"
178+
plaintext_tally_selection2.tally = 9
179+
selections: list[PlaintextTallySelection] = [
180+
plaintext_tally_selection1,
181+
plaintext_tally_selection2,
182+
]
183+
selection_names: dict[str, str] = {
184+
"S1": "Abraham Lincoln",
185+
"S2": "Abraham Lincoln",
186+
}
187+
selection_write_ins: dict[str, bool] = {
188+
"S1": False,
189+
"S2": False,
190+
}
191+
parties: dict[str, str] = {
192+
"S1": "National Union Party",
193+
"S2": "National Union Party",
194+
}
195+
196+
# ACT
197+
result = _get_contest_details(
198+
selections, selection_names, selection_write_ins, parties
199+
)
200+
201+
# ASSERT
202+
self.assertEqual(10, result["nonWriteInTotal"])
203+
self.assertEqual(None, result["writeInTotal"])
204+
self.assertEqual(2, len(result["selections"]))
205+
206+
selection = result["selections"][0]
207+
self.assertEqual("Abraham Lincoln", selection["name"])
208+
self.assertEqual(1, selection["tally"])
209+
self.assertEqual("National Union Party", selection["party"])
210+
self.assertEqual(0.1, selection["percent"])
211+
212+
selection = result["selections"][1]
213+
self.assertEqual("Abraham Lincoln", selection["name"])
214+
self.assertEqual(9, selection["tally"])
215+
self.assertEqual("National Union Party", selection["party"])
216+
self.assertEqual(0.9, selection["percent"])
217+
58218
@patch("electionguard.tally.PlaintextTallySelection")
59219
def test_one_write_in(self, plaintext_tally_selection: MagicMock) -> None:
60220
# ARRANGE

0 commit comments

Comments
 (0)