Skip to content

Commit 849131e

Browse files
authored
Merge pull request #3 from CodeForPhilly/251_add_pdf_export_bufferd
251 add pdf export bufferd [WIP]
2 parents 52f3ed1 + e852bb0 commit 849131e

File tree

10 files changed

+125
-160
lines changed

10 files changed

+125
-160
lines changed

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ dash
77
dash_bootstrap_components
88
pyyaml
99
gunicorn
10+
percy
11+
selenium

src/__init__.py

Whitespace-only changes.

src/chime_dash/app/components/__init__.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from chime_dash.app.components.base import Component, HTMLComponentError
1515
from dash_bootstrap_components.themes import BOOTSTRAP
1616

17-
from chime_dash.app.components.location import LocationComponent
1817
from chime_dash.app.components.navbar import Navbar
1918
from chime_dash.app.components.container import Container
2019

@@ -32,9 +31,7 @@ def __init__(self, language, defaults):
3231
"""
3332
super().__init__(language, defaults)
3433
self.components = OrderedDict(
35-
navbar=Navbar(language, defaults),
36-
container=Container(language, defaults),
37-
location=LocationComponent()
34+
navbar=Navbar(language, defaults), container=Container(language, defaults),
3835
)
3936
self.callback_outputs = []
4037
self.callback_inputs = OrderedDict()

src/chime_dash/app/components/container.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
"""Initializes the dash html
22
"""
33
from collections import OrderedDict
4-
from copy import deepcopy
54

65
import dash_bootstrap_components as dbc
7-
import dash_core_components as dcc
6+
87
from chime_dash.app.components.base import Component, HTMLComponentError
98
from chime_dash.app.components.content import Content
109
from chime_dash.app.components.sidebar import Sidebar
11-
from chime_dash.app.services.pdf_printer import print_to_pdf
10+
1211
from penn_chime.models import SimSirModel
1312

1413

@@ -22,8 +21,7 @@ def __init__(self, language, defaults):
2221
super().__init__(language, defaults)
2322
self.pdf_filename = None
2423
self.components = OrderedDict(
25-
sidebar=Sidebar(language, defaults),
26-
content=Content(language, defaults),
24+
sidebar=Sidebar(language, defaults), content=Content(language, defaults),
2725
)
2826
self.callback_outputs = []
2927
self.callback_inputs = OrderedDict()
@@ -35,7 +33,9 @@ def get_html(self):
3533
"""Initializes the content container dash html
3634
"""
3735
container = dbc.Container(
38-
children=dbc.Row(self.components["sidebar"].html + self.components["content"].html),
36+
children=dbc.Row(
37+
self.components["sidebar"].html + self.components["content"].html
38+
),
3939
fluid=True,
4040
className="mt-5",
4141
)
@@ -49,12 +49,6 @@ def callback(self, *args, **kwargs):
4949
kwargs["model"] = SimSirModel(pars)
5050
kwargs["pars"] = pars
5151

52-
save_to_pdf = self.components["sidebar"].save_to_pdf(kwargs)
53-
if save_to_pdf:
54-
self.pdf_filename = print_to_pdf(deepcopy(self.components['content']), kwargs)
55-
56-
kwargs['pdf_url'] = self.pdf_filename
57-
5852
callback_returns = []
5953
for component in self.components.values():
6054
try:

src/chime_dash/app/components/location.py

-18
This file was deleted.

src/chime_dash/app/components/sidebar.py

+46-35
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,37 @@
33
44
#! _INPUTS should be considered for moving else where
55
"""
6-
import dash_html_components as dhc
7-
86
from typing import List, Dict, Any, Tuple
97
from collections import OrderedDict
108

119
from dash.dependencies import Input as CallbackInput, Output
1210
from dash.development.base_component import ComponentMeta
13-
from dash_html_components import Br
11+
from dash_html_components import Br, Div, Nav
1412

1513
from penn_chime.defaults import RateLos
1614
from penn_chime.parameters import Parameters
1715

1816
from chime_dash.app.components.base import Component
19-
from chime_dash.app.utils.templates import create_switch_input, create_number_input, create_header, create_button, \
20-
create_link
17+
from chime_dash.app.utils.templates import (
18+
create_switch_input,
19+
create_number_input,
20+
create_header,
21+
create_button,
22+
create_link,
23+
)
2124

2225
FLOAT_INPUT_MIN = 0.001
2326
FLOAT_INPUT_STEP = "any"
2427

2528
_INPUTS = OrderedDict(
2629
regional_parameters={"type": "header", "size": "h3"},
27-
market_share={"type": "number", "min": FLOAT_INPUT_MIN, "step": FLOAT_INPUT_STEP, "max": 100.0, "percent": True},
30+
market_share={
31+
"type": "number",
32+
"min": FLOAT_INPUT_MIN,
33+
"step": FLOAT_INPUT_STEP,
34+
"max": 100.0,
35+
"percent": True,
36+
},
2837
susceptible={"type": "number", "min": 1, "step": 1},
2938
known_infected={"type": "number", "min": 0, "step": 1},
3039
current_hospitalized={"type": "number", "min": 0, "step": 1},
@@ -50,7 +59,7 @@
5059
"min": 0.0,
5160
"step": FLOAT_INPUT_STEP,
5261
"max": 100.0,
53-
"percent": True
62+
"percent": True,
5463
},
5564
ventilated_rate={
5665
"type": "number",
@@ -69,8 +78,7 @@
6978
show_tables={"type": "switch", "value": False},
7079
show_tool_details={"type": "switch", "value": False},
7180
show_additional_projections={"type": "switch", "value": False},
72-
save_as_pdf={"type": "button", "property": "n_clicks"},
73-
pdf_file_link={"type": "link", "property": "href"}
81+
download_as_pdf_link={"type": "link"},
7482
)
7583

7684

@@ -79,20 +87,27 @@ class Sidebar(Component):
7987
contains the various inputs used to interact
8088
with the model.
8189
"""
90+
8291
# localization temp. for widget descriptions
8392
localization_file = "sidebar.yml"
8493

8594
callback_inputs = OrderedDict(
86-
(key, CallbackInput(component_id=key, component_property=_INPUTS[key].get("property", "value")))
87-
for key in _INPUTS if _INPUTS[key]["type"] not in ("header", "link")
95+
(
96+
key,
97+
CallbackInput(
98+
component_id=key,
99+
component_property=_INPUTS[key].get("property", "value"),
100+
),
101+
)
102+
for key in _INPUTS
103+
if _INPUTS[key]["type"] not in ("header", "link")
88104
)
89105

90-
callback_outputs = [Output(component_id='pdf_file_link', component_property='href'),
91-
Output(component_id='pdf_file_link', component_property='children')]
106+
callback_outputs = [
107+
Output(component_id="download_as_pdf_link", component_property="href")
108+
]
92109

93110
def __init__(self, *args, **kwargs):
94-
self._save_to_pdf = False
95-
self.pdf_button_clicks = 0
96111
super().__init__(*args, **kwargs)
97112

98113
@staticmethod
@@ -135,7 +150,7 @@ def get_html(self) -> List[ComponentMeta]:
135150
element = create_button(idx, self.content)
136151
elif data["type"] == "link":
137152
elements.append(Br())
138-
element = create_link(idx)
153+
element = create_link(idx, self.content)
139154
else:
140155
raise ValueError(
141156
"Failed to parse input '{idx}' with data '{data}'".format(
@@ -144,14 +159,11 @@ def get_html(self) -> List[ComponentMeta]:
144159
)
145160
elements.append(element)
146161

147-
sidebar = dhc.Nav(
148-
children=dhc.Div(
162+
sidebar = Nav(
163+
children=Div(
149164
children=elements,
150165
className="p-4",
151-
style={
152-
"height": "calc(100vh - 48px)",
153-
"overflowY": "auto",
154-
},
166+
style={"height": "calc(100vh - 48px)", "overflowY": "auto",},
155167
),
156168
className="col-md-3",
157169
style={
@@ -160,23 +172,22 @@ def get_html(self) -> List[ComponentMeta]:
160172
"bottom": 0,
161173
"left": 0,
162174
"zIndex": 100,
163-
"boxShadow": "inset -1px 0 0 rgba(0, 0, 0, .1)"
164-
}
175+
"boxShadow": "inset -1px 0 0 rgba(0, 0, 0, .1)",
176+
},
165177
)
166178

167179
return [sidebar]
168180

169-
def save_to_pdf(self, kwargs):
170-
"""
171-
Return status of save to pdf flag and set it off.
172-
"""
173-
if kwargs.get('save_as_pdf', 0) and kwargs.get('save_as_pdf', 0) > self.pdf_button_clicks:
174-
self.pdf_button_clicks = kwargs.get('save_as_pdf', '0')
175-
return True
176-
return False
177-
178181
def callback( # pylint: disable=W0613, R0201
179182
self, *args, **kwargs
180183
) -> List[Dict[str, Any]]:
181-
return [kwargs.get('pdf_url', ''),
182-
self.content['download_report'] if kwargs.get('pdf_url', None) else None]
184+
185+
url = "/download-as-pdf?" + "&".join(
186+
[
187+
"{key}={val}".format(key=key, val=val)
188+
for key, val in kwargs.items()
189+
if not key in ["model", "pars"] and val is not None
190+
]
191+
)
192+
193+
return [url]
+40-40
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,74 @@
1+
"""Utilities for exporting dash app to pdf
2+
"""
13
import json
24
import base64
5+
36
from time import sleep
47

8+
from io import BytesIO
9+
510
from dash import Dash
611
from dash.testing.application_runners import ThreadedRunner
712
from dash.testing.composite import DashComposite
8-
from dash.dependencies import Input
913
from dash_bootstrap_components.themes import BOOTSTRAP
10-
11-
import dash_core_components as dcc
12-
import uuid
1314
import dash_bootstrap_components as dbc
1415

15-
from chime_dash.app.utils.templates import UPLOAD_DIRECTORY
16-
from dash_html_components import Div
1716
from selenium import webdriver
1817

1918

20-
def send_devtools(driver, cmd, params={}):
21-
resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
22-
url = driver.command_executor._url + resource
23-
body = json.dumps({'cmd': cmd, 'params': params})
24-
response = driver.command_executor._request('POST', url, body)
25-
if response['status']:
26-
raise Exception(response.get('value'))
27-
return response.get('value')
19+
def send_devtools(driver, cmd, params=None):
20+
params = params or None
21+
resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
22+
url = driver.command_executor._url + resource
23+
body = json.dumps({"cmd": cmd, "params": params})
24+
response = driver.command_executor._request("POST", url, body)
25+
if response.get("status", False):
26+
raise Exception(response.get("value"))
27+
return response.get("value")
2828

2929

30-
def save_as_pdf(driver, path, options={}):
31-
# https://timvdlippe.github.io/devtools-protocol/tot/Page#method-printToPDF
32-
result = send_devtools(driver, "Page.printToPDF", options)
33-
with open(path, 'wb') as file:
34-
file.write(base64.b64decode(result['data']))
30+
def save_as_pdf(driver, options=None):
31+
"""Saves pdf to buffer object
32+
"""
33+
options = options or {}
34+
# https://timvdlippe.github.io/devtools-protocol/tot/Page#method-printToPDF
35+
result = send_devtools(driver, "Page.printToPDF", options)
36+
37+
cached_file = BytesIO()
38+
cached_file.write(base64.b64decode(result["data"]))
39+
cached_file.seek(0)
40+
return cached_file
3541

3642

3743
def print_to_pdf(component, kwargs):
44+
"""Extracts content and prints pdf to buffer object.
45+
"""
3846
app = Dash(
3947
__name__,
4048
external_stylesheets=[
41-
"https://www1.pennmedicine.org/styles/shared/penn-medicine-header.css",
42-
BOOTSTRAP,
43-
]
49+
"https://www1.pennmedicine.org/styles/shared/penn-medicine-header.css",
50+
BOOTSTRAP,
51+
],
4452
)
4553

46-
layout = Div([
47-
dcc.Location(id="url", refresh=False),
48-
dbc.Container(
49-
children=component.html,
50-
fluid=True,
51-
className="mt-5",
52-
)])
53-
54-
app.layout = layout
55-
app.title = 'CHIME Printer'
54+
app.layout = dbc.Container(children=component.html, fluid=True)
55+
app.title = "CHIME Printer"
5656

5757
outputs = component.callback(**kwargs)
5858

59-
@app.callback(component.callback_outputs, [Input('url', 'pathname')])
60-
def callback(*args): # pylint: disable=W0612
59+
@app.callback(component.callback_outputs, list(component.callback_inputs.values()))
60+
def callback(*args): # pylint: disable=W0612, W0613
6161
return outputs
6262

6363
chrome_options = webdriver.ChromeOptions()
64-
chrome_options.add_argument('--headless')
64+
chrome_options.add_argument("--headless")
6565

6666
with ThreadedRunner() as starter:
67-
with DashComposite(starter, browser='Chrome', options=[chrome_options]) as dc:
67+
with DashComposite(starter, browser="Chrome", options=[chrome_options]) as dc:
6868
dc.start_server(app, port=8051)
69-
while 'Loading...' in dc.driver.page_source:
69+
while "Loading..." in dc.driver.page_source:
7070
sleep(1)
71-
filename = f'chime-{str(uuid.uuid4())[:8]}.pdf'
72-
save_as_pdf(dc.driver, f'{UPLOAD_DIRECTORY}/{filename}', {'landscape': False})
71+
pdf = save_as_pdf(dc.driver, {"landscape": False})
7372
dc.driver.quit()
74-
return f'/download/{filename}'
73+
74+
return pdf

src/chime_dash/app/templates/en/sidebar.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,4 @@ regional_parameters: Regional Parameters
2121
spread_and_contact: Spread and Contact Parameters
2222
severity_parameters: Severity Parameters
2323
display_parameters: Display Parameters
24-
save_as_pdf: Generate PDF
25-
download_report: Download Report
24+
download_as_pdf_link: Download Report

0 commit comments

Comments
 (0)