codeforamerica/intake

View on GitHub
printing/pdf_form_display.py

Summary

Maintainability
A
3 hrs
Test Coverage
import io
import string
import collections

from django.conf import settings
from reportlab.pdfgen.canvas import Canvas

from reportlab.lib.pagesizes import letter
from reportlab.lib import units, colors
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph


digit_chars = string.digits + '.'
unit_lookup = {
    'pt': 1,
    'in': units.inch,
    'cm': units.cm,
    'mm': units.mm
}

Margin = collections.namedtuple("Margin", ['top', 'right', 'bottom', 'left'])
Size = collections.namedtuple("Size", ['x', 'y', 'width', 'height'])
Position = collections.namedtuple("Position", ['x', 'y'])
LEADING_FACTOR = 1.2


def Style(name, font='Helvetica', size=12, leading=None, color=colors.black):
    return ParagraphStyle(
        name,
        fontName=font,
        fontSize=size,
        leading=leading or size * LEADING_FACTOR,
        textColor=color
    )


LABEL_STYLE = Style('label', 'Helvetica', size=10, color=colors.gray)
BODY_STYLE = Style('body', 'Times')
VALUE_STYLE = Style('value', 'Helvetica')
FOOTNOTE_STYLE = Style('footnote', 'Courier', size=9)


def u(amount):
    units = ''.join([c for c in amount if c in string.ascii_letters])
    units = units.lower()
    number_string = ''.join([c for c in amount if c in digit_chars])
    if '.' in number_string:
        number = float(number_string)
    else:
        number = int(number_string)
    multiplier = unit_lookup.get(units, 1)
    return number * multiplier


class PDFFormDisplay:

    main_display_fields = [
        ['date_received', 'counties'],
        ['first_name', 'middle_name', 'last_name'],
        ['aliases'],
        ['dob', 'last_four', 'driver_license_or_id', 'ssn', 'case_number',
         'pfn_number'],
        ['preferred_pronouns'],
        ['contact_preferences'],
        ['phone_number', 'alternate_phone_number'],
        ['email'],
        ['address'],
        ['is_california_resident', 'how_long_california_resident'],
        ['currently_employed', 'income_source'],
        ['monthly_income', 'monthly_expenses', 'other_income'],
        ['household_size', 'dependents', 'has_children', 'is_married'],
        ['owns_home', 'on_public_benefits'],
        ['how_did_you_hear'],
        [
            'understands_limits',
            'consent_to_represent',
            'consent_self_represent'
        ],
        [
            'identity_confirmation',
            'understands_maybe_fee'
        ],
        ['additional_information'],
    ]

    case_status_fields = [
        ['us_citizen'],
        ['is_veteran'],
        ['is_student'],
        [
            'on_probation_parole',
            'where_probation_or_parole',
            'when_probation_or_parole',
            'finished_half_probation',
            'reduced_probation',
        ],
        ['being_charged'],
        ['serving_sentence'],
        ['has_been_to_prison'],
        [
            'rap_outside_sf',
            'when_where_outside_sf'
        ],
        ['has_suspended_license'],
        ['owes_court_fees'],
        ['reasons_for_applying'],
        [
            'has_been_denied_housing_or_employment',
            'who_denied_housing_or_employment'
        ],
        ['seeking_employment_that_requires_livescan'],
        ['is_registered_under_pc_290'],
    ]

    letter_display_fields = [
        'declaration_letter_intro',
        'declaration_letter_life_changes',
        'declaration_letter_activities',
        'declaration_letter_goals',
        'declaration_letter_why',
    ]

    field_bottom_margin = 14
    related_field_bottom_margin = 4

    def __init__(self, display_form, letter_display=None, canvas=None):
        self.file = io.BytesIO()
        self.width, self.height = letter
        self.canvas = canvas
        if not canvas:
            self.canvas = Canvas(
                self.file,
                pagesize=letter)
            self.canvas.setAuthor('Clear My Record, Code for America')
        self.frame = Margin(
            u('1in'),
            u('.75in'),
            u('1in'),
            u('.75in'))
        self.form = display_form
        self.letter = letter_display
        self.cursor = Position(
            self.frame.left,
            self.height - self.frame.top
        )
        self.nice_date = \
            self.form.date_received.get_current_value().strftime("%B %-d, %Y")

    def set_cursor(self, x, y):
        self.cursor = Position(x, y)
        return self.cursor

    def move_cursor(self, x=0, y=0):
        return self.set_cursor(
            self.cursor.x + x,
            self.cursor.y + y)

    def draw_paragraph(self, text, max_width, max_height, style):
        if not text:
            text = ''
        if not isinstance(text, str):
            text = str(text)
        text = text.strip(string.whitespace)
        text = text.replace('\n', "<br/>")
        p = Paragraph(text, style)
        used_width, used_height = p.wrap(max_width, max_height)
        line_widths = p.getActualLineWidths0()
        number_of_lines = len(line_widths)
        if number_of_lines > 1:
            actual_width = used_width
        elif number_of_lines == 1:
            actual_width = min(line_widths)
            used_width, used_height = p.wrap(actual_width + 0.1, max_height)
        else:
            return 0, 0
        p.drawOn(self.canvas, self.cursor.x, self.cursor.y - used_height)
        return used_width, used_height

    def draw_field_label(self, text, max_width):
        return self.draw_paragraph(
            text,
            max_width,
            LABEL_STYLE.leading * 2,
            LABEL_STYLE)

    def draw_field_value(self, text, max_width):
        return self.draw_paragraph(
            text,
            max_width,
            u('5in'),
            VALUE_STYLE)

    def draw_field(self, field, max_width):
        label_text = field.get_display_label()
        label_width, label_height = \
            self.draw_field_label(label_text, max_width)
        self.move_cursor(0, -label_height)
        if field.context_key == 'counties':
            display_value = field.get_display_value(
                unlisted_counties=self.form.cleaned_data.get(
                    'unlisted_counties'))
        else:
            display_value = field.get_display_value()
        value_width, value_height = self.draw_field_value(
            display_value, max_width)
        total_width = max([label_width, value_width])
        total_height = label_height + value_height
        return total_width, total_height

    def keys_to_fields(self, keys, form_attr='form'):
        form = getattr(self, form_attr)
        return [
            form.fields[key]
            for key in keys
            if key in form.fields]

    def draw_vertical_section(self, fields, max_width):
        existing_fields = self.keys_to_fields(fields)
        if not existing_fields:
            return (0, 0)
        heights = []
        widths = []
        for field_index, field in enumerate(existing_fields):
            if field_index > 0:
                self.move_cursor(0, -self.related_field_bottom_margin)
                heights.append(self.related_field_bottom_margin)
            label_text = field.get_display_label()
            dx, dy = self.draw_field_label(label_text, max_width)
            self.move_cursor(0, -dy)
            heights.append(dy)
            widths.append(dx)
            value_text = field.get_display_value()
            dx, dy = self.draw_field_value(value_text, max_width)
            widths.append(dx)
            self.move_cursor(0, -dy)
            heights.append(dy)
        return max(widths), sum(heights)

    def draw_field_row(self, fields, max_width):
        self.cursor = Position(self.frame.left, self.cursor.y)
        field_index = 0
        field_gutter = 24
        existing_fields = self.keys_to_fields(fields)
        num_fields = len(existing_fields)
        if not num_fields:
            return (0, 0)
        num_gutters = num_fields - 1
        max_row_width = max_width
        total_gutter_width = field_gutter * num_gutters
        max_field_width = \
            (max_row_width - total_gutter_width) / num_fields
        field_heights = []
        incremental_widths = []
        for field in existing_fields:
            if field_index > 0:
                self.move_cursor(field_gutter, 0)
                incremental_widths.append(field_gutter)
            start_point = self.cursor
            dx, dy = self.draw_field(field, max_field_width)
            incremental_widths.append(dx)
            field_heights.append(dy)
            self.set_cursor(start_point.x + dx, start_point.y)
            field_index += 1
        return (
            sum(incremental_widths),
            max(field_heights)
        )

    def draw_main_fields(self):
        self.set_cursor(self.frame.left, self.height - self.frame.top)
        max_width = u('4.75in')
        for row in self.main_display_fields:
            start = self.cursor
            dx, dy = self.draw_field_row(row, max_width)
            margin = self.field_bottom_margin if dx else 0
            self.set_cursor(
                self.frame.left,
                start.y - (dy + margin))

    def draw_case_status_fields(self):
        left_edge = u('5.625in')
        max_width = u('2.125in')
        self.set_cursor(left_edge, self.height - self.frame.top)
        for section in self.case_status_fields:
            dx, dy = self.draw_vertical_section(section, max_width)
            vertical_margin = self.field_bottom_margin if dx else 0
            self.move_cursor(0, -vertical_margin)

    def draw_letter(self):
        max_width = u('6.5in')
        max_height = self.height - (
            self.frame.top + self.frame.bottom)
        self.set_cursor(u('1in'), self.height - self.frame.top)
        dx, dy = self.draw_paragraph(
            self.nice_date, max_width, max_height, BODY_STYLE)
        self.move_cursor(0, -(dy + self.field_bottom_margin))
        dx, dy = self.draw_paragraph(
            "To Whom It May Concern,", max_width, max_height, BODY_STYLE)
        self.move_cursor(0, -(dy + self.field_bottom_margin))
        for field in self.keys_to_fields(self.letter_display_fields, 'letter'):
            dx, dy = self.draw_paragraph(
                field.get_display_value(), max_width, max_height, BODY_STYLE)
            self.move_cursor(0, -(dy + self.field_bottom_margin))
        dx, dy = self.draw_paragraph(
            "Sincerely,", max_width, max_height, BODY_STYLE)
        self.move_cursor(0, -(dy + self.field_bottom_margin))
        full_name = " ".join([
            self.form.first_name.get_display_value(),
            self.form.last_name.get_display_value()
        ])
        dx, dy = self.draw_paragraph(
            full_name, max_width, max_height, BODY_STYLE)

    def draw_header(self):
        self.set_cursor(self.frame.left, self.height - u('0.5in'))
        width = self.width - u('1.5in')
        dx, dy = self.draw_paragraph(
            self.form.submission.get_external_url(),
            width, u('0.5in'),
            FOOTNOTE_STYLE
        )

    def render(self, save=True, title=None):
        self.draw_header()
        self.draw_main_fields()
        self.draw_case_status_fields()
        self.canvas.showPage()
        if self.letter:
            self.draw_letter()
            self.canvas.showPage()
        if title:
            self.canvas.setTitle(title)
        if save:
            self.canvas.save()
        return self.canvas, self.file