diff --git a/README.md b/README.md
index c7915e86..be1f81f9 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,12 @@ pip install -e . # At the root of the repository
python -m cara.apps.calculator
```
+To run with the CERN theme:
+
+```
+python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern
+```
+
### Running the CARA Expert-App app locally
```
diff --git a/app.sh b/app.sh
index a9fc0e45..1aa80c4a 100755
--- a/app.sh
+++ b/app.sh
@@ -1,6 +1,17 @@
+#!/bin/bash
+
if [[ "$APP_NAME" == "cara-webservice" ]]; then
- echo "Starting the cara webservice"
- python -m cara.apps.calculator --no-debug
+ args=("$@")
+ if [ "$DEBUG" != "true" ] && [[ ! "${args[@]}" =~ "--no-debug" ]]; then
+ args+=("--no-debug")
+ fi
+
+ if [ ! -z "$CARA_THEME" ]; then
+ args+=("--theme=${CARA_THEME}")
+ fi
+
+ echo "Starting the cara webservice with: python -m cara.apps.calculator ${args[@]}"
+ python -m cara.apps.calculator "${args[@]}"
elif [[ "$APP_NAME" == "cara-voila" ]]; then
echo "Starting the voila service"
voila app/ --port=8080 --no-browser --base_url=/voila-server/ --Voila.tornado_settings="{'allow_origin': '*'}"
diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py
index 9690fed7..8779270b 100644
--- a/cara/apps/calculator/__init__.py
+++ b/cara/apps/calculator/__init__.py
@@ -5,6 +5,7 @@ import json
import os
from pathlib import Path
import traceback
+import typing
import uuid
import zlib
@@ -12,13 +13,13 @@ import jinja2
from tornado.web import Application, RequestHandler, StaticFileHandler
from . import model_generator
-from .report_generator import build_report
+from .report_generator import ReportGenerator
from .user import AuthenticatedUser, AnonymousUser
# The calculator version is based on a combination of the model version and the
# semantic version of the calculator itself. The version uses the terms
-# "{MAJOR}.{MINOR}.{PATCH}" to describe the 3 distinct numbers constituing a version.
+# "{MAJOR}.{MINOR}.{PATCH}" to describe the 3 distinct numbers constituting a version.
# Effectively, if the model increases its MAJOR version then so too should this
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
@@ -98,16 +99,17 @@ class ConcentrationModel(BaseRequestHandler):
return
base_url = self.request.protocol + "://" + self.request.host
- report = build_report(base_url, form.build_model(), form)
+ report_generator = self.settings['report_generator']
+ report = report_generator.build_report(base_url, form)
self.finish(report)
class StaticModel(BaseRequestHandler):
def get(self):
form = model_generator.FormData.from_dict(model_generator.baseline_raw_form_data())
- model = form.build_model()
base_url = self.request.protocol + "://" + self.request.host
- report = build_report(base_url, model, form)
+ report_generator = self.settings['report_generator']
+ report = report_generator.build_report(base_url, form)
self.finish(report)
@@ -153,10 +155,14 @@ class ReadmeHandler(BaseRequestHandler):
self.finish(readme)
-def make_app(debug=False, prefix='/calculator'):
+def make_app(
+ debug: bool = False,
+ prefix: str = '/calculator',
+ theme_dir: typing.Optional[Path] = None,
+) -> Application:
static_dir = Path(__file__).absolute().parent.parent / 'static'
calculator_static_dir = Path(__file__).absolute().parent / 'static'
- urls = [
+ urls: typing.Any = [
(r'/?', LandingPage),
(r'/_c/(.*)', CompressedCalculatorFormInputs),
(r'/static/(.*)', StaticFileHandler, {'path': static_dir}),
@@ -169,8 +175,12 @@ def make_app(debug=False, prefix='/calculator'):
cara_templates = Path(__file__).parent.parent / "templates"
calculator_templates = Path(__file__).parent / "templates"
+ templates_directories = [cara_templates, calculator_templates]
+ if theme_dir:
+ templates_directories.insert(0, theme_dir / 'templates')
+ loader = jinja2.FileSystemLoader([str(path) for path in templates_directories])
template_environment = jinja2.Environment(
- loader=jinja2.FileSystemLoader([cara_templates, calculator_templates]),
+ loader=loader,
)
return Application(
@@ -178,6 +188,7 @@ def make_app(debug=False, prefix='/calculator'):
debug=debug,
template_environment=template_environment,
default_handler_class=Missing404Handler,
+ report_generator=ReportGenerator(loader),
xsrf_cookies=True,
# COOKIE_SECRET being undefined will result in no login information being
# presented to the user.
diff --git a/cara/apps/calculator/__main__.py b/cara/apps/calculator/__main__.py
index 55c134f7..1b9bcce2 100644
--- a/cara/apps/calculator/__main__.py
+++ b/cara/apps/calculator/__main__.py
@@ -1,4 +1,5 @@
import argparse
+from pathlib import Path
from tornado.ioloop import IOLoop
@@ -10,6 +11,11 @@ def configure_parser(parser):
"--no-debug", help="Don't enable debug mode",
action="store_false",
)
+ parser.add_argument(
+ "--theme",
+ help="A directory containing extensions for templates and static data",
+ default=None,
+ )
return parser
@@ -17,7 +23,12 @@ def main():
parser = argparse.ArgumentParser()
configure_parser(parser)
args = parser.parse_args()
- app = make_app(debug=args.no_debug)
+ theme_dir = args.theme
+ if theme_dir is not None:
+ theme_dir = Path(theme_dir).absolute()
+ assert theme_dir.exists()
+ assert (theme_dir / 'templates').exists()
+ app = make_app(debug=args.no_debug, theme_dir=theme_dir)
app.listen(8080)
IOLoop.instance().start()
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 8effd90c..8e3691f5 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -2,7 +2,6 @@ import base64
import dataclasses
from datetime import datetime
import io
-from pathlib import Path
import typing
import zlib
@@ -137,7 +136,7 @@ def plot(times, concentrations, model: models.ExposureModel):
for i, (presence_start, presence_finish) in enumerate(model.exposed.presence.boundaries()):
plt.fill_between(
times, concentrations, 0,
- where=(np.array(times)>presence_start) & (np.array(times) Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }} Applicable rules:
- Report
-
-
- Please ensure that this scenario conforms to current CERN HSE rules (minimum ventilation requirements, mask wearing and the maximum number of people permitted in a space).
- The results of this simulation are colour coded according to the risk values authorized at CERN (approved in December 2020):
-
-
Simulation:
Simulation Name: {{ form.simulation_name }}
Room Number: {{ form.room_number }}
- +Input data:
Virus variant: @@ -78,9 +74,9 @@ Sliding / Side-Hung
Opening distance: {{ form.opening_distance }} m
Windows open: +
Windows open: {% if form.window_opening_regime == "windows_open_periodically" %} - Periodically for {{ form.windows_duration | readable_minutes}} + Periodically for {{ form.windows_duration | readable_minutes}} every {{ form.windows_frequency | readable_minutes}} {% elif form.window_opening_regime == "windows_open_permanently" %} Permanently @@ -192,7 +188,7 @@
Mask wearing:
+Mask wearing:
Masks worn at workstations? {{ 'Yes' if form.mask_wearing_option == "mask_on" else 'No' }}
Results:
- 15) or (expected_new_cases >= 1)) %} - class="red_bkg">Not Acceptable: -{% elif 5 <= prob_inf <= 15 %} - class="yellow_bkg"> Attention: -{% elif prob_inf < 5 %} - class="green_bkg">Acceptable: -{% endif %} - - -In this scenario, the estimated probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }} and the expected number of new cases is {{ expected_new_cases | float_format }}. - -{% if ((prob_inf > 15) or (expected_new_cases >= 1)) %} - This exceeds the authorised risk threshold or number of expected new cases. - The risk level must be reduced before this activity can be undertaken. -{% elif (5 <= prob_inf <= 15) %} - This activity has an elevated level of risk, ALARA principles must be applied to minimise the level of risk before undertaking the activity. - See the footnotes for more details on the ALARA principles. -{% elif (prob_inf < 5) %} - This level of risk is within acceptable parameters, no further actions are required. -{% endif %}
+ {% block report_summary %} + In this scenario, the estimated probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }} and the expected number of new cases is {{ expected_new_cases | float_format }}. + {% endblock report_summary %} +Exposure graph:
Alternative scenarios:
+
+ {% block report_scenarios_summary_table %}
| {{ scenario_name }} | {{ scenario_stats.probability_of_infection | non_zero_percentage }} | {{ scenario_stats.expected_new_cases | float_format }} | @@ -278,6 +256,7 @@ In this scenario, the estimated probability of one exposed occupant getting infe {% endfor %}
- Footnotes for ALARA:
- ALARA stands for As Low As Reasonably Achievable. It can be summarised based on 3 main points:
-
Disclaimer:
The risk assessment tool simulates the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming a homogenous mixture, and estimates the risk of COVID-19 infection therein. The results DO NOT include short-range airborne exposure (where the physical distance plays a factor) nor the other known modes of SARS-CoV-2 transmission. Hence, the output from this model is only valid when the other recommended public health & safety instructions are observed, such as adequate physical distancing, good hand hygiene and other barrier measures.
- The model used is based on scientific publications relating to airborne transmission of infectious diseases, dose-response exposures and aerosol science, as of December 2020 . It can be used to compare the effectiveness of different airborne-related risk mitigation measures.
- Note that this model applies a deterministic approach, i.e., it is assumed at least one person is infected and shedding viruses into the simulated volume. Nonetheless, it is also important to understand that the absolute risk of infection is uncertain, as it will depend on the probability that someone infected attends the event. The model is most useful for comparing the impact and effectiveness of different mitigation measures such as ventilation, filtration, exposure time, physical activity and the size of the room, only considering long-range airborne transmission of COVID-19 in indoor settings.
- This tool is designed to be informative, allowing the user to adapt different settings and model the relative impact on the estimated infection probabilities. The objective is to facilitate targeted decision-making and investment through comparisons, rather than a singular determination of absolute risk. While the SARS-CoV-2 virus is in circulation among the population, the notion of 'zero risk' or a 'completely safe scenario' does not exist. Each event modelled is unique and the results generated therein are only as accurate as the inputs and assumptions.
- CARA is made available for internal CERN use only. It is intended for Members of Personnel with roles related to Supervision, Health & Safety or Space Management, in order to simulate the concerned workplaces on CERN sites. For use outside of this scope, it has to be performed under a pre-defined framework and licence agreement issued by CERN Knowledge Transfer (kt@cern.ch).