Skip to content

Commit

Permalink
Merge pull request #98 from TrustTheVote-Project/dev-make-flat
Browse files Browse the repository at this point in the history
Merge development branch after last Friday's sample ballot, close relevant issues
  • Loading branch information
stratofax authored Aug 29, 2022
2 parents a2b9344 + 06a6587 commit 5dd5be5
Show file tree
Hide file tree
Showing 49 changed files with 1,453 additions and 565 deletions.
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.
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.
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 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

0 comments on commit 5dd5be5

Please sign in to comment.