Skip to content

Commit

Permalink
add user guide and improve zip export
Browse files Browse the repository at this point in the history
  • Loading branch information
adaurat committed Dec 4, 2024
1 parent 9bb6f52 commit ab1d26d
Show file tree
Hide file tree
Showing 16 changed files with 312 additions and 31 deletions.
97 changes: 97 additions & 0 deletions qmra/risk_assessment/exports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from zipfile import ZipFile
import base64
from django.db.models import QuerySet
from django.template.loader import render_to_string

from qmra.risk_assessment.models import RiskAssessment, RiskAssessmentResult, Inflow, Treatment
import pandas as pd

from qmra.risk_assessment.plots import risk_plots


def inflows_as_df(inflows: QuerySet[Inflow]):
dfs = []
for inflow in inflows.all():
dfs += [pd.DataFrame({
"Pathogen": [inflow.pathogen],
"Minimum Concentration": [inflow.min],
"Maximum Concentration": [inflow.max],
})]
return pd.concat(dfs)


def treatments_as_df(treatments: QuerySet[Treatment]) -> pd.DataFrame:
dfs = []
for t in treatments.all():
dfs += [pd.DataFrame({
"Treatment": [t.name] * 3,
"Pathogen group": ["Viruses", "Bacteria", "Protozoa"],
"Maximum LRV": [t.viruses_max, t.bacteria_max, t.protozoa_max],
"Minimum LRV": [t.viruses_min, t.bacteria_min, t.protozoa_min]
})]
return pd.concat(dfs)


def risk_assessment_result_as_df(pathogen: str, r: RiskAssessmentResult) -> pd.DataFrame:
return pd.DataFrame({
("", "pathogen"): [pathogen] * 2,
("", "stat"): ["Maximum LRV", "Minimum LRV"],
("Infection prob.", "min"): [
r.infection_maximum_lrv_min, r.infection_minimum_lrv_min
],
("Infection prob.", "25%"): [
r.infection_maximum_lrv_q1, r.infection_minimum_lrv_q1
],
("Infection prob.", "50%"): [
r.infection_maximum_lrv_median, r.infection_minimum_lrv_median
],
("Infection prob.", "75%"): [
r.infection_maximum_lrv_q3, r.infection_minimum_lrv_q3
],
("Infection prob.", "max"): [
r.infection_maximum_lrv_max, r.infection_minimum_lrv_max
],
("DALYs pppy", "min"): [
r.dalys_maximum_lrv_min, r.dalys_minimum_lrv_min
],
("DALYs pppy", "25%"): [
r.dalys_maximum_lrv_q1, r.dalys_minimum_lrv_q1
],
("DALYs pppy", "50%"): [
r.dalys_maximum_lrv_median, r.dalys_minimum_lrv_median
],
("DALYs pppy", "75%"): [
r.dalys_maximum_lrv_q3, r.dalys_minimum_lrv_q3
],
("DALYs pppy", "max"): [
r.dalys_maximum_lrv_max, r.dalys_minimum_lrv_max
],
})


def results_as_df(results: dict[str, RiskAssessmentResult]) -> pd.DataFrame:
dfs = []
for pathogen, r in results.items():
dfs += [risk_assessment_result_as_df(pathogen, r)]
return pd.concat(dfs)


def risk_assessment_as_zip(buffer, risk_assessment: RiskAssessment):
inflows = inflows_as_df(risk_assessment.inflows)
treatments = treatments_as_df(risk_assessment.treatments)
results = results_as_df({r.pathogen: r for r in risk_assessment.results.all()})
plots = risk_plots(risk_assessment.results.all(), "png")
report = render_to_string("assessment-result-export.html",
context=dict(results=risk_assessment.results.all(),
infection_risk=risk_assessment.infection_risk,
risk_plot_data=base64.b64encode(plots[0]).decode("utf-8"),
daly_plot_data=base64.b64encode(plots[1]).decode("utf-8")))
with ZipFile(buffer, mode="w") as archive:
archive.mkdir("exposure-assessment")
archive.mkdir("results-plots")
archive.writestr("exposure-assessment/inflows.csv", inflows.to_csv(sep=",", decimal=".", index=False))
archive.writestr("exposure-assessment/treatments.csv", treatments.to_csv(sep=",", decimal=".", index=False))
archive.writestr(f"{risk_assessment.name}-result.csv", results.to_csv(sep=",", decimal=".", index=False))
archive.writestr(f"{risk_assessment.name}-report.html", report)
archive.writestr("results-plots/infection-probability.png", plots[0])
archive.writestr("results-plots/dalys-pppy.png", plots[1])
2 changes: 1 addition & 1 deletion qmra/risk_assessment/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs):
self.helper.form_tag = False
self.helper.label_class = "text-muted small"
self.helper.layout = Layout(
Row(Column("name"), Column("description")),
Row(Column("name"), Column("description"), css_id="name-and-description"),
Row(Column("exposure_name"), Column("events_per_year"), Column("volume_per_event"), css_id="exposure-form-fieldset"),
# Row("source_name", css_id="source-form")
)
Expand Down
10 changes: 5 additions & 5 deletions qmra/risk_assessment/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
COLOR_SEQS = dict(min=MIN_COLOR_SEQ, max=MAX_COLOR_SEQ, none=NONE_COLOR_SEQ)


def risk_plots(risk_assessment_results, risk_category="none"):
def risk_plots(risk_assessment_results, output_type="div"):
infection_prob_fig = go.Figure()
dalys_fig = go.Figure()
for i, r in enumerate(risk_assessment_results):
Expand Down Expand Up @@ -111,7 +111,7 @@ def risk_plots(risk_assessment_results, risk_category="none"):
dalys_fig.update_traces(
marker_size=8
)

return plot(infection_prob_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False), \
plot(dalys_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False)

if output_type == "div":
return plot(infection_prob_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False), \
plot(dalys_fig, output_type="div", config={'displayModeBar': False}, include_plotlyjs=False)
return infection_prob_fig.to_image(format=output_type), dalys_fig.to_image(format=output_type)
9 changes: 5 additions & 4 deletions qmra/risk_assessment/templates/assessment-configurator.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ <h4 class="ml-3">Risk assessment parameters</h4>
{% include "risk-assessment-form-fieldset.html" with risk_assessment_form=risk_assessment_form %}
{% include "inflows-form-fieldset.html" with inflow_form=inflow_form source_name_field=risk_assessment_form.source_name %}
{% include "treatments-form-fieldset.html" with treatment_form=treatment_form add_treatment_form=add_treatment_form %}
<div class="my-2 configurator-section" id="configurator-commands" style="z-index: 1000">
<div class="my-2 configurator-section" id="configurator-commands" style="z-index: 98">
<div class="col ">
<input id="save-risk-assessment-btn" type="submit" class="btn btn-primary w-100 my-2" value="Save">
</div>
Expand All @@ -20,19 +20,19 @@ <h4 class="ml-3">Risk assessment parameters</h4>
<div class="w-100">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-toggle="tab" data-target="#assessment-result" type="button"
<button class="nav-link active" id="result-button" data-toggle="tab" data-target="#assessment-result" type="button"
role="tab" aria-controls="home" aria-selected="true">
Result
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-toggle="tab" data-target="#info" type="button" role="tab"
<button class="nav-link" id="references-button" data-toggle="tab" data-target="#info" type="button" role="tab"
aria-controls="home" aria-selected="false">
References
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-toggle="tab" data-target="#guide" type="button" role="tab"
<button id="start-user-guide" class="nav-link" data-toggle="tab" data-target="#guide" type="button" role="tab"
aria-controls="home" aria-selected="false">
User guide
</button>
Expand Down Expand Up @@ -96,6 +96,7 @@ <h4>Treatments</h4>
{% endblock %}
{% block script %}
<div>
{% include "guided-tour.html" %}
<script type="text/javascript">
document.addEventListener(`keypress`, evt => {
const form = evt.target.closest(`#configurator`);
Expand Down
26 changes: 26 additions & 0 deletions qmra/risk_assessment/templates/assessment-result-export.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
{% include "head.html" %}
<body>
<div class="container">
{% include "assessment-result.html" with results=results infection_risk=infection_risk risk_plot_data=risk_plot_data daly_plot_data=daly_plot_data %}
</div>
</body>
<style type="text/css">

.max-risk {
background: #FFECF4;
color: #FF0532;
}

.min-risk {
background: #FFDDB5;
color: #ED5500;
}

.none-risk {
background: #E2FBAC;
color: #088B3C;
}
</style>
</html>
15 changes: 15 additions & 0 deletions qmra/risk_assessment/templates/assessment-result.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,31 @@ <h4 class="mt-3 mx-5 text-center">Result</h4>
</div>
{% endif %}
</div>
{% if risk_plot is not None %}
<div id="hero-graph" class="mb-3">
{% autoescape off %}
{{ risk_plot }}
{% endautoescape %}
</div>
{% endif %}
{% if daly_plot is not None %}
<div id="hero-graph" class="mb-3">
{% autoescape off %}
{{ daly_plot }}
{% endautoescape %}
</div>
{% endif %}
{% if risk_plot_data is not None %}
<div class="mb-3">
<img src="data:image/png;charset=utf-8;base64, {{risk_plot_data}}">
</div>
{% endif %}
{% if daly_plot_data is not None %}
<div class="mb-3">
<img src="data:image/png;charset=utf-8;base64, {{daly_plot_data}}">
</div>
{% endif %}

<!-- <h4 style="text-align: center">Risk per pathogen</h4>
<div class="w-50 m-auto">
<table class="table">
Expand Down
57 changes: 57 additions & 0 deletions qmra/risk_assessment/templates/guided-tour.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script type="text/javascript">
const steps = [
{
title: "Name and description",
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.",
target: "#name-and-description",
order: 0
},
{
title: "Exposure",
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.",
target: "#exposure-form-fieldset",
order: 1
},
{
title: "Source water and inflows",
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.",
target: "#inflow-content",
order: 2
},
{
title: "Treatments",
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.",
target: "#treatment-content",
order: 3
},
{
title: "Results",
content: "Clicking on this tab will allow you to display the result of your risk assessment.",
target: "#result-button",
order: 4
},
{
title: "Save",
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.",
target: "#configurator-commands",
order: 5
},
{
title: "References",
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.",
target: "#references-button",
order: 6
},
];
document.addEventListener("DOMContentLoaded", () => {
const tg = new tourguide.TourGuideClient({
steps: steps
})
tg.onAfterExit(() => {
document.querySelector("#result-button").click();
})
document.querySelector("#start-user-guide").addEventListener("click", ev => {
tg.start();
})
})
</script>
4 changes: 2 additions & 2 deletions qmra/risk_assessment/templates/inflows-form-js.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
const y = parseFloat(x.toFixed(4));
var step = "any";
if (y < 1 && y > 0){
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10))-1)+"1";
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10)))+"1";
}
elem.step = step;
}},
Expand All @@ -22,7 +22,7 @@
const y = parseFloat(x.toFixed(4));
var step = "any";
if (y < 1 && y > 0){
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10))-1)+"1";
step = "."+"0".repeat(-Math.floor(Math.log(y)/Math.log(10)))+"1";
}
elem.step = step;
}}
Expand Down
1 change: 1 addition & 0 deletions qmra/risk_assessment/templates/risk-assessment-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ <h4 class="media-heading text-truncate kwb_headline">
</div>
</div>
<div style="height: 21px; display: flex; margin-left: auto; line-height: 0 !important">
<a class="mr-1" href="{% url 'assessment-export' assessment.id %}" style="line-height: 1.25;">ZIP</a>
<label class="text-muted small form-inline mb-0 mr-1">compare </label>
<input type="checkbox" class="select-assessment-btn mr-2" value="{{assessment.id}}"
style="width: 20px; height: 20px">
Expand Down
55 changes: 55 additions & 0 deletions qmra/risk_assessment/tests/test_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import io

from assertpy import assert_that
from django.test import TestCase
import pandas as pd

from qmra.risk_assessment import exports
from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment, DefaultTreatments
from qmra.risk_assessment.risk import assess_risk
from qmra.user.models import User


class TestResultExport(TestCase):

def test_that(self):
given_user = User.objects.create_user("test-user2", "[email protected]", "password")
given_user.save()
given_ra = RiskAssessment.objects.create(
user=given_user,
events_per_year=1,
volume_per_event=2,
)
given_ra.save()
given_inflows = [
Inflow.objects.create(
risk_assessment=given_ra,
pathogen="Rotavirus",
min=0.1, max=0.2
),
Inflow.objects.create(
risk_assessment=given_ra,
pathogen="Campylobacter jejuni",
min=0.1, max=0.2
),
Inflow(
risk_assessment=given_ra,
pathogen="Cryptosporidium parvum",
min=0.1, max=0.2
),
]
given_treatments = [
Treatment.from_default(t, given_ra)
for t in list(DefaultTreatments.data.values())[:3]
]
given_ra.inflows.set(given_inflows, bulk=False)
given_ra.treatments.set(given_treatments, bulk=False)

results = assess_risk(given_ra, given_inflows, given_treatments)
given_ra = RiskAssessment.objects.get(pk=given_ra.id)
with io.BytesIO() as buffer:
exports.risk_assessment_as_zip(buffer, given_ra)
buffer.seek(0)
with open("test.zip", "wb") as f:
f.write(buffer.getvalue())
assert_that(buffer).is_not_none()
5 changes: 5 additions & 0 deletions qmra/risk_assessment/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
views.risk_assessment_result,
name="assessment-result",
),
path(
"assessment/<uuid:risk_assessment_id>/export",
views.export_risk_assessment,
name="assessment-export",
),
path(
"assessment/results",
views.risk_assessment_result,
Expand Down
Loading

0 comments on commit ab1d26d

Please sign in to comment.