Skip to content

Commit 975b443

Browse files
committed
✨ Implement Cast and Spoil Ballot (#42)
* ✨ Implement Cast/Spoil Ballot and Default Election Description - Implement calls to cast/spoil ballots - Implement calls to get default election description - Add serialization methods until new electionguard package is pulled in - Add jsons package - Add electionguard package * 🧪 Add postman collection for existing tests
1 parent 52a05dd commit 975b443

File tree

9 files changed

+861
-2
lines changed

9 files changed

+861
-2
lines changed

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ mypy = "*"
1010
pylint = "*"
1111

1212
[packages]
13+
electionguard = "*"
1314
fastapi = "*"
15+
jsons = "*"
1416
uvicorn = "*"
1517

1618
[requires]

Pipfile.lock

Lines changed: 163 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/api/v1/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from fastapi import APIRouter
22

3-
from app.api.v1.endpoints import ping
3+
from app.api.v1.endpoints import ballot, ping, election
44

55
api_router = APIRouter()
66
api_router.include_router(ping.router, prefix="/ping", tags=["ping"])
7+
api_router.include_router(election.router, prefix="/election", tags=["election"])
8+
api_router.include_router(ballot.router, prefix="/ballot", tags=["ballot"])

app/api/v1/endpoints/ballot.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from electionguard.ballot import CiphertextBallot
2+
from electionguard.ballot_box import accept_ballot, BallotBoxState
3+
from electionguard.election import (
4+
CiphertextElectionContext,
5+
ElectionDescription,
6+
InternalElectionDescription,
7+
)
8+
from electionguard.ballot_store import BallotStore
9+
from fastapi import APIRouter, Body, HTTPException
10+
from pydantic import BaseModel
11+
from typing import Any
12+
from app.utils.serialize import write_json, write_json_object
13+
14+
router = APIRouter()
15+
16+
17+
class AcceptBallotRequest(BaseModel):
18+
ballot: Any
19+
description: Any
20+
context: Any
21+
22+
23+
@router.post("/cast")
24+
def cast_ballot(request: AcceptBallotRequest = Body(...)) -> Any:
25+
"""
26+
Cast ballot
27+
"""
28+
cast_ballot = handle_ballot(request, BallotBoxState.CAST)
29+
if not cast_ballot:
30+
raise HTTPException(
31+
status_code=500,
32+
detail="Ballot failed to be cast",
33+
)
34+
return write_json_object(cast_ballot)
35+
36+
37+
@router.post("/spoil")
38+
def spoil_ballot(request: AcceptBallotRequest = Body(...)) -> Any:
39+
"""
40+
Spoil ballot
41+
"""
42+
spoiled_ballot = handle_ballot(request, BallotBoxState.SPOILED)
43+
if not spoiled_ballot:
44+
raise HTTPException(
45+
status_code=500,
46+
detail="Ballot failed to be spoiled",
47+
)
48+
return write_json_object(spoiled_ballot)
49+
50+
51+
def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any:
52+
ballot = CiphertextBallot.from_json(write_json(request.ballot))
53+
description = ElectionDescription.from_json(write_json(request.description))
54+
internal_description = InternalElectionDescription(description)
55+
context = CiphertextElectionContext.from_json(write_json(request.context))
56+
57+
accepted_ballot = accept_ballot(
58+
ballot,
59+
state,
60+
internal_description,
61+
context,
62+
BallotStore(),
63+
)
64+
65+
return accepted_ballot

app/api/v1/endpoints/election.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from electionguard.election import ElectionDescription
2+
from fastapi import APIRouter, HTTPException
3+
from os.path import realpath, join
4+
from typing import Any
5+
6+
7+
router = APIRouter()
8+
9+
DATA_FOLDER_PATH = realpath(join(__file__, "../../../../data"))
10+
DESCRIPTION_FILE = join(DATA_FOLDER_PATH, "election_description.json")
11+
READ = "r"
12+
13+
14+
@router.get("/description")
15+
def get_default_election_description() -> Any:
16+
"""
17+
Return a default election description
18+
"""
19+
with open(DESCRIPTION_FILE, READ) as description_file:
20+
result = description_file.read()
21+
description = ElectionDescription.from_json(result)
22+
if not description:
23+
raise HTTPException(
24+
status_code=500,
25+
detail="Default description not found",
26+
)
27+
return description

0 commit comments

Comments
 (0)