From 7a62a04839567390d146975caa1a2c0ba569aa83 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 17 May 2023 17:25:48 +0200 Subject: [PATCH 01/61] added CO2 data class --- caimira/models.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/caimira/models.py b/caimira/models.py index 3d8d5a3f..a9e1c892 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -38,6 +38,7 @@ import numpy as np from scipy.interpolate import interp1d import scipy.stats as sct +from scipy.optimize import minimize if not typing.TYPE_CHECKING: from memoization import cached @@ -440,6 +441,18 @@ def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: return self.air_exch +@dataclass(frozen=True) +class CustomVentilation(_VentilationBase): + # The ventilation value for a given time + ventilation_value: PiecewiseConstant + + def transition_times(self, room: Room) -> typing.Set[float]: + return self.ventilation_value.transition_times + + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: + return self.ventilation_value.value(time) + + @dataclass(frozen=True) class Virus: #: RNA copies / mL @@ -1472,6 +1485,63 @@ def _normed_interpolated_longrange_exposure_between_bounds( return normed_int_concentration_interpolated +@dataclass(frozen=True) +class CO2Data: + # TODO - docstring + room_volume: float + number: typing.Union[int, IntPiecewiseConstant] + presence: typing.Optional[Interval] + ventilation_transition_times: typing.Tuple[float, ...] + times: typing.Sequence[float] + CO2_concentrations: typing.Sequence[float] + + def CO2_concentrations_from_params(self, + exhalation_rate: float, + ventilation_values: typing.Tuple[float, ...]) -> typing.List[float]: + + CO2_concentrations = CO2ConcentrationModel( + room=Room(volume=self.room_volume), + ventilation=CustomVentilation(PiecewiseConstant( + self.ventilation_transition_times, ventilation_values)), + CO2_emitters=Population( + number=self.number, + presence=self.presence, + mask=Mask.types['No mask'], + activity=Activity( + exhalation_rate=exhalation_rate, inhalation_rate=exhalation_rate), + host_immunity=0. + ) + ) + + return [CO2_concentrations.concentration(time) for time in self.times] + + def CO2_fit_params(self): + if len(self.times) != len(self.CO2_concentrations): + raise ValueError('times and CO2_concentrations must have same length.') + + if len(self.times) < 2: + raise ValueError( + 'times and CO2_concentrations must contain at last two elements') + + def fun(x): + exhalation_rate = x[0] + ventilation_values = tuple(x[1:]) + the_concentrations = self.CO2_concentrations_from_params( + exhalation_rate=exhalation_rate, + ventilation_values=ventilation_values + ) + return np.sqrt(np.sum((np.array(self.CO2_concentrations) - np.array(the_concentrations))**2)) + + # The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations) + res_dict = minimize(fun=fun, x0=np.ones(len(self.ventilation_transition_times)), method='powell', bounds=[ + (0, None) for _ in range(len(self.ventilation_transition_times))], options={'xtol': 1e-3}) + + exhalation_rate = res_dict['x'][0] + ventilation_values = res_dict['x'][1:] + + return exhalation_rate, ventilation_values + + @dataclass(frozen=True) class ExposureModel: """ @@ -1489,6 +1559,9 @@ class ExposureModel: #: Geographical data geographical_data: Cases + #: CO2 data + CO2_profile: CO2Data = () + #: The number of times the exposure event is repeated (default 1). repeats: int = config.exposure_model['repeats'] # type: ignore From 641a572d9d2305a253f002df6d7ac497507db10f Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 17 May 2023 17:27:05 +0200 Subject: [PATCH 02/61] ui modifications --- caimira/apps/calculator/defaults.py | 2 + caimira/apps/calculator/model_generator.py | 2 + caimira/apps/calculator/static/js/form.js | 20 ++ .../templates/base/calculator.form.html.j2 | 212 ++++++++++-------- 4 files changed, 139 insertions(+), 97 deletions(-) diff --git a/caimira/apps/calculator/defaults.py b/caimira/apps/calculator/defaults.py index 8bae7e23..08318c7f 100644 --- a/caimira/apps/calculator/defaults.py +++ b/caimira/apps/calculator/defaults.py @@ -22,6 +22,8 @@ 'ceiling_height': 0., 'conditional_probability_plot': False, 'conditional_probability_viral_loads': False, + 'CO2_data': '{}', + 'CO2_data_option': '{}', 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, 'exposed_finish': '17:30', diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index b844aa68..d52b569b 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -37,6 +37,8 @@ class FormData: ceiling_height: float conditional_probability_plot: bool conditional_probability_viral_loads: bool + CO2_data: dict + CO2_data_option: bool exposed_coffee_break_option: str exposed_coffee_duration: int exposed_finish: minutes_since_midnight diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index a88c5ccc..1d1e39f1 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -493,6 +493,20 @@ function on_coffee_break_option_change() { } } +function on_CO2_data_option_change() { + CO2_data_options = $('input[type=radio][name=CO2_data_option]'); + CO2_data_options.each(function (index){ + if (this.checked) { + getChildElement($(this)).show(); + require_fields(this); + } + else { + getChildElement($(this)).hide(); + require_fields(this); + } + }) +} + /* -------UI------- */ function show_disclaimer() { @@ -1039,6 +1053,12 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_coffee_break_option_change(); + // When the CO2_data_option changes we want to make its respective + // children show/hide. + $("input[type=radio][name=CO2_data_option]").change(on_CO2_data_option_change); + // Call the function now to handle forward/back button presses in the browser. + on_CO2_data_option_change(); + // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. validateMaxInfectedPeople(); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 8277f829..83b30069 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -185,127 +185,145 @@
+ Ventilation data:
?
-
- -
Ventilation type:
- -
-
- - - - -
-
- -
+
+
Use CO₂ concentration values:
+
+ + + + +
+ + - - +
+
HEPA filtration:
From 7617ac3c05ae7cd829c132bea58c59d596156286 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 27 Jun 2023 15:11:17 +0200 Subject: [PATCH 19/61] fixed download template by adding file to CERNBox resources --- caimira/apps/calculator/static/js/co2_form.js | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index b48c5d90..c2a03447 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -92,18 +92,14 @@ function displayJsonToHtmlTable(jsonData) { } } -function downloadTemplate() { - let final_export = [["Times", "CO2"], [8.5, 440.44]]; - // Prepare the CSV file. - let csvContent = "data:text/csv;charset=utf8," - + final_export.map(e => e.join(",")).join("\n"); - var encodedUri = encodeURI(csvContent); - // Set a name for the file. - var link = document.createElement("a"); - link.setAttribute("href", encodedUri); - link.setAttribute("download", "CO2_template.XLSX"); - document.body.appendChild(link); - link.click(); +function downloadTemplate(uri = 'https://caimira-resources.web.cern.ch/CO2_template.xlsx', filename = 'CO2_template.xlsx') { + var link = document.createElement("a"); + link.download = filename; + link.href = uri; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + delete link; } function insertErrorFor(referenceNode, text) { From fb887f7db6e1ea10f0fa47cb589e292acc079ad2 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 27 Jun 2023 17:03:27 +0200 Subject: [PATCH 20/61] added back compatibility (form report -> calculator) --- caimira/apps/calculator/__init__.py | 1 - caimira/apps/calculator/static/js/co2_form.js | 27 ++++++++++++------- caimira/apps/calculator/static/js/form.js | 18 +++++++++++++ .../templates/base/calculator.form.html.j2 | 2 +- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 12b8d885..668a91bf 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -379,7 +379,6 @@ def generate_image(transition_times: tuple, ventilation_values: tuple): fig = plt.figure(figsize=(7, 4), dpi=110) plt.plot(form.CO2_data['times'], form.CO2_data['CO2']) for index, time in enumerate(transition_times[:-1]): - print(time) plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--') y_location = (form.CO2_data['CO2'][min(range(len(form.CO2_data['times'])), key=lambda i: abs(form.CO2_data['times'][i]-time))]) plt.text(x = time + 0.04, y = y_location, s=round(ventilation_values[index], 2)) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index c2a03447..5315379b 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -1,3 +1,4 @@ +// JS file to handle manipulation on CO2 Fitting Algorithm Dialog. const CO2_data = [ 'CO2_data', 'specific_breaks', @@ -92,6 +93,7 @@ function displayJsonToHtmlTable(jsonData) { } } +// Method to download Excel template available on CERNBox function downloadTemplate(uri = 'https://caimira-resources.web.cern.ch/CO2_template.xlsx', filename = 'CO2_template.xlsx') { var link = document.createElement("a"); link.download = filename; @@ -114,15 +116,26 @@ function validate() { $('span.' + "error_text").remove(); let submit = true; for (var i = 0; i < CO2_data.length; i++) { - let element = $(`[name=${CO2_data[i]}]`); - if (element[0].value === '') { - insertErrorFor($('#CO2_input_data_div'), `'${element[0].name}' must be defined.`); // raise error for total number and room volume. + let element = $(`[name=${CO2_data[i]}]`)[0]; + if (element.value === '') { + insertErrorFor($('#CO2_input_data_div'), `'${element.name}' must be defined.`); // raise error for total number and room volume. submit = false; }; } return submit; } +function display_fitting_data(json_response) { + $("#DIV_CO2_fitting_result").show(); + $("#CO2_data_plot").attr("src", json_response['CO2_plot']); + delete json_response['CO2_plot']; + $("#CO2_fitting_result").val(JSON.stringify(json_response)); + $("#exhalation_rate_fit").html('Exhalation rate: ' + String(json_response['exhalation_rate'].toFixed(2)) + ' m³/h'); + // $("#ventilation_rate_fit").html(json_response['ventilation_values']); + $("#generate_fitting_data").html('Fit data'); + $("#save_and_dismiss_dialog").show(); +} + function submit_fitting_algorithm(url) { if (validate()) { let CO2_mapping = {}; @@ -143,13 +156,7 @@ function submit_fitting_algorithm(url) { }) .then((response) => response.json()) .then((json_response) => { - $("#DIV_CO2_fitting_result").show(); - $("#CO2_fitting_result").val(JSON.stringify(json_response)); - $("#exhalation_rate_fit").html('Exhalation rate: ' + String(json_response['exhalation_rate'].toFixed(2)) + ' m³/h'); - // $("#ventilation_rate_fit").html(json_response['ventilation_values']); - $("#CO2_data_plot").attr("src", json_response['CO2_plot']); - $("#generate_fitting_data").html('Fit data'); - $("#save_and_dismiss_dialog").show(); + display_fitting_data(json_response); }); } } diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index 02c2408a..3b7ce7bf 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -946,6 +946,12 @@ $(document).ready(function () { // Validation after } + + // Read CO2 Fitting Algorithms result + else if (name == 'CO2_fitting_result' || name == 'CO2_data') { + // Validation after + } + //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; @@ -956,6 +962,18 @@ $(document).ready(function () { // Handle default URL values if they are not explicitly defined. + // Populate CO2 Fitting Algorithm Dialog + let CO2_data = url.searchParams.has('CO2_data') ? url.searchParams.get('CO2_data') : null; + if (CO2_data) { + let CO2_inputs = JSON.parse(CO2_data); + let input_for_table = []; + for (let i = 0; i < CO2_inputs['times'].length; i++) { + input_for_table.push({'Times': CO2_inputs['times'][i], 'CO2': CO2_inputs['CO2'][i]}); + }; + displayJsonToHtmlTable(input_for_table); + submit_fitting_algorithm(`${$('#url_prefix').data().calculator_prefix}/co2-fit`); + } + // Populate primary vaccine dropdown $("#vaccine_type option").remove(); let primary_vaccine = url.searchParams.has('vaccine_type') ? url.searchParams.get('vaccine_type') : null; diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 7561d4a6..bb04819b 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -708,7 +708,7 @@
@@ -578,6 +591,9 @@ {% elif form.activity_type == "gym" %} Gym - For comparison only, all persons doing heavy physical exercise, breathing and not speaking. {% endif %} + {% if form.CO2_data_option %} +

Exhalation rate from fitting algorithm - {{form.CO2_fitting_result['exhalation_rate'] | round(2, 'floor')}} m³/h

+ {% endif %}

{% if form.short_range_option == "short_range_yes" %}
  • diff --git a/caimira/models.py b/caimira/models.py index 1d068088..a9f2c0ec 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1578,10 +1578,7 @@ def __post_init__(self): c_model.ventilation.air_exchange(c_model.room, time)) for time in c_model.state_change_times()))): raise ValueError("If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time.") - if not isinstance(self.exposed.number, int): - raise NotImplementedError("Cannot use dynamic occupancy for" - " the exposed population") - + @method_cache def population_state_change_times(self) -> typing.List[float]: """ diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index 4bca0f73..8fe6b891 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -27,7 +27,7 @@ def full_exposure_model(): short_range=(), exposed=models.Population( number=10, - presence=models.SpecificInterval(((8, 12), (13, 17), )), + presence=models.SpecificInterval(((8, 12), (13, 17), )), mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], host_immunity=0. @@ -51,11 +51,37 @@ def baseline_infected_population_number(): @pytest.fixture -def dynamic_single_exposure_model(full_exposure_model, baseline_infected_population_number): +def baseline_exposed_population_number(): + return models.Population( + number=models.IntPiecewiseConstant( + (8, 12, 13, 17), (10, 0, 10)), + presence=None, + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + host_immunity=0., + ) + + +@pytest.fixture +def dynamic_infected_single_exposure_model(full_exposure_model, baseline_infected_population_number): return dc_utils.nested_replace(full_exposure_model, {'concentration_model.infected': baseline_infected_population_number, }) +@pytest.fixture +def dynamic_exposed_single_exposure_model(full_exposure_model, baseline_exposed_population_number): + return dc_utils.nested_replace(full_exposure_model, + {'exposed': baseline_exposed_population_number, }) + + +@pytest.fixture +def dynamic_population_exposure_model(full_exposure_model, baseline_infected_population_number ,baseline_exposed_population_number): + return dc_utils.nested_replace(full_exposure_model, { + 'concentration_model.infected': baseline_infected_population_number, + 'exposed': baseline_exposed_population_number, + }) + + @pytest.mark.parametrize( "time", [4., 8., 10., 12., 13., 14., 16., 20., 24.], @@ -91,16 +117,16 @@ def test_population_number(full_exposure_model: models.ExposureModel, [4., 8., 10., 12., 13., 14., 16., 20., 24.], ) def test_concentration_model_dynamic_population(full_exposure_model: models.ExposureModel, - dynamic_single_exposure_model: models.ExposureModel, + dynamic_infected_single_exposure_model: models.ExposureModel, time: float): - assert full_exposure_model.concentration(time) == dynamic_single_exposure_model.concentration(time) + assert full_exposure_model.concentration(time) == dynamic_infected_single_exposure_model.concentration(time) @pytest.mark.parametrize("number_of_infected",[1, 2, 3, 4, 5]) @pytest.mark.parametrize("time",[9., 12.5, 16.]) def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureModel, - dynamic_single_exposure_model: models.ExposureModel, + dynamic_infected_single_exposure_model: models.ExposureModel, time: float, number_of_infected: int): @@ -112,8 +138,8 @@ def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureM } ) - npt.assert_almost_equal(static_multiple_exposure_model.concentration(time), dynamic_single_exposure_model.concentration(time) * number_of_infected) - npt.assert_almost_equal(static_multiple_exposure_model.deposited_exposure(), dynamic_single_exposure_model.deposited_exposure() * number_of_infected) + npt.assert_almost_equal(static_multiple_exposure_model.concentration(time), dynamic_infected_single_exposure_model.concentration(time) * number_of_infected) + npt.assert_almost_equal(static_multiple_exposure_model.deposited_exposure(), dynamic_infected_single_exposure_model.deposited_exposure() * number_of_infected) @pytest.mark.parametrize( From 1cfd7b684fbfff7602cf0e03d266148c333ae4af Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 4 Jul 2023 11:31:33 +0100 Subject: [PATCH 24/61] simplified the way to select previous ventilations --- caimira/apps/calculator/static/js/form.js | 14 +++++--------- .../apps/templates/base/calculator.form.html.j2 | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index 715808f0..f24c21cc 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -247,6 +247,8 @@ function on_ventilation_type_change() { ventilation_types = $('input[type=radio][name=ventilation_type]'); ventilation_types.each(function (index) { if (this.checked) { + + if ($(this).val() != 'from_fitting') $('#button_fit_data').attr('data-previous-vent', $(this).val()) getChildElement($(this)).show(); require_fields(this); } else { @@ -498,20 +500,14 @@ function ventilation_from_fitting(condition_from_fitting) { $('input[type=radio][id=mechanical_ventilation]').prop("disabled", condition_from_fitting); $('input[type=radio][id=natural_ventilation]').prop("disabled", condition_from_fitting); $('input[type=radio][id=from_fitting]').prop("disabled", !condition_from_fitting); + if (condition_from_fitting) { $('input[type=radio][id=from_fitting]').prop('checked',true); $('#DIVfrom_fitting').after($('#window_opening_regime')); } else { - // Select the URL ventilation option, if any (from back-navigation) - var url = new URL(decodeURIComponent(window.location.href)); - let ventilation_from_url; - if (url.searchParams.has('ventilation_type')) { - ventilation_from_url = url.searchParams.get('ventilation_type'); - if (ventilation_from_url == 'from_fitting') ventilation_from_url = 'no_ventilation'; - } - else ventilation_from_url = 'no_ventilation'; - $(`input[type=radio][id=${ventilation_from_url}]`).prop('checked',true); + let selected_ventilation = $("#button_fit_data").attr('data-previous-vent'); + $(`input[type=radio][id=${selected_ventilation}]`).prop('checked',true); $('#DIVopening_distance').after($('#window_opening_regime')); } on_ventilation_type_change(); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 7c6cb522..f6f68ffa 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -692,7 +692,7 @@

  • - +
    From 594051eddc1f9abfac189fbece874b1573ea868e Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 4 Jul 2023 15:17:46 +0100 Subject: [PATCH 25/61] added visualisation for ach table in form and report --- caimira/apps/calculator/report_generator.py | 7 +++++++ caimira/apps/calculator/static/js/co2_form.js | 13 +++++++++---- caimira/apps/calculator/static/js/form.js | 1 - .../apps/templates/base/calculator.report.html.j2 | 10 +++++++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 76c363f5..b70610bf 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -319,6 +319,12 @@ def readable_minutes(minutes: int) -> str: return time_str + unit +def hour_format(hour: float) -> str: + hours = int(hour) + minutes = int(hour % 1 * 60) + + return f"{hours}:{minutes}" + def percentage(absolute: float) -> float: return absolute * 100 @@ -514,6 +520,7 @@ def _template_environment(self) -> jinja2.Environment: env.filters['non_zero_percentage'] = non_zero_percentage env.filters['readable_minutes'] = readable_minutes env.filters['minutes_to_time'] = minutes_to_time + env.filters['hour_format'] = hour_format env.filters['float_format'] = "{0:.2f}".format env.filters['int_format'] = "{:0.0f}".format env.filters['percentage'] = percentage diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index e5277f3f..d03186a5 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -125,6 +125,12 @@ function validate() { return submit; } +function display_transition_times_hour_format(start, stop) { + var minutes_start = start % 1 * 60; + var minutes_stop = stop % 1 * 60; + return Math.floor(start) + ':' + minutes_start.toPrecision(2) + ' - ' + Math.floor(stop) + ':' + minutes_stop.toPrecision(2); +} + function display_fitting_data(json_response) { $("#DIV_CO2_fitting_result").show(); $("#CO2_data_plot").attr("src", json_response['CO2_plot']); @@ -132,11 +138,10 @@ function display_fitting_data(json_response) { delete json_response['CO2_plot']; $("#CO2_fitting_result").val(JSON.stringify(json_response)); $("#exhalation_rate_fit").html('Exhalation rate: ' + String(json_response['exhalation_rate'].toFixed(2)) + ' m³/h'); - let ventilation_table = "TimeVentilation value (ACH)"; + let ventilation_table = "Time (HH:MM)ACH value (h⁻¹)"; json_response['ventilation_values'].map((val, index) => { - console.log(json_response['transition_times']) - let transition_times = `${(json_response['transition_times'][index]).toFixed(2)} - ${(json_response['transition_times'][index + 1]).toFixed(2)}` - ventilation_table += `${transition_times}${val}`; + let transition_times = display_transition_times_hour_format(json_response['transition_times'][index], json_response['transition_times'][index + 1]); + ventilation_table += `${transition_times}${val.toPrecision(2)}`; }); $("#ventilation_rate_fit").html(ventilation_table); $("#generate_fitting_data").html('Fit data'); diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index f24c21cc..72eff671 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -500,7 +500,6 @@ function ventilation_from_fitting(condition_from_fitting) { $('input[type=radio][id=mechanical_ventilation]').prop("disabled", condition_from_fitting); $('input[type=radio][id=natural_ventilation]').prop("disabled", condition_from_fitting); $('input[type=radio][id=from_fitting]').prop("disabled", !condition_from_fitting); - if (condition_from_fitting) { $('input[type=radio][id=from_fitting]').prop('checked',true); $('#DIVfrom_fitting').after($('#window_opening_regime')); diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index cdbd26d0..fed4c361 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -530,11 +530,15 @@ {% endif %}
  • From Fitting: {% if form.ventilation_type == "from_fitting" %} - Yes + Yes - + {% for ventilation in form.CO2_fitting_result['ventilation_values'] %} - + {% set transition_time = form.CO2_fitting_result['transition_times'] %} + + + + {% endfor %}
    Ventilation value (ACH)
    Time (HH:MM)ACH value (h⁻¹)
    {{ventilation}}
    {{ transition_time[loop.index - 1] | hour_format }} - {{ transition_time[loop.index] | hour_format }}{{ ventilation | float_format }}
    {% else %} From e3b6326d238c49c8f38bb369ca9a660027b0ff3f Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 10 Jul 2023 15:45:22 +0100 Subject: [PATCH 26/61] deal with modal button labels and pre-selected ventilation options --- caimira/apps/calculator/static/js/co2_form.js | 5 ++++- caimira/apps/templates/base/calculator.form.html.j2 | 7 ++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index d03186a5..549a6329 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -28,6 +28,9 @@ const CO2_data_form = [ // Method to upload a valid excel file function upload_file() { + clear_fitting_algorithm(); + $("#generate_fitting_data").show(); + $("#save_and_dismiss_dialog").hide(); var files = document.getElementById("file_upload").files; if (files.length == 0) { alert("Please choose any file..."); @@ -145,6 +148,7 @@ function display_fitting_data(json_response) { }); $("#ventilation_rate_fit").html(ventilation_table); $("#generate_fitting_data").html('Fit data'); + $("#generate_fitting_data").hide(); $("#save_and_dismiss_dialog").show(); } @@ -181,6 +185,5 @@ function clear_fitting_algorithm() { $('span.' + "error_text").remove(); $('#DIV_CO2_fitting_result').hide(); $('#CO2_input_data_div').hide(); - $('#CO2_data_no').click(); ventilation_from_fitting(false); } diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index f6f68ffa..2b7f34b3 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -702,9 +702,6 @@

    From f9d4dead8703bcbc2ed825fa6c6e2d3f93fb47e0 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 10 Jul 2023 15:50:15 +0100 Subject: [PATCH 27/61] updated one text entry and variable declaration on JS --- caimira/apps/calculator/static/js/co2_form.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 549a6329..6fde640a 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -36,8 +36,8 @@ function upload_file() { alert("Please choose any file..."); return; } - var filename = files[0].name; - var extension = filename.substring(filename.lastIndexOf(".")).toUpperCase(); + const filename = files[0].name; + const extension = filename.substring(filename.lastIndexOf(".")).toUpperCase(); if (extension == ".XLS" || extension == ".XLSX") { //Here calling another method to read excel file into json excelFileToJSON(files[0]); @@ -92,7 +92,7 @@ function displayJsonToHtmlTable(jsonData) { format.value = JSON.stringify(structure); $('#generate_fitting_data').prop("disabled", false); } else { - table.innerHTML = "There is no data in Excel"; + table.innerHTML = "There is no data in the spreadsheet file"; } } From 5997049378634620a26f1d0a659da061996efa01 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 11 Jul 2023 14:09:17 +0100 Subject: [PATCH 28/61] renamed clear fitting algorithm method --- caimira/apps/calculator/static/js/co2_form.js | 9 +++++++-- caimira/apps/templates/base/calculator.form.html.j2 | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 6fde640a..59189d37 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -28,7 +28,7 @@ const CO2_data_form = [ // Method to upload a valid excel file function upload_file() { - clear_fitting_algorithm(); + clear_fitting_result_component(); $("#generate_fitting_data").show(); $("#save_and_dismiss_dialog").hide(); var files = document.getElementById("file_upload").files; @@ -177,7 +177,7 @@ function submit_fitting_algorithm(url) { } } -function clear_fitting_algorithm() { +function clear_fitting_result_component() { $('#generate_fitting_data').prop("disabled", true); $("#display_excel_data tbody").remove(); $('#CO2_fitting_result').val(''); @@ -185,5 +185,10 @@ function clear_fitting_algorithm() { $('span.' + "error_text").remove(); $('#DIV_CO2_fitting_result').hide(); $('#CO2_input_data_div').hide(); +} + +function clear_fitting_algorithm() { + clear_fitting_result_component(); + $('#CO2_data_no').click(); ventilation_from_fitting(false); } diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 2b7f34b3..8880de6d 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -732,7 +732,7 @@ From 91f16932ba5bd42f29072d355026e9a1a6cb4fb9 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 19 Jul 2023 17:04:31 +0200 Subject: [PATCH 29/61] ui modifications on html components --- .../templates/base/calculator.form.html.j2 | 146 +++++++++--------- .../templates/base/calculator.report.html.j2 | 2 +- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 8880de6d..f7a4bc91 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -211,7 +211,7 @@ - +
  • From 6831b1ed7bea5bc37bca8029995d2998a1acd3fc Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 19 Jul 2023 17:04:40 +0200 Subject: [PATCH 30/61] ui modifications on js logic --- caimira/apps/calculator/static/js/co2_form.js | 313 +++++++++++------- caimira/apps/calculator/static/js/form.js | 44 +-- 2 files changed, 204 insertions(+), 153 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 59189d37..bf9751bd 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -1,7 +1,5 @@ -// JS file to handle manipulation on CO2 Fitting Algorithm Dialog. const CO2_data_form = [ 'CO2_data', - 'specific_breaks', 'exposed_coffee_break_option', 'exposed_coffee_duration', 'exposed_finish', @@ -9,6 +7,8 @@ const CO2_data_form = [ 'exposed_lunch_option', 'exposed_lunch_start', 'exposed_start', + 'fitting_ventilation_states', + 'fitting_ventilation_type', 'infected_coffee_break_option', 'infected_coffee_duration', 'infected_dont_have_breaks_with_exposed', @@ -21,174 +21,239 @@ const CO2_data_form = [ 'room_volume', 'total_people', 'ventilation_type', - 'windows_duration', - 'windows_frequency', - 'window_opening_regime', -] - -// Method to upload a valid excel file -function upload_file() { - clear_fitting_result_component(); - $("#generate_fitting_data").show(); - $("#save_and_dismiss_dialog").hide(); - var files = document.getElementById("file_upload").files; - if (files.length == 0) { - alert("Please choose any file..."); - return; - } - const filename = files[0].name; - const extension = filename.substring(filename.lastIndexOf(".")).toUpperCase(); - if (extension == ".XLS" || extension == ".XLSX") { - //Here calling another method to read excel file into json - excelFileToJSON(files[0]); - } else { - alert("Please select a valid excel file."); + ]; + + // Method to upload a valid excel file + function uploadFile(endpoint) { + clearFittingResultComponent(); + const files = document.getElementById("file_upload").files; + if (files.length === 0) { + alert("Please choose any file..."); + return; } -} - -//Method to read excel file and convert it into JSON -function excelFileToJSON(file) { + const file = files[0]; + const extension = file.name.substring(file.name.lastIndexOf(".")).toUpperCase(); + extension === ".XLS" || extension === ".XLSX" + ? excelFileToJSON(endpoint, file) + : alert("Please select a valid excel file."); + } + + // Method to read excel file and convert it into JSON + function excelFileToJSON(endpoint, file) { try { - var reader = new FileReader(); - reader.readAsBinaryString(file); - reader.onload = function (e) { - var data = e.target.result; - var workbook = XLSX.read(data, { type: "binary" }); - var firstSheetName = workbook.SheetNames[0]; - //reading only first sheet data - var jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName]); - //displaying the json result into HTML table - displayJsonToHtmlTable(jsonData); - }; + const reader = new FileReader(); + reader.readAsBinaryString(file); + reader.onload = function (e) { + const data = e.target.result; + const workbook = XLSX.read(data, { type: "binary" }); + const firstSheetName = workbook.SheetNames[0]; + const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName]); + displayJsonToHtmlTable(endpoint, jsonData); + }; } catch (e) { - console.error(e); + console.error(e); } -} - -//Method to display the data in HTML Table -function displayJsonToHtmlTable(jsonData) { - var table = document.getElementById("display_excel_data"); - var format = document.getElementById("formatted_data"); - let structure = { times: [], CO2: [] }; + } + + // Method to display the data in HTML Table + function displayJsonToHtmlTable(endpoint, jsonData) { + const table = document.getElementById("display_excel_data"); + const format = document.getElementById("CO2_data"); + const structure = { times: [], CO2: [] }; if (jsonData.length > 0) { - var htmlData = "TimeCO2 Value"; - let jsonLength = jsonData.length; - for (var i = 0; i < jsonLength; i++) { - var row = jsonData[i]; - if (i < 5) { - htmlData += - ` - ${row["Times"].toFixed(2)} - - ${row["CO2"].toFixed(2)} - `; - } - structure["times"].push(row["Times"]); - structure["CO2"].push(row["CO2"]); + let htmlData = "TimeCO2 Value"; + const jsonLength = jsonData.length; + for (let i = 0; i < jsonLength; i++) { + const row = jsonData[i]; + if (i < 5) { + htmlData += ` + + ${row["Times"].toFixed(2)} + ${row["CO2"].toFixed(2)} + `; } - - if (jsonLength >= 5) htmlData += " ... ... "; - table.innerHTML = htmlData; - format.value = JSON.stringify(structure); - $('#generate_fitting_data').prop("disabled", false); + structure.times.push(row["Times"]); + structure.CO2.push(row["CO2"]); + } + + if (jsonLength >= 5) { + htmlData += " ... ... "; + } + format.value = JSON.stringify(structure); + $('#generate_fitting_data').prop("disabled", false); + $('#fitting_ventilation_states').prop('disabled', false); + $('[name=fitting_ventilation_type]').prop('disabled', false); + plotCO2Data(endpoint); } else { - table.innerHTML = "There is no data in the spreadsheet file"; + table.innerHTML = "There is no data in the spreadsheet file"; } -} - -// Method to download Excel template available on CERNBox -function downloadTemplate(uri = 'https://caimira-resources.web.cern.ch/CO2_template.xlsx', filename = 'CO2_template.xlsx') { - var link = document.createElement("a"); + } + + // Method to download Excel template available on CERNBox + function downloadTemplate(uri = 'https://caimira-resources.web.cern.ch/CO2_template.xlsx', filename = 'CO2_template.xlsx') { + const link = document.createElement("a"); link.download = filename; link.href = uri; document.body.appendChild(link); link.click(); document.body.removeChild(link); delete link; -} - -function insertErrorFor(referenceNode, text) { - var element = document.createElement("span"); + } + + function insertErrorFor(referenceNode, text) { + const element = document.createElement("span"); element.setAttribute("class", "error_text"); element.classList.add("red_text"); element.innerHTML = "  " + text; $(referenceNode).before(element); -} - -function validate() { + } + + function validateFormInputs(obj) { $('span.' + "error_text").remove(); let submit = true; - for (var i = 0; i < CO2_data_form.length; i++) { - let element = $(`[name=${CO2_data_form[i]}]`)[0]; - if (element.value === '') { - insertErrorFor($('#CO2_input_data_div'), `'${element.name}' must be defined.`); // raise error for total number and room volume. + for (let i = 0; i < CO2_data_form.length; i++) { + const element = $(`[name=${CO2_data_form[i]}]`)[0]; + if (element.name !== 'fitting_ventilation_states' && element.value === '') { + insertErrorFor($('#DIVCO2_data_dialog'), `'${element.name}' must be defined.
    `); + submit = false; + } + } + if (submit) { + $($(obj).data('target')).modal('show'); + } + return submit; + } + +function validateCO2Form() { + let submit = true; + if (validateFormInputs($('#button_fit_data'))) submit = true; + + // Check if natural ventilation is selected + if ($('input[name="fitting_ventilation_type"]:checked')[0].value == 'fitting_natural_ventilation') { + // Validate ventilation scheme + const element = $('[name=fitting_ventilation_states')[0] + if (element.value !== '') { + // validate input format + try { + const parsedValue = JSON.parse(element.value); + if (!Array.isArray(parsedValue)) { + insertErrorFor($('#DIVCO2_fitting_result'), `'${element.name}' must be a list.
    `); + submit = false; + }; + } catch { + insertErrorFor($('#DIVCO2_fitting_result'), `'${element.name}' must be a list of numbers.
    `); + submit = false; + }; + } else { + insertErrorFor($('#DIVCO2_fitting_result'), `'${element.name}' must be defined.
    `); submit = false; }; - } + }; + return submit; } -function display_transition_times_hour_format(start, stop) { - var minutes_start = start % 1 * 60; - var minutes_stop = stop % 1 * 60; - return Math.floor(start) + ':' + minutes_start.toPrecision(2) + ' - ' + Math.floor(stop) + ':' + minutes_stop.toPrecision(2); +function displayTransitionTimesHourFormat(start, stop) { + var minutes_start = (start % 1 * 60).toPrecision(2); + var minutes_stop = (stop % 1 * 60).toPrecision(2); + return Math.floor(start) + ':' + ((minutes_start != '0.0') ? minutes_start : '00') + ' - ' + Math.floor(stop) + ':' + ((minutes_stop != '0.0') ? minutes_stop : '00'); } -function display_fitting_data(json_response) { - $("#DIV_CO2_fitting_result").show(); +function displayFittingData(json_response) { + $("#DIVCO2_fitting_result").show(); $("#CO2_data_plot").attr("src", json_response['CO2_plot']); // Not needed for the form submit delete json_response['CO2_plot']; $("#CO2_fitting_result").val(JSON.stringify(json_response)); $("#exhalation_rate_fit").html('Exhalation rate: ' + String(json_response['exhalation_rate'].toFixed(2)) + ' m³/h'); let ventilation_table = "Time (HH:MM)ACH value (h⁻¹)"; - json_response['ventilation_values'].map((val, index) => { - let transition_times = display_transition_times_hour_format(json_response['transition_times'][index], json_response['transition_times'][index + 1]); + json_response['ventilation_values'].forEach((val, index) => { + let transition_times = displayTransitionTimesHourFormat(json_response['transition_times'][index], json_response['transition_times'][index + 1]); ventilation_table += `${transition_times}${val.toPrecision(2)}`; }); + $('#disable_fitting_algorithm').prop('disabled', false); $("#ventilation_rate_fit").html(ventilation_table); $("#generate_fitting_data").html('Fit data'); $("#generate_fitting_data").hide(); $("#save_and_dismiss_dialog").show(); } -function submit_fitting_algorithm(url) { - if (validate()) { - let CO2_mapping = {}; - CO2_data_form.map(el => { - let element = $(`[name=${el}]`); - // Validate radio buttons - if (element.length != 1) CO2_mapping[element[0].name] = $(`[name=${element[0].name}]:checked`)[0].value - else CO2_mapping[element[0].name] = element[0].value; - }) - $('#CO2_input_data_div').show(); - $("#generate_fitting_data").html( - `Loading...` - ); - $('#CO2_input_data').html(JSON.stringify(CO2_mapping, null, "\t")) +function formatCO2DataForm(CO2_data_form) { + let CO2_mapping = {}; + CO2_data_form.map(el => { + let element = $(`[name=${el}]`); + // Validate checkboxes + if (element[0].type == 'checkbox') { + CO2_mapping[element[0].name] = String(+element[0].checked); + } + // Validate radio buttons + else if (element[0].type == 'radio') CO2_mapping[element[0].name] = $(`[name=${element[0].name}]:checked`)[0].value; + else CO2_mapping[element[0].name] = element[0].value; + }); + return CO2_mapping; +} + +function plotCO2Data(url) { + if (validateFormInputs()) { + let CO2_mapping = formatCO2DataForm(CO2_data_form); fetch(url, { method: "POST", body: JSON.stringify(CO2_mapping), }) - .then((response) => response.json()) - .then((json_response) => { - display_fitting_data(json_response); - }); + .then((response) => + response.json() + .then(json_response => $("#CO2_data_plot").attr("src", json_response['CO2_plot'])) + .then($('#DIVCO2_fitting_to_submit').show()) + .catch(error => console.log(error)) + ); } } -function clear_fitting_result_component() { - $('#generate_fitting_data').prop("disabled", true); - $("#display_excel_data tbody").remove(); +function submitFittingAlgorithm(url) { + if (validateCO2Form()) { + // Disable all the ventilation inputs + $('#fitting_ventilation_states, [name=fitting_ventilation_type]').prop('disabled', true); + + // Prepare data for submission + const CO2_mapping = formatCO2DataForm(CO2_data_form); + $('#CO2_input_data_div').show(); + $('#disable_fitting_algorithm').prop('disabled', true); + $('#generate_fitting_data') + .html('Loading...') + .prop('disabled', true); + $('#CO2_input_data').html(JSON.stringify(CO2_mapping, null, '\t')); + + fetch(url, { + method: 'POST', + body: JSON.stringify(CO2_mapping), + }) + .then((response) => response.json()) + .then((json_response) => { + displayFittingData(json_response); + }); + } + } + + function clearFittingResultComponent() { + // Remove all the previously generated fitting elements + $('#generate_fitting_data').prop('disabled', true); $('#CO2_fitting_result').val(''); - $('#formatted_data').val(''); - $('span.' + "error_text").remove(); - $('#DIV_CO2_fitting_result').hide(); - $('#CO2_input_data_div').hide(); -} - -function clear_fitting_algorithm() { - clear_fitting_result_component(); - $('#CO2_data_no').click(); - ventilation_from_fitting(false); -} + $('#CO2_data').val('{}'); + $('#fitting_ventilation_states').val(''); + $('span.error_text').remove(); + $('#DIVCO2_fitting_result, #CO2_input_data_div').hide(); + $('#CO2_data_plot').attr('src', ''); + + // Update the ventilation scheme components + $('#fitting_ventilation_states, [name=fitting_ventilation_type]').prop('disabled', false); + + // Update the bottom right buttons + $('#generate_fitting_data').show(); + $('#save_and_dismiss_dialog').hide(); + } + + function disableFittingAlgorithm() { + clearFittingResultComponent(); + $('#CO2_data_no').click(); + } + diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index 72eff671..f497a3c4 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -247,8 +247,6 @@ function on_ventilation_type_change() { ventilation_types = $('input[type=radio][name=ventilation_type]'); ventilation_types.each(function (index) { if (this.checked) { - - if ($(this).val() != 'from_fitting') $('#button_fit_data').attr('data-previous-vent', $(this).val()) getChildElement($(this)).show(); require_fields(this); } else { @@ -495,32 +493,13 @@ function on_coffee_break_option_change() { } } -function ventilation_from_fitting(condition_from_fitting) { - $('input[type=radio][id=no_ventilation]').prop("disabled", condition_from_fitting); - $('input[type=radio][id=mechanical_ventilation]').prop("disabled", condition_from_fitting); - $('input[type=radio][id=natural_ventilation]').prop("disabled", condition_from_fitting); - $('input[type=radio][id=from_fitting]').prop("disabled", !condition_from_fitting); - if (condition_from_fitting) { - $('input[type=radio][id=from_fitting]').prop('checked',true); - $('#DIVfrom_fitting').after($('#window_opening_regime')); - } - else { - let selected_ventilation = $("#button_fit_data").attr('data-previous-vent'); - $(`input[type=radio][id=${selected_ventilation}]`).prop('checked',true); - $('#DIVopening_distance').after($('#window_opening_regime')); - } - on_ventilation_type_change(); -} - -function on_CO2_data_option_change() { - CO2_data_options = $('input[type=radio][name=CO2_data_option]'); - CO2_data_options.each(function (index){ +function on_CO2_fitting_ventilation_change() { + ventilation_options = $('input[type=radio][name=fitting_ventilation_type]'); + ventilation_options.each(function (index) { if (this.checked) { - if (this.id == 'CO2_data_yes') ventilation_from_fitting(true); - else if (this.id == 'CO2_data_no') ventilation_from_fitting(false); getChildElement($(this)).show(); require_fields(this); - } + } else { getChildElement($(this)).hide(); require_fields(this); @@ -700,6 +679,13 @@ function validate_form(form) { on_short_range_option_change(); } + // Check if fitting is selected + if ($('input[type=radio][id=from_fitting]').prop('checked') ) { + if ($('#CO2_fitting_result').val() == '') + $("input[type=radio][id=no_ventilation]").prop("checked", true); + on_ventilation_type_change(); + } + if (submit) { $("#generate_report").prop("disabled", true); //Add spinner to button @@ -969,7 +955,7 @@ $(document).ready(function () { // Populate CO2 Fitting Algorithm Dialog let CO2_data = url.searchParams.has('CO2_fitting_result') ? url.searchParams.get('CO2_fitting_result') : null; - if (CO2_data) display_fitting_data(JSON.parse(CO2_data)); + if (CO2_data) displayFittingData(JSON.parse(CO2_data)); // Populate primary vaccine dropdown $("#vaccine_type option").remove(); @@ -1086,11 +1072,11 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_coffee_break_option_change(); - // When the CO2_data_option changes we want to make its respective + // When the ventilation on the fitting changes we want to make its respective // children show/hide. - $("input[type=radio][name=CO2_data_option]").change(on_CO2_data_option_change); + $("input[type=radio][name=fitting_ventilation_type]").change(on_CO2_fitting_ventilation_change); // Call the function now to handle forward/back button presses in the browser. - on_CO2_data_option_change(); + on_CO2_fitting_ventilation_change(); // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. From d3298c32310a04b16b5043d80d72ba653d787415 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 19 Jul 2023 17:05:11 +0200 Subject: [PATCH 31/61] back-end updates for co2 logic --- caimira/apps/calculator/__init__.py | 57 +++++----- .../apps/calculator/co2_model_generator.py | 100 +++++++++++------- caimira/apps/calculator/defaults.py | 1 - caimira/apps/calculator/model_generator.py | 20 ++-- caimira/apps/calculator/report_generator.py | 2 +- caimira/models.py | 9 +- 6 files changed, 106 insertions(+), 83 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 97b20fca..401849d7 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -351,8 +351,20 @@ def check_xsrf_cookie(self): Thus, XSRF cookies are disabled by overriding base class implementation of this method with a pass statement. """ pass + + def generate_ventilation_plot(self, CO2Data, transition_times = None, ventilation_values = None): + fig = plt.figure(figsize=(7, 4), dpi=110) + plt.plot(CO2Data['times'], CO2Data['CO2']) + if (transition_times and ventilation_values): + for index, time in enumerate(transition_times[:-1]): + plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--') + y_location = (CO2Data['CO2'][min(range(len(CO2Data['times'])), key=lambda i: abs(CO2Data['times'][i]-time))]) + plt.text(x = time + 0.04, y = y_location, s=round(ventilation_values[index], 2)) + plt.xlabel('Time of day') + plt.ylabel('Concentration (ppm)') + return img2base64(_figure2bytes(fig)) - async def post(self) -> None: + async def post(self, endpoint: str) -> None: requested_model_config = tornado.escape.json_decode(self.request.body) try: form = co2_model_generator.CO2FormData.from_dict(requested_model_config) @@ -365,31 +377,24 @@ async def post(self) -> None: self.finish(json.dumps(response_json)) return - executor = loky.get_reusable_executor( - max_workers=self.settings['handler_worker_pool_size'], - timeout=300, - ) - report_task = executor.submit( - co2_model_generator.CO2FormData.build_model, form, - ) - report = await asyncio.wrap_future(report_task) - - def generate_ventilation_plot(transition_times: tuple, ventilation_values: tuple): - fig = plt.figure(figsize=(7, 4), dpi=110) - plt.plot(form.CO2_data['times'], form.CO2_data['CO2']) - for index, time in enumerate(transition_times[:-1]): - plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--') - y_location = (form.CO2_data['CO2'][min(range(len(form.CO2_data['times'])), key=lambda i: abs(form.CO2_data['times'][i]-time))]) - plt.text(x = time + 0.04, y = y_location, s=round(ventilation_values[index], 2)) - plt.xlabel('Time of day') - plt.ylabel('Concentration (ppm)') - return fig + if endpoint == 'plot': + self.finish({'CO2_plot': self.generate_ventilation_plot(form.CO2_data)}) + else: + executor = loky.get_reusable_executor( + max_workers=self.settings['handler_worker_pool_size'], + timeout=300, + ) + report_task = executor.submit( + co2_model_generator.CO2FormData.build_model, form, + ) + report = await asyncio.wrap_future(report_task) + + result = dict(report.CO2_fit_params()) + result['fitting_ventilation_type'] = 'fitting_natural_ventilation' + result['transition_times'] = report.ventilation_transition_times + result['CO2_plot'] = self.generate_ventilation_plot(form.CO2_data, report.ventilation_transition_times, result['ventilation_values']) + self.finish(result) - result = dict(report.CO2_fit_params()) - result['transition_times'] = report.ventilation_transition_times - result['CO2_plot'] = img2base64(_figure2bytes(generate_ventilation_plot(report.ventilation_transition_times, result['ventilation_values']))) - self.finish(result) - def get_url(app_root: str, relative_path: str = '/'): return app_root.rstrip('/') + relative_path.rstrip('/') @@ -412,7 +417,7 @@ def make_app( base_urls: typing.List = [ (get_root_url(r'/?'), LandingPage), (get_root_calculator_url(r'/?'), CalculatorForm), - (get_root_calculator_url(r'/co2-fit/'), CO2Data), + (get_root_calculator_url(r'/co2-fit/(.*)'), CO2Data), (get_root_calculator_url(r'/report'), ConcentrationModel), (get_root_url(r'/static/(.*)'), StaticFileHandler, {'path': static_dir}), (get_root_calculator_url(r'/static/(.*)'), StaticFileHandler, {'path': calculator_static_dir}), diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 39e61d29..063fc6a2 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -4,7 +4,10 @@ import typing from caimira import models +from caimira import data from . import model_generator +import caimira.monte_carlo as mc +from caimira.monte_carlo.data import activity_distributions, virus_distributions, mask_distributions, short_range_distances minutes_since_midnight = typing.NewType('minutes_since_midnight', int) @@ -18,7 +21,6 @@ @dataclasses.dataclass class CO2FormData: CO2_data: dict - specific_breaks: dict exposed_coffee_break_option: str exposed_coffee_duration: int exposed_finish: minutes_since_midnight @@ -26,6 +28,8 @@ class CO2FormData: exposed_lunch_option: bool exposed_lunch_start: minutes_since_midnight exposed_start: minutes_since_midnight + fitting_ventilation_states: list + fitting_ventilation_type: str infected_coffee_break_option: str #Used if infected_dont_have_breaks_with_exposed infected_coffee_duration: int #Used if infected_dont_have_breaks_with_exposed infected_dont_have_breaks_with_exposed: bool @@ -38,15 +42,11 @@ class CO2FormData: room_volume: float total_people: int ventilation_type: str - windows_duration: float - windows_frequency: float - window_opening_regime: str #: The default values for undefined fields. Note that the defaults here #: and the defaults in the html form must not be contradictory. _DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = { 'CO2_data': '{}', - 'specific_breaks': '{}', 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, 'exposed_finish': '17:30', @@ -54,6 +54,8 @@ class CO2FormData: 'exposed_lunch_option': True, 'exposed_lunch_start': '12:30', 'exposed_start': '08:30', + 'fitting_ventilation_states': '[]', + 'fitting_ventilation_type': 'fitting_natural_ventilation', 'infected_coffee_break_option': 'coffee_break_0', 'infected_coffee_duration': 5, 'infected_dont_have_breaks_with_exposed': False, @@ -66,9 +68,6 @@ class CO2FormData: 'room_volume': _NO_DEFAULT, 'total_people': _NO_DEFAULT, 'ventilation_type': 'no_ventilation', - 'windows_duration': 10., - 'windows_frequency': 60., - 'window_opening_regime': 'windows_open_permanently', } @classmethod @@ -100,18 +99,47 @@ def from_dict(cls, form_data: typing.Dict) -> "CO2FormData": return instance def build_model(self) -> models.CO2Data: - population_presence=self.population_present_interval() - last_time_present = population_presence.boundaries()[-1][-1] - last_present_time_index = next((index for index, time in enumerate(self.CO2_data['times']) - if time > last_time_present), len(self.CO2_data['times'])) + infected_population: models.Population = self.infected_population() + exposed_population: models.Population = self.exposed_population() + all_state_changes=self.population_present_interval() + + total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) + for _, stop in zip(all_state_changes[:-1], all_state_changes[1:])] return models.CO2Data( room_volume=self.room_volume, - number=self.total_people, - presence=population_presence, - ventilation_transition_times=self.ventilation_transition_times(last_time_present), - times=self.CO2_data['times'][:last_present_time_index], - CO2_concentrations=self.CO2_data['CO2'][:last_present_time_index], + number=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), + presence=None, + ventilation_transition_times=self.ventilation_transition_times(), + times=self.CO2_data['times'], + CO2_concentrations=self.CO2_data['CO2'], ) + + def exposed_population(self) -> models.Population: + infected_occupants = self.infected_people + # The number of exposed occupants is the total number of occupants + # minus the number of infected occupants. + exposed_occupants = self.total_people - infected_occupants + + exposed = models.Population( + number=exposed_occupants, + presence=self.exposed_present_interval(), + activity=models.Activity.types['Seated'], + mask=models.Mask.types['No mask'], + host_immunity=0., + ) + return exposed + + def infected_population(self) -> models.Population: + infected_occupants = self.infected_people + + infected = models.Population( + number=infected_occupants, + presence=self.infected_present_interval(), + activity=models.Activity.types['Seated'], + mask=models.Mask.types['No mask'], + host_immunity=0., + ) + return infected def _compute_breaks_in_interval(self, start, finish, n_breaks, duration) -> models.BoundarySequence_t: break_delay = ((finish - start) - (n_breaks * duration)) // (n_breaks+1) @@ -287,40 +315,32 @@ def present_interval( LOG.debug("trailing interval") present_intervals.append((current_time / 60, finish / 60)) return models.SpecificInterval(tuple(present_intervals)) - - def infected_present_interval(self) -> models.Interval: - if self.specific_breaks != {}: # It means the breaks are specific and not predefined - breaks = self.generate_specific_break_times(self.specific_breaks['infected_breaks']) - else: - breaks = self.infected_lunch_break_times() + self.infected_coffee_break_times() - return self.present_interval( - self.infected_start, self.infected_finish, - breaks=breaks, - ) - def population_present_interval(self) -> models.Interval: + def population_present_interval(self) -> typing.List[float]: state_change_times = set(self.infected_present_interval().transition_times()) state_change_times.update(self.exposed_present_interval().transition_times()) - all_state_changes = sorted(state_change_times) - return models.SpecificInterval(tuple(zip(all_state_changes[:-1], all_state_changes[1:]))) + return sorted(state_change_times) def exposed_present_interval(self) -> models.Interval: - if self.specific_breaks != {}: # It means the breaks are specific and not predefined - breaks = self.generate_specific_break_times(self.specific_breaks['exposed_breaks']) - else: - breaks = self.exposed_lunch_break_times() + self.exposed_coffee_break_times() + breaks = self.exposed_lunch_break_times() + self.exposed_coffee_break_times() return self.present_interval( self.exposed_start, self.exposed_finish, breaks=breaks, ) + + def infected_present_interval(self) -> models.Interval: + breaks = self.infected_lunch_break_times() + self.infected_coffee_break_times() + return self.present_interval( + self.infected_start, self.infected_finish, + breaks=breaks, + ) - def ventilation_transition_times(self, last_present_time) -> typing.Tuple[float, ...]: - if self.ventilation_type == 'from_fitting' and self.window_opening_regime == 'windows_open_periodically': - transition_times = sorted(models.PeriodicInterval(self.windows_frequency, - self.windows_duration, min(self.infected_start, self.exposed_start)/60).transition_times()) - return tuple(filter(lambda x: x <= last_present_time, transition_times)) + def ventilation_transition_times(self) -> typing.Tuple[float, ...]: + # Check what type of ventilation is considered for the fitting + if self.fitting_ventilation_type == 'fitting_natural_ventilation': + return tuple(self.fitting_ventilation_states) else: - return tuple((min(self.infected_start, self.exposed_start)/60, max(self.infected_finish, self.exposed_finish)/60), ) # all day long + return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) #: Mapping of field name to a callable which can convert values from form #: input (URL encoded arguments / string) into the correct type. diff --git a/caimira/apps/calculator/defaults.py b/caimira/apps/calculator/defaults.py index 35ced634..339a2bed 100644 --- a/caimira/apps/calculator/defaults.py +++ b/caimira/apps/calculator/defaults.py @@ -22,7 +22,6 @@ 'ceiling_height': 0., 'conditional_probability_plot': False, 'conditional_probability_viral_loads': False, - 'CO2_data_option': False, 'CO2_fitting_result': '{}', 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 97749fd2..387b8e9d 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -37,7 +37,6 @@ class FormData: ceiling_height: float conditional_probability_plot: bool conditional_probability_viral_loads: bool - CO2_data_option: bool CO2_fitting_result: dict exposed_coffee_break_option: str exposed_coffee_duration: int @@ -438,16 +437,15 @@ def outside_temp(self) -> models.PiecewiseConstant: def ventilation(self) -> models._VentilationBase: always_on = models.PeriodicInterval(period=120, duration=120) - periodic_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, min(self.infected_start, self.exposed_start)/60) - - if self.CO2_data_option: - ventilations = [] - if self.ventilation_type == 'from_fitting' and self.window_opening_regime == 'windows_open_periodically': - for index, time in enumerate(sorted(list(periodic_interval.transition_times()))[:-1]): - if index < len(self.CO2_fitting_result['ventilation_values']): - ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((time, time + self.windows_duration/60), )), - air_exch=self.CO2_fitting_result['ventilation_values'][index])) - else: break + periodic_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, + min(self.infected_start, self.exposed_start)/60) + if self.ventilation_type == 'from_fitting': + ventilations = [] + if self.CO2_fitting_result['fitting_ventilation_type'] == 'fitting_natural_ventilation': + transition_times = self.CO2_fitting_result['transition_times'] + for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): + ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), + air_exch=self.CO2_fitting_result['ventilation_values'][index])) else: ventilations.append(models.AirChange(active=always_on, air_exch=self.CO2_fitting_result['ventilation_values'][0])) return models.MultipleVentilation(tuple(ventilations)) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index b70610bf..eb33791d 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -323,7 +323,7 @@ def hour_format(hour: float) -> str: hours = int(hour) minutes = int(hour % 1 * 60) - return f"{hours}:{minutes}" + return f"{hours}:{minutes if minutes != 0 else '00'}" def percentage(absolute: float) -> float: return absolute * 100 diff --git a/caimira/models.py b/caimira/models.py index a9f2c0ec..c515144f 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1527,11 +1527,12 @@ def fun(x): exhalation_rate=exhalation_rate, ventilation_values=ventilation_values ) - return np.sqrt(np.sum((np.array(self.CO2_concentrations) - np.array(the_concentrations))**2)) - + return np.sqrt(np.sum((np.array(self.CO2_concentrations) - + np.array(the_concentrations))**2)) # The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations) - res_dict = minimize(fun=fun, x0=np.ones(len(self.ventilation_transition_times)), method='powell', bounds=[ - (0, None) for _ in range(len(self.ventilation_transition_times))], options={'xtol': 1e-3}) + res_dict = minimize(fun=fun, x0=np.ones(len(self.ventilation_transition_times)), method='powell', + bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))], + options={'xtol': 1e-3}) exhalation_rate = res_dict['x'][0] ventilation_values = res_dict['x'][1:] From 9abb4d64cf91932e8b6e43f86dd31b3edab4edaa Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 19 Jul 2023 17:29:02 +0200 Subject: [PATCH 32/61] version update and UI bugfix on form submission --- caimira/apps/calculator/__init__.py | 6 +++--- caimira/apps/templates/base/calculator.form.html.j2 | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 401849d7..3002f6f1 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -29,7 +29,7 @@ from . import markdown_tools from . import model_generator, co2_model_generator -from .report_generator import ReportGenerator, calculate_report_data +from .report_generator import ReportGenerator, calculate_report_data, img2base64, _figure2bytes from .user import AuthenticatedUser, AnonymousUser # The calculator version is based on a combination of the model version and the @@ -39,7 +39,7 @@ # 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 # increase the overall CAiMIRA version (found at ``caimira.__version__``). -__version__ = "4.13.0" +__version__ = "4.14.0" LOG = logging.getLogger(__name__) @@ -390,7 +390,7 @@ async def post(self, endpoint: str) -> None: report = await asyncio.wrap_future(report_task) result = dict(report.CO2_fit_params()) - result['fitting_ventilation_type'] = 'fitting_natural_ventilation' + result['fitting_ventilation_type'] = form.fitting_ventilation_type result['transition_times'] = report.ventilation_transition_times result['CO2_plot'] = self.generate_ventilation_plot(form.CO2_data, report.ventilation_transition_times, result['ventilation_values']) self.finish(result) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index f7a4bc91..c12cb6c0 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -344,15 +344,15 @@
    Ventilation scheme:
    - + - +
    From 580c63eb82ed4585640759378648957f5d7a8af4 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 20 Jul 2023 15:24:39 +0200 Subject: [PATCH 33/61] replaced alert message on file upload --- caimira/apps/calculator/static/js/co2_form.js | 8 ++++++-- caimira/apps/templates/base/calculator.form.html.j2 | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index bf9751bd..39596a61 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -28,9 +28,11 @@ const CO2_data_form = [ clearFittingResultComponent(); const files = document.getElementById("file_upload").files; if (files.length === 0) { - alert("Please choose any file..."); + $("#upload-error").show(); return; - } + } else { + $("#upload-error").hide(); + }; const file = files[0]; const extension = file.name.substring(file.name.lastIndexOf(".")).toUpperCase(); extension === ".XLS" || extension === ".XLSX" @@ -120,6 +122,7 @@ const CO2_data_form = [ } if (submit) { $($(obj).data('target')).modal('show'); + $("#upload-error").hide(); } return submit; } @@ -242,6 +245,7 @@ function submitFittingAlgorithm(url) { $('#fitting_ventilation_states').val(''); $('span.error_text').remove(); $('#DIVCO2_fitting_result, #CO2_input_data_div').hide(); + $('#DIVCO2_fitting_to_submit').hide(); $('#CO2_data_plot').attr('src', ''); // Update the ventilation scheme components diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index c12cb6c0..4516d187 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -297,7 +297,6 @@ -
    @@ -331,7 +330,7 @@ -
    +
    From 1fa2f8085bcdd463f7796f55de47f3d566a7d459 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 20 Jul 2023 16:10:22 +0200 Subject: [PATCH 34/61] added error handling on valid excel files and replaced plain javascript with jquery calls --- caimira/apps/calculator/static/js/co2_form.js | 40 +++++-------------- .../templates/base/calculator.form.html.j2 | 7 ++-- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 39596a61..518127c7 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -26,7 +26,7 @@ const CO2_data_form = [ // Method to upload a valid excel file function uploadFile(endpoint) { clearFittingResultComponent(); - const files = document.getElementById("file_upload").files; + const files = $("#file_upload")[0].files; if (files.length === 0) { $("#upload-error").show(); return; @@ -37,7 +37,7 @@ const CO2_data_form = [ const extension = file.name.substring(file.name.lastIndexOf(".")).toUpperCase(); extension === ".XLS" || extension === ".XLSX" ? excelFileToJSON(endpoint, file) - : alert("Please select a valid excel file."); + : $('#upload-file-extention-error').show(); } // Method to read excel file and convert it into JSON @@ -59,36 +59,21 @@ const CO2_data_form = [ // Method to display the data in HTML Table function displayJsonToHtmlTable(endpoint, jsonData) { - const table = document.getElementById("display_excel_data"); - const format = document.getElementById("CO2_data"); + // const table = $("#display_excel_data"); + const format = $("#CO2_data"); const structure = { times: [], CO2: [] }; if (jsonData.length > 0) { - let htmlData = "TimeCO2 Value"; - const jsonLength = jsonData.length; - for (let i = 0; i < jsonLength; i++) { + for (let i = 0; i < jsonData.length; i++) { const row = jsonData[i]; - if (i < 5) { - htmlData += ` - - ${row["Times"].toFixed(2)} - ${row["CO2"].toFixed(2)} - `; - } structure.times.push(row["Times"]); structure.CO2.push(row["CO2"]); } - - if (jsonLength >= 5) { - htmlData += " ... ... "; - } - format.value = JSON.stringify(structure); + format.val(JSON.stringify(structure)); $('#generate_fitting_data').prop("disabled", false); $('#fitting_ventilation_states').prop('disabled', false); $('[name=fitting_ventilation_type]').prop('disabled', false); plotCO2Data(endpoint); - } else { - table.innerHTML = "There is no data in the spreadsheet file"; - } + }; } // Method to download Excel template available on CERNBox @@ -103,15 +88,12 @@ const CO2_data_form = [ } function insertErrorFor(referenceNode, text) { - const element = document.createElement("span"); - element.setAttribute("class", "error_text"); - element.classList.add("red_text"); - element.innerHTML = "  " + text; + const element = $('').addClass('error_text red_text').html('  ' + text); $(referenceNode).before(element); } function validateFormInputs(obj) { - $('span.' + "error_text").remove(); + $('span.error_text').remove(); let submit = true; for (let i = 0; i < CO2_data_form.length; i++) { const element = $(`[name=${CO2_data_form[i]}]`)[0]; @@ -123,6 +105,7 @@ const CO2_data_form = [ if (submit) { $($(obj).data('target')).modal('show'); $("#upload-error").hide(); + $('#upload-file-extention-error').hide(); } return submit; } @@ -165,7 +148,7 @@ function displayTransitionTimesHourFormat(start, stop) { function displayFittingData(json_response) { $("#DIVCO2_fitting_result").show(); $("#CO2_data_plot").attr("src", json_response['CO2_plot']); - // Not needed for the form submit + // Not needed for the form submission delete json_response['CO2_plot']; $("#CO2_fitting_result").val(JSON.stringify(json_response)); $("#exhalation_rate_fit").html('Exhalation rate: ' + String(json_response['exhalation_rate'].toFixed(2)) + ' m³/h'); @@ -260,4 +243,3 @@ function submitFittingAlgorithm(url) { clearFittingResultComponent(); $('#CO2_data_no').click(); } - diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 4516d187..109e2c99 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -331,12 +331,11 @@ +
    - -
    +
    - +
    - +
    @@ -323,7 +323,7 @@