Automatic report regeneration
This commit is contained in:
parent
1ff9a4d352
commit
2997607e60
11 changed files with 186 additions and 127 deletions
|
|
@ -33,7 +33,7 @@ A room number is included, if you do not wish to use a formal room number any re
|
||||||
|
|
||||||
### Room Data
|
### Room Data
|
||||||
|
|
||||||
Please enter either the room volume (in m<sup>3</sup>) or both the floor area (m<sup>2</sup>) and the room height (m).
|
Please enter either the room volume (in m³) or both the floor area (m²) and the room height (m).
|
||||||
This information is available via GIS Portal (https://gis.cern.ch/gisportal/).
|
This information is available via GIS Portal (https://gis.cern.ch/gisportal/).
|
||||||
|
|
||||||
### Ventilation type
|
### Ventilation type
|
||||||
|
|
@ -99,7 +99,6 @@ As an example, for a shared office with 4 people, where one person is infected,
|
||||||
There are a few predefined activities in the tool at present.
|
There are a few predefined activities in the tool at present.
|
||||||
|
|
||||||
**Office ** = All persons seated, talking occasionally (1/3rd of the time, with normal breathing the other 2/3rds of the time). Everyone (exposed and infected occupants) is treated the same in this model.
|
**Office ** = All persons seated, talking occasionally (1/3rd of the time, with normal breathing the other 2/3rds of the time). Everyone (exposed and infected occupants) is treated the same in this model.
|
||||||
|
|
||||||
**Meeting** = All persons seated, having a conversation (approximately each occupant is 1/N % of the time talking, where N is the number of occupants). Everyone (exposed and infected occupants) is treated the same in this model.
|
**Meeting** = All persons seated, having a conversation (approximately each occupant is 1/N % of the time talking, where N is the number of occupants). Everyone (exposed and infected occupants) is treated the same in this model.
|
||||||
|
|
||||||
**Library** = All persons seated, breathing only (not talking), all the time.
|
**Library** = All persons seated, breathing only (not talking), all the time.
|
||||||
|
|
@ -191,9 +190,8 @@ It contains a summary of all the input data, which will allow the simulation to
|
||||||
|
|
||||||
This part of the report shows the ``P(i)`` or probability of one exposed person getting infected.
|
This part of the report shows the ``P(i)`` or probability of one exposed person getting infected.
|
||||||
It is estimated based on the emission rate of virus into the simulated volume, and the amount which is inhaled by exposed individuals.
|
It is estimated based on the emission rate of virus into the simulated volume, and the amount which is inhaled by exposed individuals.
|
||||||
This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amount of virus each day, the cumulative probability of infection is ``(1-(1-P(i))^5)```.
|
This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amount of virus each day, the cumulative probability of infection is ``(1-(1-P(i))^5)``.
|
||||||
If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile.
|
If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile.
|
||||||
|
|
||||||
The ``expected number of new cases`` for the simulation is calculated based on the probability of infection, multiplied by the number of exposed occupants.
|
The ``expected number of new cases`` for the simulation is calculated based on the probability of infection, multiplied by the number of exposed occupants.
|
||||||
|
|
||||||
### Exposure graph
|
### Exposure graph
|
||||||
|
|
@ -207,6 +205,13 @@ It is determined by:
|
||||||
* Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.
|
* Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.
|
||||||
* A HEPA filter removes infectious quanta from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).
|
* A HEPA filter removes infectious quanta from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).
|
||||||
|
|
||||||
|
### QR code
|
||||||
|
|
||||||
|
At the end of the report you can find a unique QR code / hyperlink for this report. This provides an automatic way to review the calculator form with the corresponding specified parameters.
|
||||||
|
This allows for:
|
||||||
|
* sharing reports by either scanning or clicking on the QR code to obtain a shareable link.
|
||||||
|
* easily regenerating reports with any new versions of the CARA model released in the future.
|
||||||
|
|
||||||
# Conclusion
|
# Conclusion
|
||||||
|
|
||||||
This tool provides informative comparisons for COVID-19 (long-range) airborne risk only - see Disclaimer
|
This tool provides informative comparisons for COVID-19 (long-range) airborne risk only - see Disclaimer
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ class ConcentrationModel(BaseRequestHandler):
|
||||||
self.finish(json.dumps(response_json))
|
self.finish(json.dumps(response_json))
|
||||||
return
|
return
|
||||||
|
|
||||||
report = build_report(form.build_model(), form)
|
base_url = self.request.protocol + "://" + self.request.host
|
||||||
|
report = build_report(base_url, form.build_model(), form)
|
||||||
self.finish(report)
|
self.finish(report)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,7 +64,8 @@ class StaticModel(BaseRequestHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
form = model_generator.FormData.from_dict(model_generator.baseline_raw_form_data())
|
form = model_generator.FormData.from_dict(model_generator.baseline_raw_form_data())
|
||||||
model = form.build_model()
|
model = form.build_model()
|
||||||
report = build_report(model, form)
|
base_url = self.request.protocol + "://" + self.request.host
|
||||||
|
report = build_report(base_url, model, form)
|
||||||
self.finish(report)
|
self.finish(report)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,66 +82,29 @@ class FormData:
|
||||||
if value == "":
|
if value == "":
|
||||||
form_data[key] = "0"
|
form_data[key] = "0"
|
||||||
|
|
||||||
time_attributes = [
|
for attr_name in BOOLEAN_ATTRIBUTES:
|
||||||
'exposed_lunch_start', 'exposed_lunch_finish', 'exposed_start', 'exposed_finish',
|
form_data[attr_name] = form_data[attr_name] == '1'
|
||||||
'infected_lunch_start', 'infected_lunch_finish', 'infected_start', 'infected_finish',
|
for attr_name in FLOAT_ATTRIBUTES:
|
||||||
]
|
form_data[attr_name] = float(form_data[attr_name])
|
||||||
for attr_name in time_attributes:
|
for attr_name in INT_ATTRIBUTES:
|
||||||
|
form_data[attr_name] = int(form_data[attr_name])
|
||||||
|
for attr_name in TIME_ATTRIBUTES:
|
||||||
form_data[attr_name] = time_string_to_minutes(form_data[attr_name])
|
form_data[attr_name] = time_string_to_minutes(form_data[attr_name])
|
||||||
|
|
||||||
boolean_attributes = [
|
form_data.pop('_xsrf', None)
|
||||||
'hepa_option', 'exposed_lunch_option', 'infected_lunch_option', 'infected_dont_have_breaks_with_exposed',
|
instance = cls(**form_data)
|
||||||
]
|
|
||||||
for attr_name in boolean_attributes:
|
|
||||||
form_data[attr_name] = form_data[attr_name] == '1'
|
|
||||||
|
|
||||||
instance = cls(
|
|
||||||
activity_type=form_data['activity_type'],
|
|
||||||
air_changes=float(form_data['air_changes']),
|
|
||||||
air_supply=float(form_data['air_supply']),
|
|
||||||
ceiling_height=float(form_data['ceiling_height']),
|
|
||||||
exposed_coffee_break_option=form_data['exposed_coffee_break_option'],
|
|
||||||
exposed_coffee_duration=int(form_data['exposed_coffee_duration']),
|
|
||||||
exposed_finish=form_data['exposed_finish'],
|
|
||||||
exposed_lunch_finish=form_data['exposed_lunch_finish'],
|
|
||||||
exposed_lunch_option=form_data['exposed_lunch_option'],
|
|
||||||
exposed_lunch_start=form_data['exposed_lunch_start'],
|
|
||||||
exposed_start=form_data['exposed_start'],
|
|
||||||
floor_area=float(form_data['floor_area']),
|
|
||||||
hepa_amount=float(form_data['hepa_amount']),
|
|
||||||
hepa_option=form_data['hepa_option'],
|
|
||||||
infected_coffee_break_option=form_data['infected_coffee_break_option'],
|
|
||||||
infected_coffee_duration=int(form_data['infected_coffee_duration']),
|
|
||||||
infected_dont_have_breaks_with_exposed=form_data['infected_dont_have_breaks_with_exposed'],
|
|
||||||
infected_finish=form_data['infected_finish'],
|
|
||||||
infected_lunch_finish=form_data['infected_lunch_finish'],
|
|
||||||
infected_lunch_option=form_data['infected_lunch_option'],
|
|
||||||
infected_lunch_start=form_data['infected_lunch_start'],
|
|
||||||
infected_people=int(form_data['infected_people']),
|
|
||||||
infected_start=form_data['infected_start'],
|
|
||||||
mask_type=form_data['mask_type'],
|
|
||||||
mask_wearing_option=form_data['mask_wearing_option'],
|
|
||||||
mechanical_ventilation_type=form_data['mechanical_ventilation_type'],
|
|
||||||
model_version=form_data['model_version'],
|
|
||||||
opening_distance=float(form_data['opening_distance']),
|
|
||||||
event_month=form_data['event_month'],
|
|
||||||
room_number=form_data['room_number'],
|
|
||||||
room_volume=float(form_data['room_volume']),
|
|
||||||
simulation_name=form_data['simulation_name'],
|
|
||||||
total_people=int(form_data['total_people']),
|
|
||||||
ventilation_type=form_data['ventilation_type'],
|
|
||||||
volume_type=form_data['volume_type'],
|
|
||||||
windows_duration=float(form_data['windows_duration']),
|
|
||||||
windows_frequency=float(form_data['windows_frequency']),
|
|
||||||
window_height=float(form_data['window_height']),
|
|
||||||
window_type=form_data['window_type'],
|
|
||||||
window_width=float(form_data['window_width']),
|
|
||||||
windows_number=int(form_data['windows_number']),
|
|
||||||
window_opening_regime=form_data['window_opening_regime'],
|
|
||||||
)
|
|
||||||
instance.validate()
|
instance.validate()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_dict(self, form: "FormData") -> dict:
|
||||||
|
form_dict = form.__dict__.copy()
|
||||||
|
for attr_name in TIME_ATTRIBUTES:
|
||||||
|
form_dict[attr_name] = time_minutes_to_string(form_dict[attr_name])
|
||||||
|
for attr_name in BOOLEAN_ATTRIBUTES:
|
||||||
|
form_dict[attr_name] = form_dict[attr_name] & 1
|
||||||
|
return form_dict
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
# Validate time intervals selected by user
|
# Validate time intervals selected by user
|
||||||
time_intervals = [
|
time_intervals = [
|
||||||
|
|
@ -604,6 +567,12 @@ WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'}
|
||||||
|
|
||||||
COFFEE_OPTIONS_INT = {'coffee_break_0':0, 'coffee_break_1':1, 'coffee_break_2':2, 'coffee_break_4':4}
|
COFFEE_OPTIONS_INT = {'coffee_break_0':0, 'coffee_break_1':1, 'coffee_break_2':2, 'coffee_break_4':4}
|
||||||
|
|
||||||
|
BOOLEAN_ATTRIBUTES = {'hepa_option', 'exposed_lunch_option', 'infected_lunch_option', 'infected_dont_have_breaks_with_exposed'}
|
||||||
|
FLOAT_ATTRIBUTES = {'air_changes', 'air_supply', 'ceiling_height', 'floor_area', 'hepa_amount', 'opening_distance',
|
||||||
|
'room_volume', 'windows_duration', 'windows_frequency', 'window_height', 'window_width'}
|
||||||
|
INT_ATTRIBUTES = {'exposed_coffee_duration', 'infected_coffee_duration', 'infected_people', 'total_people', 'windows_number'}
|
||||||
|
TIME_ATTRIBUTES = {'exposed_lunch_start', 'exposed_lunch_finish', 'exposed_start', 'exposed_finish',
|
||||||
|
'infected_lunch_start', 'infected_lunch_finish', 'infected_start', 'infected_finish'}
|
||||||
|
|
||||||
def time_string_to_minutes(time: str) -> minutes_since_midnight:
|
def time_string_to_minutes(time: str) -> minutes_since_midnight:
|
||||||
"""
|
"""
|
||||||
|
|
@ -612,3 +581,11 @@ def time_string_to_minutes(time: str) -> minutes_since_midnight:
|
||||||
:return: The number of minutes between 'time' and 00:00
|
:return: The number of minutes between 'time' and 00:00
|
||||||
"""
|
"""
|
||||||
return minutes_since_midnight(60 * int(time[:2]) + int(time[3:]))
|
return minutes_since_midnight(60 * int(time[:2]) + int(time[3:]))
|
||||||
|
|
||||||
|
def time_minutes_to_string(time: int) -> str:
|
||||||
|
"""
|
||||||
|
Converts time from an integer number of minutes after 00:00 to string-format
|
||||||
|
:param time: The number of minutes between 'time' and 00:00
|
||||||
|
:return: A string of the form "HH:MM" representing a time of day
|
||||||
|
"""
|
||||||
|
return "{0:0=2d}".format(int(time/60)) + ":" + "{0:0=2d}".format(time%60)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import qrcode
|
||||||
|
import urllib
|
||||||
import jinja2
|
import jinja2
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('agg')
|
matplotlib.use('agg')
|
||||||
|
|
@ -61,16 +63,46 @@ def calculate_report_data(model: models.ExposureModel):
|
||||||
"emission_rate": er,
|
"emission_rate": er,
|
||||||
"exposed_occupants": exposed_occupants,
|
"exposed_occupants": exposed_occupants,
|
||||||
"expected_new_cases": expected_new_cases,
|
"expected_new_cases": expected_new_cases,
|
||||||
"scenario_plot_src": embed_figure(plot(times, concentrations, model)),
|
"scenario_plot_src": img2base64(_figure2bytes(plot(times, concentrations, model))),
|
||||||
"repeated_events": repeated_events,
|
"repeated_events": repeated_events,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def embed_figure(figure) -> str:
|
def generate_qr_code(prefix, form: FormData):
|
||||||
# Draw the scenario graph.
|
form_dict = FormData.to_dict(form)
|
||||||
img_data = io.BytesIO()
|
url = prefix + "?" + urllib.parse.urlencode(form_dict)
|
||||||
|
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||||
|
box_size=10,
|
||||||
|
border=4,
|
||||||
|
)
|
||||||
|
qr.add_data(url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white").convert('RGB')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'image': img2base64(_img2bytes(img)),
|
||||||
|
'link': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _img2bytes(figure):
|
||||||
|
# Draw the image
|
||||||
|
img_data = io.BytesIO()
|
||||||
|
figure.save(img_data, format='png', bbox_inches="tight")
|
||||||
|
return img_data
|
||||||
|
|
||||||
|
|
||||||
|
def _figure2bytes(figure):
|
||||||
|
# Draw the image
|
||||||
|
img_data = io.BytesIO()
|
||||||
figure.savefig(img_data, format='png', bbox_inches="tight")
|
figure.savefig(img_data, format='png', bbox_inches="tight")
|
||||||
|
return img_data
|
||||||
|
|
||||||
|
|
||||||
|
def img2base64(img_data) -> str:
|
||||||
plt.close()
|
plt.close()
|
||||||
img_data.seek(0)
|
img_data.seek(0)
|
||||||
pic_hash = base64.b64encode(img_data.read()).decode('ascii')
|
pic_hash = base64.b64encode(img_data.read()).decode('ascii')
|
||||||
|
|
@ -220,12 +252,12 @@ def comparison_report(scenarios: typing.Dict[str, models.ExposureModel]):
|
||||||
'expected_new_cases': model.expected_new_cases(),
|
'expected_new_cases': model.expected_new_cases(),
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'plot': embed_figure(comparison_plot(scenarios)),
|
'plot': img2base64(_figure2bytes(comparison_plot(scenarios))),
|
||||||
'stats': statistics,
|
'stats': statistics,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_report(model: models.ExposureModel, form: FormData):
|
def build_report(base_url: str, model: models.ExposureModel, form: FormData):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
time = now.strftime("%d/%m/%Y %H:%M:%S")
|
time = now.strftime("%d/%m/%Y %H:%M:%S")
|
||||||
request = {"the": "form", "request": "data"}
|
request = {"the": "form", "request": "data"}
|
||||||
|
|
@ -240,6 +272,7 @@ def build_report(model: models.ExposureModel, form: FormData):
|
||||||
context.update(calculate_report_data(model))
|
context.update(calculate_report_data(model))
|
||||||
alternative_scenarios = manufacture_alternative_scenarios(form)
|
alternative_scenarios = manufacture_alternative_scenarios(form)
|
||||||
context['alternative_scenarios'] = comparison_report(alternative_scenarios)
|
context['alternative_scenarios'] = comparison_report(alternative_scenarios)
|
||||||
|
context['qr_code'] = generate_qr_code(f'{base_url}/calculator', form)
|
||||||
|
|
||||||
cara_templates = Path(__file__).parent.parent / "templates"
|
cara_templates = Path(__file__).parent.parent / "templates"
|
||||||
calculator_templates = Path(__file__).parent / "templates"
|
calculator_templates = Path(__file__).parent / "templates"
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ function on_ventilation_type_change() {
|
||||||
unrequire_fields(this);
|
unrequire_fields(this);
|
||||||
|
|
||||||
//Clear invalid inputs for this newly hidden child element
|
//Clear invalid inputs for this newly hidden child element
|
||||||
removeInvalid(getChildElement($(this)).find('input').not('input[type=radio]').attr('id'));
|
removeInvalid("#"+getChildElement($(this)).find('input').not('input[type=radio]').attr('id'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -456,6 +456,32 @@ function parseTimeToMins(cTime) {
|
||||||
|
|
||||||
/* -------On Load------- */
|
/* -------On Load------- */
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
//Pre-fill form with known values
|
||||||
|
(new URL(decodeURIComponent(window.location.href))).searchParams.forEach((value, name) => {
|
||||||
|
|
||||||
|
//If element exists
|
||||||
|
if(document.getElementsByName(name).length > 0) {
|
||||||
|
var elemObj = document.getElementsByName(name)[0];
|
||||||
|
|
||||||
|
//Pre-select checked radios
|
||||||
|
if (elemObj.type === 'radio') {
|
||||||
|
if (value !== 'not-applicable') {
|
||||||
|
$('[name="'+name+'"][value="'+value+'"]').prop('checked',true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Pre-select checkboxes
|
||||||
|
else if (elemObj.type === 'checkbox') {
|
||||||
|
elemObj.checked = (value==1);
|
||||||
|
}
|
||||||
|
//Ignore 0 (default) values from server side
|
||||||
|
else if (!(elemObj.classList.contains("non_zero") || elemObj.classList.contains("remove_zero")) || (value != "0.0" && value != "0")) {
|
||||||
|
elemObj.value = value;
|
||||||
|
validateValue(elemObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// When the document is ready, deal with the fact that we may be here
|
// When the document is ready, deal with the fact that we may be here
|
||||||
// as a result of a forward/back browser action. If that is the case, update
|
// as a result of a forward/back browser action. If that is the case, update
|
||||||
// the visibility of some of our inputs.
|
// the visibility of some of our inputs.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "layout.html.j2" %}
|
{% extends "layout.html.j2" %}
|
||||||
|
|
||||||
{% set MODEL_VERSION="v1.4.0" %}
|
{% set MODEL_VERSION="v1.4.1" %}
|
||||||
{% set DEBUG=False %}
|
{% set DEBUG=False %}
|
||||||
{% set active_page="calculator/" %}
|
{% set active_page="calculator/" %}
|
||||||
|
|
||||||
|
|
@ -92,8 +92,8 @@
|
||||||
<label for="windows_open_permanently">Permanently</label><br>
|
<label for="windows_open_permanently">Permanently</label><br>
|
||||||
<span class="tabbed"><input type="radio" id="windows_open_periodically" name="window_opening_regime" value="windows_open_periodically" onclick="require_fields(this)"></span>
|
<span class="tabbed"><input type="radio" id="windows_open_periodically" name="window_opening_regime" value="windows_open_periodically" onclick="require_fields(this)"></span>
|
||||||
<label for="windows_open_periodically">Periodically:</label>
|
<label for="windows_open_periodically">Periodically:</label>
|
||||||
<input type="number" step="any" id="windows_duration" class="disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#windows_open_periodically"> /
|
<input type="number" step="any" id="windows_duration" class="non_zero disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#windows_open_periodically"> /
|
||||||
<input type="number" step="any" id="windows_frequency" class="disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#windows_open_periodically">
|
<input type="number" step="any" id="windows_frequency" class="non_zero disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#windows_open_periodically">
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,6 @@ In this scenario, the estimated probability of one exposed occupant getting infe
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<div style="clear: both;">
|
<div style="clear: both;">
|
||||||
|
|
||||||
|
|
@ -287,11 +286,15 @@ In this scenario, the estimated probability of one exposed occupant getting infe
|
||||||
<li>Limitation - the personal doses have to be kept below the legal limits (in this case the CERN exposure limits)</li>
|
<li>Limitation - the personal doses have to be kept below the legal limits (in this case the CERN exposure limits)</li>
|
||||||
<li>Optimisation - the personal doses and collective doses have to be kept as low as reasonably achievable (ALARA).</li>
|
<li>Optimisation - the personal doses and collective doses have to be kept as low as reasonably achievable (ALARA).</li>
|
||||||
</ol>
|
</ol>
|
||||||
For more information, please refer to <a href="https://cds.cern.ch/record/1533023/files/CERN-2013-001-p415.pdf"> this document from CERN HSE </a> and <a href="https://www.cdc.gov/nceh/radiation/safety.html#:~:text=ALARA%20stands%20for%20%E2%80%9Cas%20low,time%2C%20distance%2C%20and%20shielding."> this publication from the CDC.</a> <br>
|
For more information, please refer to <a href="https://cds.cern.ch/record/1533023/files/CERN-2013-001-p415.pdf"> this document from CERN HSE </a> and <a href="https://www.cdc.gov/nceh/radiation/safety.html#:~:text=ALARA%20stands%20for%20%E2%80%9Cas%20low,time%2C%20distance%2C%20and%20shielding."> this publication from the CDC.</a>
|
||||||
|
<br><br>
|
||||||
<br>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{{ qr_code.link }}"><img style="width:250pt;" id="qr_code" src="{{ qr_code.image }}"/></a>
|
||||||
|
<i style="position:absolute; margin-top:3.5cm">Click the QR code to regenerate the report and get a shareable link.<br>Alternatively, scan to regenerate the report.<br> Mobile-friendly app coming soon!</i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br><br><br>
|
<br><br><br>
|
||||||
<div style="border: 2px solid black; padding: 15px;">
|
<div style="border: 2px solid black; padding: 15px;">
|
||||||
<p class="image"> <img <img align="middle" src="/calculator/static/images/disclaimer.jpg" width="40" height="40"><b>Disclaimer:</b><br><br></p>
|
<p class="image"> <img <img align="middle" src="/calculator/static/images/disclaimer.jpg" width="40" height="40"><b>Disclaimer:</b><br><br></p>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ def baseline_form(baseline_form_data):
|
||||||
def test_generate_report(baseline_form):
|
def test_generate_report(baseline_form):
|
||||||
model = baseline_form.build_model()
|
model = baseline_form.build_model()
|
||||||
|
|
||||||
report = report_generator.build_report(model, baseline_form)
|
report = report_generator.build_report("", model, baseline_form)
|
||||||
assert report != ""
|
assert report != ""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,64 +3,73 @@
|
||||||
# pip freeze | grep -v cara >> requirements.txt
|
# pip freeze | grep -v cara >> requirements.txt
|
||||||
|
|
||||||
.[app]
|
.[app]
|
||||||
|
anyio==2.1.0
|
||||||
|
appnope==0.1.2
|
||||||
argon2-cffi==20.1.0
|
argon2-cffi==20.1.0
|
||||||
async-generator==1.10
|
async-generator==1.10
|
||||||
attrs==20.2.0
|
attrs==20.3.0
|
||||||
backcall==0.2.0
|
backcall==0.2.0
|
||||||
bleach==3.2.1
|
bleach==3.3.0
|
||||||
certifi==2020.6.20
|
certifi==2020.12.5
|
||||||
cffi==1.14.3
|
cffi==1.14.5
|
||||||
|
contextvars==2.4
|
||||||
cycler==0.10.0
|
cycler==0.10.0
|
||||||
dataclasses==0.7
|
dataclasses==0.8
|
||||||
decorator==4.4.2
|
decorator==4.4.2
|
||||||
defusedxml==0.6.0
|
defusedxml==0.6.0
|
||||||
entrypoints==0.3
|
entrypoints==0.3
|
||||||
importlib-metadata==2.0.0
|
idna==3.1
|
||||||
ipykernel==5.3.4
|
immutables==0.15
|
||||||
ipympl==0.5.8
|
importlib-metadata==3.5.0
|
||||||
|
ipykernel==5.5.0
|
||||||
|
ipympl==0.6.3
|
||||||
ipython==7.16.1
|
ipython==7.16.1
|
||||||
ipython-genutils==0.2.0
|
ipython-genutils==0.2.0
|
||||||
ipywidgets==7.5.1
|
ipywidgets==7.6.3
|
||||||
jedi==0.17.2
|
jedi==0.18.0
|
||||||
Jinja2==2.11.2
|
Jinja2==2.11.3
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
jupyter-client==6.1.7
|
jupyter-client==6.1.11
|
||||||
jupyter-core==4.6.3
|
jupyter-core==4.7.1
|
||||||
jupyter-server==1.0.5
|
jupyter-server==1.4.1
|
||||||
jupyterlab-pygments==0.1.2
|
jupyterlab-pygments==0.1.2
|
||||||
kiwisolver==1.2.0
|
jupyterlab-widgets==1.0.0
|
||||||
|
kiwisolver==1.3.1
|
||||||
MarkupSafe==1.1.1
|
MarkupSafe==1.1.1
|
||||||
matplotlib==3.3.2
|
matplotlib==3.3.4
|
||||||
mistune==0.8.4
|
mistune==0.8.4
|
||||||
nbclient==0.5.1
|
nbclient==0.5.2
|
||||||
nbconvert==6.0.7
|
nbconvert==6.0.7
|
||||||
nbformat==5.0.8
|
nbformat==5.1.2
|
||||||
nest-asyncio==1.4.1
|
nest-asyncio==1.5.1
|
||||||
notebook==6.1.4
|
notebook==6.2.0
|
||||||
numpy==1.19.2
|
numpy==1.19.5
|
||||||
packaging==20.4
|
packaging==20.9
|
||||||
pandocfilters==1.4.2
|
pandocfilters==1.4.3
|
||||||
parso==0.7.1
|
parso==0.8.1
|
||||||
pexpect==4.8.0
|
pexpect==4.8.0
|
||||||
pickleshare==0.7.5
|
pickleshare==0.7.5
|
||||||
Pillow==8.0.0
|
Pillow==8.1.0
|
||||||
prometheus-client==0.8.0
|
prometheus-client==0.9.0
|
||||||
prompt-toolkit==3.0.8
|
prompt-toolkit==3.0.16
|
||||||
ptyprocess==0.6.0
|
ptyprocess==0.7.0
|
||||||
pycparser==2.20
|
pycparser==2.20
|
||||||
Pygments==2.7.1
|
Pygments==2.8.0
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
pyrsistent==0.17.3
|
pyrsistent==0.17.3
|
||||||
python-dateutil==2.8.1
|
python-dateutil==2.8.1
|
||||||
pyzmq==19.0.2
|
pyzmq==22.0.3
|
||||||
|
qrcode==6.1
|
||||||
Send2Trash==1.5.0
|
Send2Trash==1.5.0
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
terminado==0.9.1
|
sniffio==1.2.0
|
||||||
|
terminado==0.9.2
|
||||||
testpath==0.4.4
|
testpath==0.4.4
|
||||||
tornado==6.0.4
|
tornado==6.1
|
||||||
traitlets==4.3.3
|
traitlets==4.3.3
|
||||||
voila==0.2.4
|
typing-extensions==3.7.4.3
|
||||||
|
voila==0.2.7
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
widgetsnbextension==3.5.1
|
widgetsnbextension==3.5.1
|
||||||
zipp==3.3.1
|
zipp==3.4.0
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,6 @@ ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-mistune.*]
|
[mypy-mistune.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-qrcode.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -24,6 +24,7 @@ REQUIREMENTS: dict = {
|
||||||
'matplotlib',
|
'matplotlib',
|
||||||
'mistune',
|
'mistune',
|
||||||
'numpy',
|
'numpy',
|
||||||
|
'qrcode[pil]',
|
||||||
'tornado',
|
'tornado',
|
||||||
'voila >=0.2.4',
|
'voila >=0.2.4',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue