Skip to content

Commit ab1d26d

Browse files
committed
add user guide and improve zip export
1 parent 9bb6f52 commit ab1d26d

16 files changed

+312
-31
lines changed

qmra/risk_assessment/exports.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from zipfile import ZipFile
2+
import base64
3+
from django.db.models import QuerySet
4+
from django.template.loader import render_to_string
5+
6+
from qmra.risk_assessment.models import RiskAssessment, RiskAssessmentResult, Inflow, Treatment
7+
import pandas as pd
8+
9+
from qmra.risk_assessment.plots import risk_plots
10+
11+
12+
def inflows_as_df(inflows: QuerySet[Inflow]):
13+
dfs = []
14+
for inflow in inflows.all():
15+
dfs += [pd.DataFrame({
16+
"Pathogen": [inflow.pathogen],
17+
"Minimum Concentration": [inflow.min],
18+
"Maximum Concentration": [inflow.max],
19+
})]
20+
return pd.concat(dfs)
21+
22+
23+
def treatments_as_df(treatments: QuerySet[Treatment]) -> pd.DataFrame:
24+
dfs = []
25+
for t in treatments.all():
26+
dfs += [pd.DataFrame({
27+
"Treatment": [t.name] * 3,
28+
"Pathogen group": ["Viruses", "Bacteria", "Protozoa"],
29+
"Maximum LRV": [t.viruses_max, t.bacteria_max, t.protozoa_max],
30+
"Minimum LRV": [t.viruses_min, t.bacteria_min, t.protozoa_min]
31+
})]
32+
return pd.concat(dfs)
33+
34+
35+
def risk_assessment_result_as_df(pathogen: str, r: RiskAssessmentResult) -> pd.DataFrame:
36+
return pd.DataFrame({
37+
("", "pathogen"): [pathogen] * 2,
38+
("", "stat"): ["Maximum LRV", "Minimum LRV"],
39+
("Infection prob.", "min"): [
40+
r.infection_maximum_lrv_min, r.infection_minimum_lrv_min
41+
],
42+
("Infection prob.", "25%"): [
43+
r.infection_maximum_lrv_q1, r.infection_minimum_lrv_q1
44+
],
45+
("Infection prob.", "50%"): [
46+
r.infection_maximum_lrv_median, r.infection_minimum_lrv_median
47+
],
48+
("Infection prob.", "75%"): [
49+
r.infection_maximum_lrv_q3, r.infection_minimum_lrv_q3
50+
],
51+
("Infection prob.", "max"): [
52+
r.infection_maximum_lrv_max, r.infection_minimum_lrv_max
53+
],
54+
("DALYs pppy", "min"): [
55+
r.dalys_maximum_lrv_min, r.dalys_minimum_lrv_min
56+
],
57+
("DALYs pppy", "25%"): [
58+
r.dalys_maximum_lrv_q1, r.dalys_minimum_lrv_q1
59+
],
60+
("DALYs pppy", "50%"): [
61+
r.dalys_maximum_lrv_median, r.dalys_minimum_lrv_median
62+
],
63+
("DALYs pppy", "75%"): [
64+
r.dalys_maximum_lrv_q3, r.dalys_minimum_lrv_q3
65+
],
66+
("DALYs pppy", "max"): [
67+
r.dalys_maximum_lrv_max, r.dalys_minimum_lrv_max
68+
],
69+
})
70+
71+
72+
def results_as_df(results: dict[str, RiskAssessmentResult]) -> pd.DataFrame:
73+
dfs = []
74+
for pathogen, r in results.items():
75+
dfs += [risk_assessment_result_as_df(pathogen, r)]
76+
return pd.concat(dfs)
77+
78+
79+
def risk_assessment_as_zip(buffer, risk_assessment: RiskAssessment):
80+
inflows = inflows_as_df(risk_assessment.inflows)
81+
treatments = treatments_as_df(risk_assessment.treatments)
82+
results = results_as_df({r.pathogen: r for r in risk_assessment.results.all()})
83+
plots = risk_plots(risk_assessment.results.all(), "png")
84+
report = render_to_string("assessment-result-export.html",
85+
context=dict(results=risk_assessment.results.all(),
86+
infection_risk=risk_assessment.infection_risk,
87+
risk_plot_data=base64.b64encode(plots[0]).decode("utf-8"),
88+
daly_plot_data=base64.b64encode(plots[1]).decode("utf-8")))
89+
with ZipFile(buffer, mode="w") as archive:
90+
archive.mkdir("exposure-assessment")
91+
archive.mkdir("results-plots")
92+
archive.writestr("exposure-assessment/inflows.csv", inflows.to_csv(sep=",", decimal=".", index=False))
93+
archive.writestr("exposure-assessment/treatments.csv", treatments.to_csv(sep=",", decimal=".", index=False))
94+
archive.writestr(f"{risk_assessment.name}-result.csv", results.to_csv(sep=",", decimal=".", index=False))
95+
archive.writestr(f"{risk_assessment.name}-report.html", report)
96+
archive.writestr("results-plots/infection-probability.png", plots[0])
97+
archive.writestr("results-plots/dalys-pppy.png", plots[1])

qmra/risk_assessment/forms.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs):
3434
self.helper.form_tag = False
3535
self.helper.label_class = "text-muted small"
3636
self.helper.layout = Layout(
37-
Row(Column("name"), Column("description")),
37+
Row(Column("name"), Column("description"), css_id="name-and-description"),
3838
Row(Column("exposure_name"), Column("events_per_year"), Column("volume_per_event"), css_id="exposure-form-fieldset"),
3939
# Row("source_name", css_id="source-form")
4040
)

qmra/risk_assessment/plots.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
COLOR_SEQS = dict(min=MIN_COLOR_SEQ, max=MAX_COLOR_SEQ, none=NONE_COLOR_SEQ)
2727

2828

29-
def risk_plots(risk_assessment_results, risk_category="none"):
29+
def risk_plots(risk_assessment_results, output_type="div"):
3030
infection_prob_fig = go.Figure()
3131
dalys_fig = go.Figure()
3232
for i, r in enumerate(risk_assessment_results):
@@ -111,7 +111,7 @@ def risk_plots(risk_assessment_results, risk_category="none"):
111111
dalys_fig.update_traces(
112112
marker_size=8
113113
)
114-
115-
return plot(infection_prob_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False), \
116-
plot(dalys_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False)
117-
114+
if output_type == "div":
115+
return plot(infection_prob_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False), \
116+
plot(dalys_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False)
117+
return infection_prob_fig.to_image(format=output_type), dalys_fig.to_image(format=output_type)

qmra/risk_assessment/templates/assessment-configurator.html

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ <h4 class="ml-3">Risk assessment parameters</h4>
1111
{% include "risk-assessment-form-fieldset.html" with risk_assessment_form=risk_assessment_form %}
1212
{% include "inflows-form-fieldset.html" with inflow_form=inflow_form source_name_field=risk_assessment_form.source_name %}
1313
{% include "treatments-form-fieldset.html" with treatment_form=treatment_form add_treatment_form=add_treatment_form %}
14-
<div class="my-2 configurator-section" id="configurator-commands" style="z-index: 1000">
14+
<div class="my-2 configurator-section" id="configurator-commands" style="z-index: 98">
1515
<div class="col ">
1616
<input id="save-risk-assessment-btn" type="submit" class="btn btn-primary w-100 my-2" value="Save">
1717
</div>
@@ -20,19 +20,19 @@ <h4 class="ml-3">Risk assessment parameters</h4>
2020
<div class="w-100">
2121
<ul class="nav nav-tabs" role="tablist">
2222
<li class="nav-item">
23-
<button class="nav-link active" data-toggle="tab" data-target="#assessment-result" type="button"
23+
<button class="nav-link active" id="result-button" data-toggle="tab" data-target="#assessment-result" type="button"
2424
role="tab" aria-controls="home" aria-selected="true">
2525
Result
2626
</button>
2727
</li>
2828
<li class="nav-item">
29-
<button class="nav-link" data-toggle="tab" data-target="#info" type="button" role="tab"
29+
<button class="nav-link" id="references-button" data-toggle="tab" data-target="#info" type="button" role="tab"
3030
aria-controls="home" aria-selected="false">
3131
References
3232
</button>
3333
</li>
3434
<li class="nav-item">
35-
<button class="nav-link" data-toggle="tab" data-target="#guide" type="button" role="tab"
35+
<button id="start-user-guide" class="nav-link" data-toggle="tab" data-target="#guide" type="button" role="tab"
3636
aria-controls="home" aria-selected="false">
3737
User guide
3838
</button>
@@ -96,6 +96,7 @@ <h4>Treatments</h4>
9696
{% endblock %}
9797
{% block script %}
9898
<div>
99+
{% include "guided-tour.html" %}
99100
<script type="text/javascript">
100101
document.addEventListener(`keypress`, evt => {
101102
const form = evt.target.closest(`#configurator`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
{% include "head.html" %}
4+
<body>
5+
<div class="container">
6+
{% include "assessment-result.html" with results=results infection_risk=infection_risk risk_plot_data=risk_plot_data daly_plot_data=daly_plot_data %}
7+
</div>
8+
</body>
9+
<style type="text/css">
10+
11+
.max-risk {
12+
background: #FFECF4;
13+
color: #FF0532;
14+
}
15+
16+
.min-risk {
17+
background: #FFDDB5;
18+
color: #ED5500;
19+
}
20+
21+
.none-risk {
22+
background: #E2FBAC;
23+
color: #088B3C;
24+
}
25+
</style>
26+
</html>

qmra/risk_assessment/templates/assessment-result.html

+15
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,31 @@ <h4 class="mt-3 mx-5 text-center">Result</h4>
4545
</div>
4646
{% endif %}
4747
</div>
48+
{% if risk_plot is not None %}
4849
<div id="hero-graph" class="mb-3">
4950
{% autoescape off %}
5051
{{ risk_plot }}
5152
{% endautoescape %}
5253
</div>
54+
{% endif %}
55+
{% if daly_plot is not None %}
5356
<div id="hero-graph" class="mb-3">
5457
{% autoescape off %}
5558
{{ daly_plot }}
5659
{% endautoescape %}
5760
</div>
61+
{% endif %}
62+
{% if risk_plot_data is not None %}
63+
<div class="mb-3">
64+
<img src="data:image/png;charset=utf-8;base64, {{risk_plot_data}}">
65+
</div>
66+
{% endif %}
67+
{% if daly_plot_data is not None %}
68+
<div class="mb-3">
69+
<img src="data:image/png;charset=utf-8;base64, {{daly_plot_data}}">
70+
</div>
71+
{% endif %}
72+
5873
<!-- <h4 style="text-align: center">Risk per pathogen</h4>
5974
<div class="w-50 m-auto">
6075
<table class="table">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<script type="text/javascript">
2+
const steps = [
3+
{
4+
title: "Name and description",
5+
content: "Before configuring your first QMRA model, give the risk assessment an informative name. It is also helpful to add some information on the scope and purpose of the risk assessment, for which you can use the provided text field. The text will be part of the final report, so you may adapt the level of detail to the target group of people, you want to share the report with. Don't worry, both can be adapted even after the model is configured.",
6+
target: "#name-and-description",
7+
order: 0
8+
},
9+
{
10+
title: "Exposure",
11+
content: "A QMRA model always contain an exposure assessment. Within exposure assessment the frequency of exposure is defined as the number of exposure events. The ingested volume defines the magnitude of exposure per event.\nWhen you select an exposure name, default values for the number of events and the volume per event will be set. You can modify these values as you wish.",
12+
target: "#exposure-form-fieldset",
13+
order: 1
14+
},
15+
{
16+
title: "Source water and inflows",
17+
content: "In this section you can define the type of source water and the concentrations of reference pathogens for your QMRA model. When you select a source water type, the app will fill default values for the reference pathogens Rotavirus, Cryptosporidium spp. and Campylobacter jejuni. Once again, you can modify these values as you wish.",
18+
target: "#inflow-content",
19+
order: 2
20+
},
21+
{
22+
title: "Treatments",
23+
content: "The last step of a QMRA model is the configuration of the planned or implemented treatment processes. Each treatment is associated with a certain logremoval value (LRV) for viruses, bacteria, and protozoan parasites, respectively. When you add a treatment to your model, it is initialized with default LRVs collected from international guideline documents. Note, however, that the most reliable results may be achieved by providing locally obtained removal rates. Note also that treatments can be non-technical barriers or contain negative LRV for simulating recontamination.",
24+
target: "#treatment-content",
25+
order: 3
26+
},
27+
{
28+
title: "Results",
29+
content: "Clicking on this tab will allow you to display the result of your risk assessment.",
30+
target: "#result-button",
31+
order: 4
32+
},
33+
{
34+
title: "Save",
35+
content: "Once the parameters for the exposure and the inflows are set (an assessment can contain zero or more treatments), you can save your risk assessment. This will take you to your list of risk assessment where you can compare multiple scenarios to each other and export assessments as zip archives.",
36+
target: "#configurator-commands",
37+
order: 5
38+
},
39+
{
40+
title: "References",
41+
content: "You can inspect the references for the default values of the elements you selected (exposure, source water, treatments) by clicking on this tab. Note that any change you made to the default values will not be reflected in this section.",
42+
target: "#references-button",
43+
order: 6
44+
},
45+
];
46+
document.addEventListener("DOMContentLoaded", () => {
47+
const tg = new tourguide.TourGuideClient({
48+
steps: steps
49+
})
50+
tg.onAfterExit(() => {
51+
document.querySelector("#result-button").click();
52+
})
53+
document.querySelector("#start-user-guide").addEventListener("click", ev => {
54+
tg.start();
55+
})
56+
})
57+
</script>

qmra/risk_assessment/templates/inflows-form-js.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
const y = parseFloat(x.toFixed(4));
1414
var step = "any";
1515
if (y < 1 && y > 0){
16-
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10))-1)+"1";
16+
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10)))+"1";
1717
}
1818
elem.step = step;
1919
}},
@@ -22,7 +22,7 @@
2222
const y = parseFloat(x.toFixed(4));
2323
var step = "any";
2424
if (y < 1 && y > 0){
25-
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10))-1)+"1";
25+
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10)))+"1";
2626
}
2727
elem.step = step;
2828
}}

qmra/risk_assessment/templates/risk-assessment-list.html

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ <h4 class="media-heading text-truncate kwb_headline">
7777
</div>
7878
</div>
7979
<div style="height: 21px; display: flex; margin-left: auto; line-height: 0 !important">
80+
<a class="mr-1" href="{% url 'assessment-export' assessment.id %}" style="line-height: 1.25;">ZIP</a>
8081
<label class="text-muted small form-inline mb-0 mr-1">compare </label>
8182
<input type="checkbox" class="select-assessment-btn mr-2" value="{{assessment.id}}"
8283
style="width: 20px; height: 20px">
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import io
2+
3+
from assertpy import assert_that
4+
from django.test import TestCase
5+
import pandas as pd
6+
7+
from qmra.risk_assessment import exports
8+
from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment, DefaultTreatments
9+
from qmra.risk_assessment.risk import assess_risk
10+
from qmra.user.models import User
11+
12+
13+
class TestResultExport(TestCase):
14+
15+
def test_that(self):
16+
given_user = User.objects.create_user("test-user2", "[email protected]", "password")
17+
given_user.save()
18+
given_ra = RiskAssessment.objects.create(
19+
user=given_user,
20+
events_per_year=1,
21+
volume_per_event=2,
22+
)
23+
given_ra.save()
24+
given_inflows = [
25+
Inflow.objects.create(
26+
risk_assessment=given_ra,
27+
pathogen="Rotavirus",
28+
min=0.1, max=0.2
29+
),
30+
Inflow.objects.create(
31+
risk_assessment=given_ra,
32+
pathogen="Campylobacter jejuni",
33+
min=0.1, max=0.2
34+
),
35+
Inflow(
36+
risk_assessment=given_ra,
37+
pathogen="Cryptosporidium parvum",
38+
min=0.1, max=0.2
39+
),
40+
]
41+
given_treatments = [
42+
Treatment.from_default(t, given_ra)
43+
for t in list(DefaultTreatments.data.values())[:3]
44+
]
45+
given_ra.inflows.set(given_inflows, bulk=False)
46+
given_ra.treatments.set(given_treatments, bulk=False)
47+
48+
results = assess_risk(given_ra, given_inflows, given_treatments)
49+
given_ra = RiskAssessment.objects.get(pk=given_ra.id)
50+
with io.BytesIO() as buffer:
51+
exports.risk_assessment_as_zip(buffer, given_ra)
52+
buffer.seek(0)
53+
with open("test.zip", "wb") as f:
54+
f.write(buffer.getvalue())
55+
assert_that(buffer).is_not_none()

qmra/risk_assessment/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
views.risk_assessment_result,
2424
name="assessment-result",
2525
),
26+
path(
27+
"assessment/<uuid:risk_assessment_id>/export",
28+
views.export_risk_assessment,
29+
name="assessment-export",
30+
),
2631
path(
2732
"assessment/results",
2833
views.risk_assessment_result,

0 commit comments

Comments
 (0)