From 965b93177342cfece21ba0cf1ada65083846fa93 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 24 Jul 2023 11:32:36 +0200 Subject: [PATCH] validation on the excel file given as input (using SheetJS) --- caimira/apps/calculator/static/js/co2_form.js | 552 +++++++++++------- .../templates/base/calculator.form.html.j2 | 3 +- caimira/apps/templates/base/layout.html.j2 | 5 +- 3 files changed, 337 insertions(+), 223 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 4618263c..c87739ef 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -1,245 +1,357 @@ const CO2_data_form = [ - 'CO2_data', - 'exposed_coffee_break_option', - 'exposed_coffee_duration', - 'exposed_finish', - 'exposed_lunch_finish', - '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', - 'infected_finish', - 'infected_lunch_finish', - 'infected_lunch_option', - 'infected_lunch_start', - 'infected_people', - 'infected_start', - 'room_volume', - 'total_people', - 'ventilation_type', - ]; - - // Method to upload a valid excel file - function uploadFile(endpoint) { - clearFittingResultComponent(); - const files = $("#file_upload")[0].files; - if (files.length === 0) { - $("#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" - ? excelFileToJSON(endpoint, file) - : $('#upload-file-extention-error').show(); + "CO2_data", + "exposed_coffee_break_option", + "exposed_coffee_duration", + "exposed_finish", + "exposed_lunch_finish", + "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", + "infected_finish", + "infected_lunch_finish", + "infected_lunch_option", + "infected_lunch_start", + "infected_people", + "infected_start", + "room_volume", + "total_people", + "ventilation_type", +]; + +// Method to upload a valid excel file +function uploadFile(endpoint) { + clearFittingResultComponent(); + const files = $("#file_upload")[0].files; + if (files.length === 0) { + $("#upload-error").show(); + return; } - - // Method to read excel file and convert it into JSON - function excelFileToJSON(endpoint, file) { - try { - 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); - } + const file = files[0]; + const extension = file.name + .substring(file.name.lastIndexOf(".")) + .toUpperCase(); + if (extension !== ".XLS" && extension !== ".XLSX") { + $("#upload-error") + .text("Please select a valid excel file (.XLS or .XLSX).") + .show(); + return; } - - // Method to display the data in HTML Table - function displayJsonToHtmlTable(endpoint, jsonData) { - // const table = $("#display_excel_data"); - const format = $("#CO2_data"); - const structure = { times: [], CO2: [] }; - if (jsonData.length > 0) { - for (let i = 0; i < jsonData.length; i++) { - const row = jsonData[i]; - structure.times.push(row["Times"]); - structure.CO2.push(row["CO2"]); - } - 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); - }; + + // FileReader API to read the Excel file + const reader = new FileReader(); + reader.onload = function (event) { + const fileContent = event.target.result; + const workbook = XLSX.read(fileContent, { type: "binary" }); + + // Assuming the first sheet is the one we want to validate + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Check if the headers match the expected format + const headerCoordinates = { + Times: "A1", + CO2: "B1", + }; + for (const header in headerCoordinates) { + const cellValue = worksheet[headerCoordinates[header]]?.v; + if ( + !cellValue || + cellValue.trim().toLowerCase() !== header.toLowerCase() + ) { + $("#upload-error") + .text(`The file does not have the expected header "${header}".`) + .show(); + return; + } + } + + // Check if there is any data below the header row + if (data.length <= 1) { + $("#upload-error") + .text( + "The Excel file is empty. Please make sure it contains data below the header row." + ) + .show(); + return; + } + + // Validate data in the columns + const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false }); + const timesColumnIndex = 0; + const CO2ColumnIndex = 1; + for (let i = 1; i < data.length; i++) { + try { + const timesCellValue = parseFloat(data[i][timesColumnIndex]); + const CO2CellValue = parseFloat(data[i][CO2ColumnIndex]); + + if (isNaN(timesCellValue) || isNaN(CO2CellValue)) { + throw new Error("Invalid data in the Times or CO2 columns."); + } + } catch (error) { + $("#upload-error") + .text( + "Invalid data in the Times or CO2 columns. Please make sure they contain only float values." + ) + .show(); + return; + } + } + + // Call function to convert Excel file to JSON and further processing + try { + generateJSONStructure(endpoint, data); + // If all validations pass, process the file here or display a success message + $("#upload-file-extention-error").hide(); + } catch (error) { + console.log(error); + } + }; + reader.readAsBinaryString(file); // Read the file as a binary string +} + +// Method to generate the JSON structure +function generateJSONStructure(endpoint, jsonData) { + const inputToPopulate = $("#CO2_data"); + + // Initialize the final structure + const finalStructure = { times: [], CO2: [] }; + + if (jsonData.length > 0) { + // Loop through the input dataArray and extract the values starting from the second array (index 1) + for (let i = 1; i < jsonData.length; i++) { + const arr = jsonData[i]; + // Assuming arr contains two float values + finalStructure.times.push(parseFloat(arr[0])); + finalStructure.CO2.push(parseFloat(arr[1])); + } + inputToPopulate.val(JSON.stringify(finalStructure)); + $("#generate_fitting_data").prop("disabled", false); + $("#fitting_ventilation_states").prop("disabled", false); + $("[name=fitting_ventilation_type]").prop("disabled", false); + plotCO2Data(endpoint); } - - // 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; +} + +// 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) { + const element = $("") + .addClass("error_text text-danger") + .html("  " + text); + $(referenceNode).before(element); +} + +function validateFormInputs(obj) { + $("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]; + if (element.name !== "fitting_ventilation_states" && element.value === "") { + insertErrorFor( + $("#DIVCO2_data_dialog"), + `'${element.name}' must be defined.
` + ); + submit = false; + } } - - function insertErrorFor(referenceNode, text) { - const element = $('').addClass('error_text text-danger').html('  ' + text); - $(referenceNode).before(element); + if (submit) { + $($(obj).data("target")).modal("show"); + $("#upload-error").hide(); + $("#upload-file-extention-error").hide(); } - - function validateFormInputs(obj) { - $('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]; - 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'); - $("#upload-error").hide(); - $('#upload-file-extention-error').hide(); - } - return submit; - } + 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; - }; - }; + let submit = true; + if (validateFormInputs($("#button_fit_data"))) submit = true; - return submit; + // 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 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'); + 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 displayFittingData(json_response) { - $("#DIVCO2_fitting_result").show(); - $("#CO2_data_plot").attr("src", json_response['CO2_plot']); - // 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'); - let ventilation_table = "Time (HH:MM)ACH value (h⁻¹)"; - 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(); + $("#DIVCO2_fitting_result").show(); + $("#CO2_data_plot").attr("src", json_response["CO2_plot"]); + // 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" + ); + let ventilation_table = + "Time (HH:MM)ACH value (h⁻¹)"; + 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 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; + 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 => $("#CO2_data_plot").attr("src", json_response['CO2_plot'])) - .then($('#DIVCO2_fitting_to_submit').show()) - .catch(error => console.log(error)) - ); - } + 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) => + $("#CO2_data_plot").attr("src", json_response["CO2_plot"]) + ) + .then($("#DIVCO2_fitting_to_submit").show()) + .catch((error) => console.log(error)) + ); + } } 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(''); - $('#CO2_data').val('{}'); - $('#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 - $('#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(); + 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(""); + $("#CO2_data").val("{}"); + $("#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 + $("#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/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 91be606c..7b7d0d6b 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -330,8 +330,7 @@ - - +

diff --git a/caimira/apps/templates/base/layout.html.j2 b/caimira/apps/templates/base/layout.html.j2 index 73d7637d..5ea37537 100644 --- a/caimira/apps/templates/base/layout.html.j2 +++ b/caimira/apps/templates/base/layout.html.j2 @@ -110,7 +110,10 @@ - + + + + {% block body_scripts %} {% endblock body_scripts %}