Skip to content

Commit de9d579

Browse files
Feature/post pr 17 comments (#20)
* DELETE/REBASE this out before PR - smushing dev envs * initial thoughts/debugging on integrating with backend * the requirements.txt file - ??? * Revert "DELETE/REBASE this out before PR - smushing dev envs" This reverts commit 821db9f. * after backing out env hack, redoing poetry install * separating running poetry export from pylint * first 3 endpoints have some initial functionality * initial stub at some json data models * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update Makefile Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update Makefile Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * add missing PHONY target * changing ContestDict to AnyContest * checkpointing: post PR and 2023/04/11 meeting * checkpoint during refactoring * initial pass on some thoughts regarding all 5 endpoints * checkpointing endpoint #4 * initial pass on endpoint #5 * wordsmithing * copying blank ballot, cast ballot, ballot check to here * refactoring backend.py - cleaning it up somewhat * another checkpoint at the tailend of the initial refactoring * adding mock verify-ballot, tally-election, and show-contest json docs * show-contest is now returning a dictionary * since the endpoint can support multiple digests, is seems easier to return (from the backend ops) a list than a dict * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/json_data_models.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * PR - removing /resuse/ endpoint * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * PR - cleaning up testing comments * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * tweaking a comment re: verbosity when tallying * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/mock-data/blank-ballot.json Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/mock-data/blank-ballot.json Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/mock-data/cast-ballot.json Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/mock-data/cast-ballot.json Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update src/vtp/web/api/mock-data/cast-ballot.json Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * Update tests/test_main.py Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com> * PR - tweaking class documentation * more PR work - clean up error testing idiom --------- Co-authored-by: Ion Y <78449689+ion-oset@users.noreply.github.com>
1 parent b27828f commit de9d579

16 files changed

Lines changed: 1613 additions & 403 deletions

Makefile

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Ancient Makefile implicit rule disabler
2+
(%): %
3+
%:: %,v
4+
%:: RCS/%,v
5+
%:: s.%
6+
%:: SCCS/s.%
7+
%.out: %
8+
%.c: %.w %.ch
9+
%.tex: %.w %.ch
10+
%.mk:
11+
12+
# Variables
13+
DOC_DIR := docs
14+
SRC_DIR := src/vtp/web/api
15+
TEST_DIR := tests
16+
17+
# Use colors for errors and warnings when in an interactive terminal
18+
INTERACTIVE := $(shell test -t 0 && echo 1)
19+
ifdef INTERACTIVE
20+
RED := \033[0;31m
21+
END := \033[0m
22+
else
23+
RED :=
24+
END :=
25+
endif
26+
27+
# Let there be no default target
28+
.PHONY: default
29+
default:
30+
@echo "${RED}There is no default make target.${END} Specify one of:"
31+
@echo "pylint - runs pylint"
32+
@echo "pytest - runs pytest"
33+
@echo "reqs - generates a new requirements.txt file"
34+
@echo "etags - constructs an emacs tags table"
35+
@echo ""
36+
@echo "See ${BUILD_DIR}/README.md for more details and info"
37+
38+
# Run pylint
39+
.PHONY: pylint
40+
pylint:
41+
@echo "${RED}NOTE - isort and black disagree on 3 files${END} - let black win"
42+
isort ${SRC_DIR} ${TEST_DIR}
43+
black ${SRC_DIR} ${TEST_DIR}
44+
pylint --recursive y ${SRC_DIR} ${TEST_DIR}
45+
46+
# Run tests
47+
.PHONY: pytest
48+
pytest:
49+
pytest ${TEST_DIR}
50+
51+
# emacs tags
52+
ETAG_SRCS := $(shell find * -type f -name '*.py' -o -name '*.md' | grep -v defunct)
53+
.PHONY: etags
54+
etags: ${ETAG_SRCS}
55+
etags ${ETAG_SRCS}
56+
57+
# Generate a requirements.txt for dependabot (ignoring the symlinks)
58+
.PHONY: reqs
59+
reqs requirements.txt: pyproject.toml poetry.lock
60+
poetry export --with dev -f requirements.txt --output requirements.txt

poetry.lock

Lines changed: 244 additions & 233 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

Lines changed: 363 additions & 0 deletions
Large diffs are not rendered by default.

src/vtp/web/api/backend.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""
2+
Backend support for the web-api. One important aspect of this file is
3+
to support web-api testing in the absense of a live ElectionData
4+
deployment. An ElectionData deployment is when a
5+
setup-vtp-demo-operation operation has been run, nominally creating a
6+
/opt/VoteTrackerPlus/demo.01 ElectionData folder and the required
7+
subfolders.
8+
9+
With an ElectionData deployment the VTP git commands can be executed
10+
and VoteTracker+ can function as designed.
11+
12+
Without an ElectionData deployment VTP git commands cannot be
13+
executed. Currently this state is configured by the _MOCK_MODE
14+
variable below. When set, and when this repo is part of the
15+
VTP-dev-env parent repo (or when the VoteTrackerPlus and
16+
VTP-mock-election.US.xx repos are simply sibling repos of this one),
17+
the commands here do not call into the VoteTrackerPlus repo and
18+
instead stub out the effective IO operations with static, non-varying
19+
mock data. That mock data is currently nominally stored in (checked
20+
into) the VTP-mock-election.US.xx repo as the mock data is a direct
21+
function of the live ElectionData election configuration and CVR data
22+
found in that repo. And that is due to two things: 1) the
23+
VTP-mock-election.US.xx holds the configuration of an election such as
24+
the blank ballot definition and 2) it also holds several hundred
25+
pre-cast random ballots so to fill the ballot cache so that
26+
ballot-checks can be immediately produced upon casting a ballot.
27+
28+
Regardless, for the time being the VTP-mock-election.US.xx also holds
29+
checkedin mock values for the data that the web-api and above layers
30+
need when running in mock mode.
31+
"""
32+
33+
import csv
34+
import json
35+
36+
from vtp.core.common import Common
37+
from vtp.ops.accept_ballot_operation import AcceptBallotOperation
38+
from vtp.ops.cast_ballot_operation import CastBallotOperation
39+
from vtp.ops.setup_vtp_demo_operation import SetupVtpDemoOperation
40+
from vtp.ops.show_contests_operation import ShowContestsOperation
41+
from vtp.ops.tally_contests_operation import TallyContestsOperation
42+
from vtp.ops.verify_ballot_receipt_operation import VerifyBallotReceiptOperation
43+
44+
45+
class VtpBackend:
46+
"""
47+
Class to keep the namespace separate and allow the creation of a
48+
shim layer in the VTP-web-api repo so that this repo can easily
49+
talk with the VoteTrackerPlus backend repo.
50+
"""
51+
52+
########
53+
# backend demo constants
54+
########
55+
# set mock mode
56+
_MOCK_MODE = False
57+
# where the blank ballot is stored for the spring demo
58+
_MOCK_BLANK_BALLOT = "mock-data/blank-ballot.json"
59+
# where the cast-ballot.json file is stored for the spring demo
60+
_MOCK_CAST_BALLOT = "mock-data/cast-ballot.json"
61+
# where the ballot-check is stored for the spring demo
62+
_MOCK_BALLOT_CHECK = "mock-data/receipt.26.csv"
63+
_MOCK_VOTER_INDEX = 26
64+
# a mock contest content
65+
_MOCK_CONTEST_CONTENT = "mock-data/mock_contest.json"
66+
# default guid - making one up
67+
_MOCK_GUID = "01d963fd74100ee3f36428740a8efd8afd781839"
68+
# default mock receipt log
69+
_MOCK_VERIFY_BALLOT_LOG = "mock-data/verify-ballot-doc.json"
70+
# default mock tally log
71+
_MOCK_TALLY_CONTESTS_LOG = "mock-data/tally-election-doc.json"
72+
# default mock show contest log
73+
_MOCK_SHOW_CONTEST_LOG = "mock-data/show-contest-doc.json"
74+
# backend default address
75+
_ADDRESS = "123, Main Street, Concord, Massachusetts"
76+
77+
@staticmethod
78+
def get_vote_store_id() -> str:
79+
"""
80+
Endpoint #1: will return a vote_store_id, a.k.a. a guid
81+
"""
82+
if VtpBackend._MOCK_MODE:
83+
# in mock mode there is no guid - make one up
84+
return VtpBackend._MOCK_GUID
85+
operation = SetupVtpDemoOperation(
86+
election_data_dir=Common.get_generic_ro_edf_dir(),
87+
)
88+
return operation.run(guid_client_store=True)
89+
90+
@staticmethod
91+
def get_empty_ballot(vote_store_id: str) -> dict:
92+
"""
93+
Endpoint #2: given an existing guid, will return the blank
94+
ballot
95+
"""
96+
if VtpBackend._MOCK_MODE:
97+
# in mock mode there is no guid - make one up
98+
with open(VtpBackend._MOCK_BLANK_BALLOT, "r", encoding="utf8") as infile:
99+
json_doc = json.load(infile)
100+
# import pdb; pdb.set_trace()
101+
return json_doc
102+
# Cet a (the) blank ballot from the backend
103+
operation = CastBallotOperation(
104+
election_data_dir=Common.get_guid_based_edf_dir(vote_store_id),
105+
)
106+
return operation.run(
107+
an_address = VtpBackend._ADDRESS,
108+
return_bb=True,
109+
)
110+
111+
@staticmethod
112+
def get_all_guid_workspaces() -> list:
113+
"""
114+
Will return a list of all the existing guid workspaces
115+
"""
116+
return SetupVtpDemoOperation.get_all_guid_workspaces()
117+
118+
@staticmethod
119+
def mock_get_cast_ballot() -> dict:
120+
"""Mock only - return a static cast ballot"""
121+
with open(VtpBackend._MOCK_CAST_BALLOT, "r", encoding="utf8") as infile:
122+
json_doc = json.load(infile)
123+
return json_doc
124+
125+
@staticmethod
126+
def mock_get_ballot_check() -> tuple[list, int]:
127+
"""Mock only - return a static cast ballot"""
128+
with open(VtpBackend._MOCK_BALLOT_CHECK, "r", encoding="utf8") as infile:
129+
csv_doc = list(csv.reader(infile))
130+
return csv_doc, VtpBackend._MOCK_VOTER_INDEX
131+
132+
@staticmethod
133+
def cast_ballot(vote_store_id: str, cast_ballot: dict) -> dict:
134+
"""
135+
Endpoint #3: will cast (upload) a cast ballot and return the
136+
ballot-check and voter-index
137+
"""
138+
if VtpBackend._MOCK_MODE:
139+
# Just return a mock ballot-check and voter-index
140+
return VtpBackend.mock_get_ballot_check()
141+
# handle the incoming ballot and return the ballot-check and voter-index
142+
operation = AcceptBallotOperation(
143+
election_data_dir=Common.get_guid_based_edf_dir(vote_store_id),
144+
)
145+
return operation.run(
146+
cast_ballot_json=cast_ballot,
147+
merge_contests=True,
148+
)
149+
150+
@staticmethod
151+
def verify_ballot_check(
152+
vote_store_id: str,
153+
ballot_check: list,
154+
vote_index: int,
155+
cvr: bool = False,
156+
) -> str:
157+
"""
158+
Endpoint #4: will verify a ballot-check and vote-inded, returning an
159+
undefined string at this time.
160+
"""
161+
if VtpBackend._MOCK_MODE:
162+
# Just return a mock verify ballot string
163+
with open(VtpBackend._MOCK_VERIFY_BALLOT_LOG, "r", encoding="utf8") as infile:
164+
json_doc = json.load(infile)
165+
return json_doc
166+
# handle the incoming ballot and return the ballot-check and voter-index
167+
operation = VerifyBallotReceiptOperation(
168+
election_data_dir=Common.get_guid_based_edf_dir(vote_store_id),
169+
stdout_printing=False,
170+
)
171+
return operation.run(
172+
receipt_data=ballot_check,
173+
row=str(vote_index),
174+
cvr=cvr,
175+
)
176+
177+
@staticmethod
178+
def tally_election_check(
179+
vote_store_id: str,
180+
contests: str,
181+
digests: str,
182+
) -> str:
183+
"""
184+
Endpoint #5: will tally an election and print stuff
185+
"""
186+
if VtpBackend._MOCK_MODE:
187+
# Just return a mock tally string
188+
with open(VtpBackend._MOCK_TALLY_CONTESTS_LOG, "r", encoding="utf8") as infile:
189+
json_doc = json.load(infile)
190+
return json_doc
191+
# handle the incoming ballot and return the ballot-check and voter-index
192+
operation = TallyContestsOperation(
193+
election_data_dir=Common.get_guid_based_edf_dir(vote_store_id),
194+
stdout_printing=False,
195+
)
196+
if digests == "None":
197+
digests = ""
198+
if contests == "None":
199+
contests = ""
200+
return operation.run(
201+
contest_uid=contests,
202+
track_contests=digests,
203+
)
204+
205+
@staticmethod
206+
def show_contest(
207+
vote_store_id: str,
208+
contests: str,
209+
) -> dict:
210+
"""
211+
Endpoint #6: display the contents of one or more contests
212+
"""
213+
if VtpBackend._MOCK_MODE:
214+
# Just return a mock contest
215+
with open(VtpBackend._MOCK_SHOW_CONTEST_LOG, "r", encoding="utf8") as infile:
216+
json_doc = json.load(infile)
217+
return json_doc
218+
# handle the show_contest
219+
operation = ShowContestsOperation(
220+
election_data_dir=Common.get_guid_based_edf_dir(vote_store_id),
221+
stdout_printing=False,
222+
)
223+
return {"contents": operation.run(contest_check=contests)}

0 commit comments

Comments
 (0)