Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge development branch after last Friday's sample ballot #98

Merged
merged 30 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3958f12
Extract GPUnit names based on ID
stratofax Aug 17, 2022
9aac1a5
Create dictionaires of lookup tables
stratofax Aug 19, 2022
b29199c
Create and test demo subcommand
stratofax Aug 19, 2022
918446e
Build more lookup dictionaries
stratofax Aug 19, 2022
b461e30
Retrieve more data
stratofax Aug 19, 2022
bcf848d
Select useable code for ballots, rebuild imports
stratofax Aug 21, 2022
e4ba4fe
Add header
stratofax Aug 22, 2022
b5ad780
Render demo ballot
stratofax Aug 22, 2022
c8ca769
Add demo PDF
stratofax Aug 22, 2022
f70c16b
Create demo ballot from the CLI
stratofax Aug 22, 2022
bde72b4
Minor updates, 100% test coverage
stratofax Aug 23, 2022
86fc485
Merge branch 'dev-make-flat' of github.com:stratofax/BallotLabFork in…
stratofax Aug 23, 2022
86819a0
Fixed election_header dict
stratofax Aug 23, 2022
2d0e4a1
This week's version of the json data generator
stratofax Aug 24, 2022
0e7a2cc
Update Spacetown data from the new spacetown JSON
stratofax Aug 24, 2022
c3ec1f8
Comment out ticket code
stratofax Aug 25, 2022
276ceb7
Move img files into src tree
stratofax Aug 25, 2022
400efad
Rewrite files to look in source dir using pathlib
stratofax Aug 25, 2022
34a854e
Fix paths for Windows, include project (not src)
stratofax Aug 25, 2022
08f9b43
Create contest data and layout classes
stratofax Aug 26, 2022
86311d2
Change loggin level to INFO
stratofax Aug 26, 2022
b507496
Add data to contest layout
stratofax Aug 26, 2022
e3d074b
Add party to layout
stratofax Aug 26, 2022
50822df
Add all 4 candidate contests
stratofax Aug 26, 2022
4ca6cfe
Remove redundant code, fix test coverage
stratofax Aug 26, 2022
05c6609
The end of the Versadm directory
stratofax Aug 26, 2022
1bdf76b
Layout updates, add ballot measures
stratofax Aug 26, 2022
c60a4fc
Add ballot measures
stratofax Aug 26, 2022
891011f
Rearrange files to support GitHub Actions file access
stratofax Aug 26, 2022
06a6587
Merge pull request #92 from stratofax/dev-make-flat
stratofax Aug 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed assets/img/warn_cyan.png
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added pdfs/ballot_demo_2022_08_21T232623.pdf
Binary file not shown.
Binary file added src/electos/ballotmaker/assets/img/warn_cyan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
81 changes: 81 additions & 0 deletions src/electos/ballotmaker/ballots/contest_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from dataclasses import dataclass, field
from typing import List


@dataclass
class BallotMeasureData:
"""Retrieve ballot measure contest data from a dict"""

_b_measure_con: dict = field(repr=False)
id: str = field(init=False)
title: str = field(init=False)
district: str = field(init=False)
text: str = field(init=False)
choices: list = field(default_factory=list, init=False, repr=True)

def __post_init__(self):
self.id = "no_id_provided"
self.title = self._b_measure_con["title"]
self.district = self._b_measure_con["district"]
self.text = self._b_measure_con["text"]
self.choices = self._b_measure_con["choices"]
# for choice_data in _choices:
# self.choices.append(ChoiceData(choice_data))


@dataclass
class ChoiceData:
_choice_data: dict = field(repr=False)
id: str = "no_id_provided"
label: str = field(init=False)

def __post_init__(self):
self.label = "no label provided"


@dataclass
class CandidateContestData:
"""Retrieve candidate contest data from a dict"""

_can_con: dict = field(repr=False)
# fields retrieved from the dict
id: str = field(init=False)
title: str = field(init=False)
votes_allowed: int = field(init=False)
district: str = field(init=False)
candidates: list = field(default_factory=list, init=False, repr=True)

def __post_init__(self):
self.id = self._can_con["id"]
self.title = self._can_con["title"]
self.votes_allowed = self._can_con["votes_allowed"]
self.district = self._can_con["district"]
_candidates = self._can_con["candidates"]
for candidate_data in _candidates:
self.candidates.append(CandidateData(candidate_data))


@dataclass
class CandidateData:
_can_data: dict = field(repr=False)
id: str = "no_id_provided"
name: str = field(init=False)
party: str = field(init=False)
party_abbr: str = field(init=False)

def __post_init__(self):
self.name = self._can_data["name"]
party_dict = self._can_data["party"]
self.party = party_dict["name"]
self.party_abbr = party_dict["abbreviation"]


if __name__ == "__main__": # pragma: no cover
from electos.ballotmaker.demo_data import spacetown_data

can_con_data_1 = CandidateContestData(spacetown_data.can_con_1)
print(can_con_data_1)
can_con_data_2 = CandidateContestData(spacetown_data.can_con_2)
print(can_con_data_2)
b_measure_data_1 = BallotMeasureData(spacetown_data.ballot_measure_1)
print(b_measure_data_1)
230 changes: 230 additions & 0 deletions src/electos/ballotmaker/ballots/contest_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# format a ballot contest.

from electos.ballotmaker.ballots.contest_data import (
BallotMeasureData,
CandidateContestData,
)
from electos.ballotmaker.ballots.page_layout import PageLayout
from reportlab.graphics.shapes import Drawing, Ellipse, _DrawingEditorMixin
from reportlab.lib.colors import black, white
from reportlab.lib.styles import LineStyle, getSampleStyleSheet
from reportlab.platypus import Paragraph, Table

oval_width = 10
oval_height = 4

# define styles
# fill colors
light = PageLayout.light
grey = PageLayout.grey

# font family info
font_normal = PageLayout.font_normal
font_bold = PageLayout.font_bold
font_size = PageLayout.font_size
normal_lead = PageLayout.normal_lead
border_pad = PageLayout.border_pad / 2

# start with the sample styles
styles = getSampleStyleSheet()
normal = styles["Normal"]
h1 = styles["Heading1"]
h2 = styles["Heading2"]

# define custom styles for contest tables
PageLayout.define_custom_style(
h1,
grey,
border_pad,
font_size + 2,
black,
font_bold,
normal_lead,
sp_before=12,
sp_after=48,
keep_w_next=1,
)
PageLayout.define_custom_style(
h2,
light,
border_pad,
font_size,
black,
font_bold,
normal_lead,
sp_before=12,
sp_after=48,
keep_w_next=1,
)
PageLayout.define_custom_style(
normal,
white,
border_pad,
font_size,
black,
font_normal,
normal_lead,
)


def build_contest_list(
title: str, instruction: str, selections: list, text: str = ""
) -> list:
"""
Builds a table with contest header, instructions
and choices
"""
row_1 = [Paragraph(title, h1), ""]
row_2 = [Paragraph(instruction, h2), ""]
contest_list = [row_1, row_2]
if text != "":
contest_list.append([Paragraph(text, normal), ""])
contest_list.extend(iter(selections))
return contest_list


def build_candidate_table(contest_list):
return Table(
data=contest_list,
colWidths=(oval_width * 3, None),
style=[
# draw lines below each contestant
("LINEBELOW", (1, 2), (1, -1), 1, grey),
# format the header
("BACKGROUND", (0, 0), (1, 0), grey),
("BACKGROUND", (0, 1), (1, 1), light),
# draw the outer border on top
("LINEABOVE", (0, 0), (1, 0), 3, black),
("LINEBEFORE", (0, 0), (0, -1), 1, black),
("LINEBELOW", (0, -1), (-1, -1), 1, black),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("SPAN", (0, 0), (1, 0)),
("SPAN", (0, 1), (1, 1)),
# ("FONTSIZE", (1, 2), (-1, -1), 48),
("TOPPADDING", (0, 2), (-1, -1), 4),
# pad the first cell
("BOTTOMPADDING", (0, 0), (0, 1), 8),
# pad below each contestant
("BOTTOMPADDING", (0, 2), (-1, -1), 16),
],
)


def build_ballot_measure_table(contest_list):
return Table(
data=contest_list,
colWidths=(oval_width * 3, None),
style=[
# draw lines below each selection
("LINEBELOW", (1, 2), (1, -1), 1, grey),
# format the header
("BACKGROUND", (0, 0), (1, 0), grey),
("BACKGROUND", (0, 1), (1, 1), light),
# draw the outer border on top
("LINEABOVE", (0, 0), (1, 0), 3, black),
("LINEBEFORE", (0, 0), (0, -1), 1, black),
("LINEBELOW", (0, -1), (-1, -1), 1, black),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("SPAN", (0, 0), (-1, 0)),
("SPAN", (0, 1), (-1, 1)),
("SPAN", (0, 2), (-1, 2)),
# ("SPAN", (0, 3), (-1, 3)),
# ("SPAN", (0, 4), (1, 1)),
# ("FONTSIZE", (1, 2), (-1, -1), 48),
("TOPPADDING", (0, 2), (-1, -1), 4),
# pad the first cell
("BOTTOMPADDING", (0, 0), (0, 1), 8),
# pad below each contestant
("BOTTOMPADDING", (0, 2), (-1, -1), 16),
],
)


class SelectionOval(_DrawingEditorMixin, Drawing):
def __init__(self, width=400, height=200, *args, **kw):
Drawing.__init__(self, width, height, *args, **kw)

self.width = oval_width + PageLayout.border_pad
self.height = oval_height + PageLayout.border_pad
oval_cx = self.width / 2
oval_cy = self.height / 2
self._add(
self,
Ellipse(oval_cx, oval_cy, oval_width, oval_height),
name="oval",
validate=None,
desc=None,
)
# self.oval.fillColor = PageLayout.white
self.oval.fillColor = white
# self.oval.strokeColor = PageLayout.black
self.oval.strokeColor = black
self.oval.strokeWidth = 0.5


class CandidateContestLayout:
"""
Generate a candidate contest table flowable
"""

def __init__(self, contest_data: CandidateContestData):
self.id = contest_data.id
self.title = contest_data.title
self.votes_allowed = contest_data.votes_allowed
if self.votes_allowed > 1:
self.instruct = f"Vote for up to {self.votes_allowed}"
else:
self.instruct = f"Vote for {self.votes_allowed}"
self.candidates = contest_data.candidates
_selections = []

oval = SelectionOval()
for candidate in self.candidates:
# add newlines around " and "
# if candidate.find(" and "):
# candidate = candidate.replace(" and ", "<br />and<br />")
contest_line = f"<b>{candidate.name}</b>"
if candidate.party_abbr != "":
contest_line += f"<br />{candidate.party_abbr}"
contest_row = [oval, Paragraph(contest_line, normal)]
_selections.append(contest_row)
# build the contest table, an attribute of the Contest object

self.contest_list = build_contest_list(
self.title, self.instruct, _selections
)
self.contest_table = build_candidate_table(self.contest_list)


class BallotMeasureLayout:
"""
Generate a candidate contest table flowable
"""

def __init__(self, contest_data: BallotMeasureData):
self.id = contest_data.id
self.title = contest_data.title
self.instruct = "Vote yes or no"
self.text = contest_data.text
self.choices = contest_data.choices

oval = SelectionOval()
_selections = []
for choice in self.choices:
contest_line = f"<b>{choice}</b>"
contest_row = [oval, Paragraph(contest_line, normal)]
_selections.append(contest_row)

self.contest_list = build_contest_list(
self.title, self.instruct, _selections, self.text
)
self.contest_table = build_ballot_measure_table(self.contest_list)


if __name__ == "__main__": # pragma: no cover
from electos.ballotmaker.demo_data import spacetown_data

contest_1 = CandidateContestData(spacetown_data.can_con_1)
print(contest_1.candidates)
layout_1 = CandidateContestLayout(contest_1)
print(layout_1.contest_list)
Loading