diff --git a/VIPSWeb/static/js/util.js b/VIPSWeb/static/js/util.js index 8712370441ba851dd5f2f844c20c4282e0a5b19d..e58eb5d30bf1ee535dc52767f585972d115ab3fc 100644 --- a/VIPSWeb/static/js/util.js +++ b/VIPSWeb/static/js/util.js @@ -69,4 +69,48 @@ function getElementComputedDisplay(theElement) { return theElement.currentStyle ? theElement.currentStyle.display : getComputedStyle(theElement, null).display; -} \ No newline at end of file +} + +/* Sorting functions used on JSON objects returned from VIPSLogic */ + +/** + * Comparing weather stations alphabetically by name + */ +var compareWeatherStations = function(a,b) +{ + return compareStrings[a["name"],b["name"]] +} + +/** + * Comparing preparations alphabetically + */ +var comparePreparations = function(a,b) +{ + return compareStrings(a["preparationName"],b["preparationName"]); +} + +var compareStrings = function(a,b) +{ + if(a < b) + { + return -1; + } + if(a > b) + { + return 1; + } + return 0; +} + +var compareForecastResults = function(a,b) +{ + if(a["resultValidTime"] < b["resultValidTime"]) + { + return -1; + } + if(a["resultValidTime"] > b["resultValidTime"]) + { + return 1; + } + return 0; +} diff --git a/VIPSWeb/static/js/validateForm.js b/VIPSWeb/static/js/validateForm.js new file mode 100644 index 0000000000000000000000000000000000000000..72b6aaff9108a5a436713213667f83b603c37c2b --- /dev/null +++ b/VIPSWeb/static/js/validateForm.js @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2015 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSWeb. + * VIPSWeb is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSWeb. If not, see <http://www.nibio.no/licenses/>. + * + */ + + +// Constant defs for field types. +var dataTypes = { + TYPE_STRING: "STRING", + TYPE_INTEGER: "INTEGER", + TYPE_DOUBLE: "DOUBLE", + TYPE_DATE: "DATE", + TYPE_TIMESTAMP: "TIMESTAMP", + TYPE_PASSWORD: "PASSWORD", + TYPE_EMAIL: "EMAIL", + TYPE_POINT_WGS84: "POINT_WGS84" + + +}; + +var fieldTypes= { + TYPE_SELECT_SINGLE: "SELECT_SINGLE", + TYPE_SELECT_MULTIPLE: "SELECT_MULTIPLE", + TYPE_INPUT: "INPUT" +}; + + +var defaultValues = { + DEFAULT_DATE_FORMAT: "YYYY-MM-DD" +}; + +// Primary field relates to secondary field as TYPE +var relationTypes = { + RELATION_TYPE_EQUALS: "EQUALS", + RELATION_TYPE_AFTER: "AFTER" +}; + +// Storage for all form definitions for current page +var formDefinitions = {}; + +// The regular expression for email strings +// Checks that string contains at least one ".", an "@" symbol and an +// appropriate top-level domain suffix (e.g. .com, .net, etc) +// Stolen from this web page: http://www.designchemical.com/blog/index.php/jquery/email-validation-using-jquery/ +var emailReg = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/; + +/** + * Validation function for all forms + * @copyright 2013 <a href="http://www.nibio.no/">NIBIO</a> + * @author Tor-Einar Skog <tor-einar.skog@bioforsk.no> + * + * It depends on the following: + * <ul> + * <li>theForm - the form to validate, passed in to the function by reference</li> + * <li>The form must have an id that corresponds to a JSON file accessible in "/formdefinitions/[form_id].json"</li> + * <li>A DOM element with id "errorMsgEl" for where to put validation error messages</li> + * <li>jQuery for Ajax calls</li> + * <li><a href="http://momentjs.com/">moment.js</a> for date validation</li> + * </ul> + * + * This function prepares for the validateFormActual function. + * + * @param {Element} theForm the form to validate + * @param {String} formDefinitionKey + * @return {boolean} If the form is valid or not + */ +function validateForm(theForm, formDefinitionKey) +{ + var isValid = true; + if(formDefinitionKey === null || formDefinitionKey === undefined) + { + formDefinitionKey = theForm.id; + } + var formDefinition = formDefinitions[formDefinitionKey]; + // Iterate through fields in form definition + for(var i in formDefinition.fields){ + var fieldDefinition = formDefinition.fields[i]; + if(!validateFieldActual(theForm[fieldDefinition.name], theForm, formDefinitionKey)){ + //alert("Validation failed for " + fieldDefinition.name); + isValid = false; + } + } + + // Check relations + if(formDefinition.relations !== undefined && formDefinition.relations !== null) + { + for(var i in formDefinition.relations) + { + var item = formDefinition.relations[i]; + var primaryFieldWebValue = theForm[item.primaryField].value; + var secondaryFieldWebValue = theForm[item.secondaryField].value; + if(primaryFieldWebValue === null || secondaryFieldWebValue === null) + { + continue; + } + + // String equality NOW! + //console.log("relationTYpe=" + item.relationType); + if(item.relationType === relationTypes.RELATION_TYPE_EQUALS) + { + if(primaryFieldWebValue !== secondaryFieldWebValue) + { + isValid = false; + invalidizeField(theForm[item.primaryField], theForm, getI18nMsg("xIsNotEqualToY",[ + getI18nMsg(item.primaryField), + getI18nMsg(item.secondaryField).toLowerCase() + ])); + } + } + // Primary field is expected to be a date AFTER secondary key + else if(item.relationType === relationTypes.RELATION_TYPE_AFTER) + { + if(!moment(primaryFieldWebValue).isAfter(moment(secondaryFieldWebValue))) + { + isValid = false; + invalidizeField(theForm[item.primaryField], theForm, getI18nMsg("xIsNotAfterY",[ + getI18nMsg(item.primaryField), + getI18nMsg(item.secondaryField).toLowerCase() + ])); + } + } + //console.log(item.primaryField); + } + } + + //console.log("isValid=" + isValid); + return isValid; +} + +/** + * Attempts to load and index the requested form definition + * @param {String} formDefinitionKey + * @param {String} path if set, the form is placed outside the standard path + * @returns {void} + */ +function loadFormDefinition(formDefinitionKey, path) +{ + if(path === null || path === undefined) + { + path = "/formdefinitions/"; + } + + $.getJSON( path + formDefinitionKey + ".json") + .done(function(json){ + formDefinitions[formDefinitionKey] = json; + }) + .fail(function() { + alert("Error getting form definition with key=" + formDefinitionKey); + }); +} + +/** + * Prepares for the validation of a field + * @param {DOMElement} fieldEl + * @param {String} formDefinitionKey + * @returns {void} + */ +function validateField(fieldEl, formDefinitionKey) +{ + // Which form does this element belong to? + var theForm = getFormForField(fieldEl); + if(formDefinitionKey === null || formDefinitionKey === undefined) + { + formDefinitionKey = theForm.id; + } + // If form definition is null, get it before we proceed + if(formDefinitions[formDefinitionKey] === undefined) + { + $.getJSON( "/formdefinitions/" + theForm.id + ".json") + .done(function(json){ + formDefinitions[theForm.id] = json; + return validateFieldActual(fieldEl, theForm, formDefinitionKey); + }); + } + else + { + return validateFieldActual(fieldEl, theForm, formDefinitionKey); + } +} + +/** + * Returns the requested field definition in a given form definition + * @param {type} fieldName + * @param {type} formDefinition + * @returns {unresolved} + */ +function getFieldDefinition(fieldName, formDefinition) +{ + for(i in formDefinition.fields) + { + if(formDefinition.fields[i].name === fieldName) + { + return formDefinition.fields[i]; + } + } + return null; +} + +/** + * Uniform way of getting the element where to put the evaluation message + * @param {Element} fieldEl + * @param {Element} theForm + * @returns {Element} + */ +function getValidationOutputEl(fieldEl, theForm) +{ + return document.getElementById(theForm.id + "_" + fieldEl.name + "_validation"); +} + +/** + * Performs the actual field validation + * @param {Element} fieldEl the form element to be validated + * @param {Element} theForm the form + * @param {String} formDefinitionKey optional key to a formdefinition for a part of the form that has been dynamically added to the main form + * @returns {boolean} + */ +function validateFieldActual(fieldEl, theForm, formDefinitionKey) +{ + var webValue = fieldEl.value; + + var fieldDefinition = getFieldDefinition(fieldEl.name, formDefinitions[formDefinitionKey !== null ? formDefinitionKey : theForm.id]); + + //console.log(fieldDefinition); + //console.log(webValue); + // Empty? + if(webValue === null || webValue === "") + { + if(fieldDefinition.required === true) + { + invalidizeField(fieldEl, theForm, getI18nMsg("Field is required",null)); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + + + // Single select field - check for nullValue + if(fieldDefinition.fieldType === fieldTypes.TYPE_SELECT_SINGLE) + { + webValue = fieldEl.options[fieldEl.selectedIndex].value; + if(fieldDefinition.nullValue === webValue && fieldDefinition.required === true) + { + invalidizeField(fieldEl, theForm, getI18nMsg("Field is required",null)); + return false; + } + else + { + validizeField(fieldEl, theForm); + // We don't return yet, because we want to validate the data type + } + } + + // Multiple select field + // If required - check that at least one item has been selected + if(fieldDefinition.fieldType === fieldTypes.TYPE_SELECT_MULTIPLE) + { + if(fieldDefinition.required) + { + var selectedOptions = 0; + for(var i=0;i<fieldEl.options.length;i++) + { + selectedOptions += fieldEl.options[i].selected ? 1 : 0; + } + if(selectedOptions === 0) + { + invalidizeField(fieldEl, theForm, getI18nMsg("fieldIsRequired",null)); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + else + { + return true; + } + } + + // STRINGS + // Strings are checked for length + if(fieldDefinition.dataType === dataTypes.TYPE_STRING) + { + if(fieldDefinition.maxLength !== null && fieldDefinition.maxLength < webValue.length) + { + invalidizeField(fieldEl, theForm, getI18nMsg("exceedsMaxLengthOf",[fieldDefinition.maxLength])); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + // Dates: check date format + if(fieldDefinition.dataType === dataTypes.TYPE_DATE) + { + var validFormat = (fieldDefinition.dateFormat !== null && fieldDefinition.dateFormat !== undefined) ? fieldDefinition.dateFormat : defaultValues.DEFAULT_DATE_FORMAT; + if(!moment(webValue, validFormat.toUpperCase(),true).isValid()) + { + invalidizeField(fieldEl, theForm, getI18nMsg("doesNotMatchFormatX",[validFormat])); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + if(fieldDefinition.dataType === dataTypes.TYPE_TIMESTAMP) + { + var validDateFormat = (fieldDefinition.dateFormat !== null && fieldDefinition.dateFormat !== undefined) ? + fieldDefinition.dateFormat : defaultValues.DEFAULT_DATE_FORMAT; + var validTimestampFormat = (fieldDefinition.timestampFormatMomentJS !== null && fieldDefinition.timestampFormatMomentJS !== undefined) ? + fieldDefinition.timestampFormatMomentJS: validDateFormat.toUpperCase() + "THH:mm:ssZZ"; + if(!moment(webValue, validTimestampFormat, true).isValid()) + { + invalidizeField(fieldEl, theForm, getI18nMsg("doesNotMatchFormatX",[validTimestampFormat])); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + // Check email format + // We use a simple regExp for this + // TODO: This may acutally call a REST service for uniform checking with Java code + if(fieldDefinition.dataType === dataTypes.TYPE_EMAIL) + { + if(!emailReg.test(webValue)) + { + invalidizeField(fieldEl, theForm, getI18nMsg("invalidFormat",null)); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + // NUMBERS + // TODO: INTEGERS should not have decimal values + if(fieldDefinition.dataType === dataTypes.TYPE_INTEGER || fieldDefinition.dataType === dataTypes.TYPE_DOUBLE) + { + if(!$.isNumeric(webValue.replace(",","."))) + { + invalidizeField(fieldEl, theForm, getI18nMsg("Number required",null)); + return false; + } + else if(fieldDefinition.minValue !== null && webValue < fieldDefinition.minValue) + { + invalidizeField(fieldEl, theForm, getI18nMsg("Lower than mMinimum",[fieldDefinition.minValue])); + return false; + } + else if(fieldDefinition.maxValue !== null && webValue > fieldDefinition.maxValue) + { + invalidizeField(fieldEl, theForm, getI18nMsg("Higher than maximum",[fieldDefinition.maxValue])); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } + + // GIS + // Point in WGS84 format + // Format [Double], [Double] + // TODO Check for minimum and maximum values + if(fieldDefinition.dataType === dataTypes.TYPE_POINT_WGS84) + { + var possibleDoubles = webValue.split(","); + if(possibleDoubles.length < 2) + { + invalidizeField(fieldEl, theForm, getI18nMsg("missingSeparatorComma")); + return false; + } + else if(possibleDoubles.length > 2) + { + invalidizeField(fieldEl, theForm, getI18nMsg("tooManySeparatorCommas")); + return false; + } + else + { + for(index in possibleDoubles) + { + if(!$.isNumeric(possibleDoubles[index])) + { + invalidizeField(fieldEl, theForm, getI18nMsg("invalidFormat")); + return false; + } + } + } + validizeField(fieldEl, theForm); + return true; + } + + // Passwords are evaluated in evaluatePassword + if(fieldDefinition.dataType === dataTypes.TYPE_PASSWORD) + { + return true; + } + + alert("ERROR: Field " + fieldEl.name + " is of type " + fieldDefinition.type + ", and evaluation of that type is not implemented."); + return false; +} + +/** + * Recursive function to travers upwards in tree until we find the form + * for the given element + * @param {DOMElement} fieldEl + * @returns {DOMelement} the form + */ +function getFormForField(fieldEl) +{ + // Stop condition + if(fieldEl.tagName.toLowerCase() === "form") + { + return fieldEl; + } + else + { + if(fieldEl.parentNode === null || fieldEl.parentNode === undefined) + { + alert("Could not find a form!"); + return null; + } + return getFormForField(fieldEl.parentNode); + } +} + +/** + * Sends a request to server and gets password evaluation. + * If not OK, highlights field and displays error (somehow) + * @param {DOMElement} passwordEl + */ +function evaluatePassword(passwordEl) +{ + // Get ID for evaluation output + var validationOutputEl = getValidationOutputEl(passwordEl, getFormForField(passwordEl)); + var passwordAttempt = passwordEl.value; + // If empty, we return immediately + if(passwordAttempt === null || passwordAttempt === "") + { + validationOutputEl.innerHTML = getI18nMsg("fieldIsRequired",null); + styleInvalid(passwordEl); + return; + } + + $.ajax({ + type:"GET", + url: "/rest/evaluatepassword/" + passwordAttempt, + statusCode:{ + 200: function(data,textStatus, jqXHR){ + if(data === "true") + { + validationOutputEl.innerHTML=""; + styleValid(passwordEl); + } + else + { + validationOutputEl.innerHTML=data.replace(/\n/g, "<br/>"); + styleInvalid(passwordEl); + } + }, + 404: function(jqXHR,textStatus,errorThrown){ + alert ("Error: " + jqXHR.responseText); + }, + 500: function(jqXHR,textStatus,errorThrown){ + alert ("Error: " + jqXHR.responseText); + } + } + }); +} + +/** + * Marks a field as invalid. Adds validation error message + * @param {Element} inputEl + * @param {Element} theForm + * @param {String} message the validation error message + * @returns {void} + */ +function invalidizeField(inputEl, theForm, message) +{ + // Make sure we don't try to manipulate hidden fields + if(inputEl.type === "hidden") + { + return; + } + styleInvalid(theForm[inputEl.name]); + getValidationOutputEl(inputEl, theForm).innerHTML = message; +} + +/** + * + * @param {type} inputEl + * @param {type} theForm + * @returns {undefined} + */ +function validizeField(inputEl, theForm) +{ + // Make sure we don't try to manipulate hidden fields + if(inputEl.type === "hidden") + { + return; + } + styleValid(theForm[inputEl.name]); + getValidationOutputEl(inputEl, theForm).innerHTML = ""; +} + +/** + * + * @param {type} inputEl + * @returns {undefined} + */ +function styleInvalid(inputEl) +{ + $(inputEl.parentNode).addClass("has-error"); +} + +function styleValid(inputEl) +{ + $(inputEl.parentNode).removeClass("has-error"); +} + +/** + * Gets a message by key. The message should be translated + * Depends on /js/resourcebundle.js for the dictionary i18Texts + * @param {String} msgKey + * @param {Array} positionalArguments - for string substitution in message + * @returns {String} the message + */ +function getI18nMsg(msgKey,positionalArguments) +{ + return replaceParams(gettext(msgKey), positionalArguments); +} + +/** + * Mimic of the MessageFormat.format method in Java. + * Very nicely explained here: http://stackoverflow.com/questions/1353408/messageformat-in-javascript-parameters-in-localized-ui-strings + * @param {String} string + * @param {Array} replacements + * @returns {String} + */ +function replaceParams(string, replacements) { + return string.replace(/\{(\d+)\}/g, function() { + return replacements[arguments[1]]; + }); +} \ No newline at end of file diff --git a/cerealblotchmodels/locale/nb/LC_MESSAGES/django.mo b/cerealblotchmodels/locale/nb/LC_MESSAGES/django.mo index 9ce97966df7f7f6c232471318e8bde395d8ed13a..bdf1de024f2c80855615a3b84e7611864ff0a126 100644 Binary files a/cerealblotchmodels/locale/nb/LC_MESSAGES/django.mo and b/cerealblotchmodels/locale/nb/LC_MESSAGES/django.mo differ diff --git a/cerealblotchmodels/locale/nb/LC_MESSAGES/django.po b/cerealblotchmodels/locale/nb/LC_MESSAGES/django.po index 94e91dcfe43cecce5f9e6d7ff128d66aece5baaf..deb28952733d84163a7b4a42134ba6e3e3fe3a1c 100644 --- a/cerealblotchmodels/locale/nb/LC_MESSAGES/django.po +++ b/cerealblotchmodels/locale/nb/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-05-29 16:02+0200\n" +"POT-Creation-Date: 2015-06-03 10:11+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -27,92 +27,110 @@ msgstr "Byggbrunflekk" msgid "Background data for the barley net blotch model" msgstr "Bakgrunnsdata for byggbrunflekkmodellen" -#: templates/cerealblotchmodels/barleynetblotchform.html:39 +#: templates/cerealblotchmodels/barleynetblotchform.html:40 msgid "Weather station" msgstr "Målestasjon" -#: templates/cerealblotchmodels/barleynetblotchform.html:42 +#: templates/cerealblotchmodels/barleynetblotchform.html:46 msgid "Sowing date" msgstr "Sådato" -#: templates/cerealblotchmodels/barleynetblotchform.html:44 +#: templates/cerealblotchmodels/barleynetblotchform.html:51 msgid "Crop" msgstr "Kultur" -#: templates/cerealblotchmodels/barleynetblotchform.html:48 +#: templates/cerealblotchmodels/barleynetblotchform.html:58 msgid "Same crop as last season" msgstr "Samme kultur som i fjor" -#: templates/cerealblotchmodels/barleynetblotchform.html:50 +#: templates/cerealblotchmodels/barleynetblotchform.html:63 msgid "Plowed" msgstr "Pløyd" -#: templates/cerealblotchmodels/barleynetblotchform.html:53 +#: templates/cerealblotchmodels/barleynetblotchform.html:69 msgid "Observation date" msgstr "Observasjonsdato" -#: templates/cerealblotchmodels/barleynetblotchform.html:55 -#, fuzzy, python-format +#: templates/cerealblotchmodels/barleynetblotchform.html:74 +#, fuzzy msgid "%% Infected leaves" msgstr "% infiserte blad" -#: templates/cerealblotchmodels/barleynetblotchform.html:57 +#: templates/cerealblotchmodels/barleynetblotchform.html:79 msgid "Spraying date" msgstr "Sprøytedato" -#: templates/cerealblotchmodels/barleynetblotchform.html:59 +#: templates/cerealblotchmodels/barleynetblotchform.html:84 msgid "Preparation" msgstr "Plantevernmiddel" -#: templates/cerealblotchmodels/barleynetblotchform.html:62 +#: templates/cerealblotchmodels/barleynetblotchform.html:90 msgid "Preparation dose" msgstr "Dose" -#: templates/cerealblotchmodels/barleynetblotchform.html:69 +#: templates/cerealblotchmodels/barleynetblotchform.html:99 msgid "Run model" msgstr "Kjør modell" -#: templates/cerealblotchmodels/barleynetblotchform.html:76 +#: templates/cerealblotchmodels/barleynetblotchform.html:106 msgid "Model is running, please wait" msgstr "Modellen kjører, vennligst vent" -#: templates/cerealblotchmodels/barleynetblotchform.html:132 +#: templates/cerealblotchmodels/barleynetblotchform.html:188 +msgid "Temperature, daily mean" +msgstr "Temperatur, døgngjennomsnitt" + +#: templates/cerealblotchmodels/barleynetblotchform.html:189 +msgid "Rain last 28 days" +msgstr "Regn siste 28 døgn" + +#: templates/cerealblotchmodels/barleynetblotchform.html:190 +msgid "Rain, daily" +msgstr "Regn, daglig" + +#: templates/cerealblotchmodels/barleynetblotchform.html:191 +msgid "Day degrees since sowing" +msgstr "Døgngrader siden såing" + +#: templates/cerealblotchmodels/barleynetblotchform.html:192 +#: templates/cerealblotchmodels/barleynetblotchform.html:253 +msgid "Threshold" +msgstr "Terskelverdi" + +#: templates/cerealblotchmodels/barleynetblotchform.html:193 +#: templates/cerealblotchmodels/barleynetblotchform.html:254 +msgid "Disease" +msgstr "Sykdom" + +#: templates/cerealblotchmodels/barleynetblotchform.html:200 msgid "Time" msgstr "Tid" -#: templates/cerealblotchmodels/barleynetblotchform.html:140 +#: templates/cerealblotchmodels/barleynetblotchform.html:208 msgid "Warning status" msgstr "Varselstatus" -#: templates/cerealblotchmodels/barleynetblotchform.html:164 +#: templates/cerealblotchmodels/barleynetblotchform.html:237 msgid "No data returned" msgstr "Ingen data returnert" -#: templates/cerealblotchmodels/barleynetblotchform.html:175 +#: templates/cerealblotchmodels/barleynetblotchform.html:250 msgid "Disease value" msgstr "Sykdomsverdi" -#: templates/cerealblotchmodels/barleynetblotchform.html:178 -msgid "Threshold" -msgstr "Terskelverdi" - -#: templates/cerealblotchmodels/barleynetblotchform.html:179 -msgid "Disease" -msgstr "Sykdom" - -#: templates/cerealblotchmodels/barleynetblotchform.html:182 +#: templates/cerealblotchmodels/barleynetblotchform.html:257 msgid "Barley net blotch development" msgstr "Utvikling av byggbrunflekk" -#: templates/cerealblotchmodels/barleynetblotchform.html:194 +#: templates/cerealblotchmodels/barleynetblotchform.html:269 msgid "Select weather station" msgstr "Velg målestasjon" -#: templates/cerealblotchmodels/barleynetblotchform.html:220 +#: templates/cerealblotchmodels/barleynetblotchform.html:296 msgid "Select crop" msgstr "Velg kultur" -#: templates/cerealblotchmodels/barleynetblotchform.html:247 +#: templates/cerealblotchmodels/barleynetblotchform.html:324 msgid "Select preparation" msgstr "Velg plantevernmiddel" diff --git a/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/barleyNetBlotchForm.json b/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/barleyNetBlotchForm.json new file mode 100644 index 0000000000000000000000000000000000000000..3de2dd8a633d0856ab88072f0ef614c1007204c7 --- /dev/null +++ b/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/barleyNetBlotchForm.json @@ -0,0 +1,78 @@ +{ + "_licenseNote": [ + "Copyright (c) 2014 Bioforsk <http://www.bioforsk.no/>. ", + "", + "This file is part of VIPSLogic. ", + "VIPSLogic is free software: you can redistribute it and/or modify ", + "it under the terms of the Bioforsk Open Source License as published by ", + "Bioforsk, either version 1 of the License, or (at your option) any ", + "later version. ", + "", + "VIPSLogic is distributed in the hope that it will be useful, ", + "but WITHOUT ANY WARRANTY; without even the implied warranty of ", + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ", + "Bioforsk Open Source License for more details. ", + "", + "You should have received a copy of the Bioforsk Open Source License ", + "along with VIPSLogic. If not, see <http://www.bioforsk.no/licenses/>. " + ], + "_comment" : "Structure of the observationForm and how to validate it", + "fields": [ + { + "name" : "weatherStationId", + "dataType" : "INTEGER", + "fieldType" : "SELECT_SINGLE", + "required" : true, + "nullValue" : "-1" + }, + { + "name" : "sowingDate", + "dataType" : "DATE", + "required" : true + }, + { + "name" : "cropOrganismId", + "dataType" : "INTEGER", + "fieldType" : "SELECT_SINGLE", + "required" : true, + "nullValue" : "-1" + }, + { + "name" : "sameCropAsLastSeason", + "dataType" : "STRING", + "required" : false + }, + { + "name" : "plowed", + "dataType" : "STRING", + "required" : false + }, + { + "name" : "observationDate", + "dataType" : "DATE", + "required" : false + }, + { + "name" : "observationValue", + "dataType" : "DOUBLE", + "required" : false + }, + { + "name" : "sprayingDate", + "dataType" : "DATE", + "required" : false + }, + { + "name" : "preparationId", + "dataType" : "INTEGER", + "fieldType" : "SELECT_SINGLE", + "required" : false, + "nullValue" : "-1" + }, + { + "name" : "preparationDose", + "dataType" : "DOUBLE", + "required" : false + } + ] +} diff --git a/cerealblotchmodels/templates/cerealblotchmodels/barleynetblotchform.html b/cerealblotchmodels/templates/cerealblotchmodels/barleynetblotchform.html index e4eb874f8be1eb50019d6353d044873c63d0b755..40efecc5f6c1b38d2e62895b5bede9450c8ae6e5 100644 --- a/cerealblotchmodels/templates/cerealblotchmodels/barleynetblotchform.html +++ b/cerealblotchmodels/templates/cerealblotchmodels/barleynetblotchform.html @@ -22,51 +22,81 @@ {% endcomment %} {% load i18n %} -{% block title%}{% trans "Barley net blotch" %}{%endblock%} +{% block title %}{% trans "Barley net blotch" %}{% endblock %} {% block content %} <h1>{% trans "Barley net blotch" %}</h1> -<form role="form" id="barleynetblotchForm"> +<form role="form" id="{{ form_id }}"> <div class="row"> <div class="col-md-12"> <h2>{% trans "Background data for the barley net blotch model" %}</h2> </div> </div> <div class="row"> - <div class="col-md-6 form-group"> + <div class="col-md-6"> <input type="hidden" name="timeZone" value="Europe/Oslo"/> - <label for="weatherStationId">{% trans "Weather station" %}</label> - <select name="weatherStationId" id="weatherStationId" class="form-control"> - </select> - <label for="sowingDate">{% trans "Sowing date" %}</label> - <input type="date" name="sowingDate" class="form-control"/> - <label for="cropOrganismId">{% trans "Crop" %}</label> - <select name="cropOrganismId" id="cropOrganismId" class="form-control"> - </select> - <input type="checkbox" name="sameCropAsLastSeason"/> - <label for="sameCropAsLastSeason">{% trans "Same crop as last season" %}</label><br/> - <input type="checkbox" name="plowed"/> - <label for="plowed"><span>{% trans "Plowed" %}</span></label><br/> + <div class="form-group"> + <label for="weatherStationId">{% trans "Weather station" %}</label> + <select name="weatherStationId" id="weatherStationId" class="form-control" onblur="validateField(this);"> + </select> + <span class="help-block" id="{{ form_id }}_weatherStationId_validation"></span> + </div> + <div class="form-group"> + <label for="sowingDate">{% trans "Sowing date" %}</label> + <input type="date" name="sowingDate" class="form-control" onblur="validateField(this);"/> + <span class="help-block" id="{{ form_id }}_sowingDate_validation"></span> + </div> + <div class="form-group"> + <label for="cropOrganismId">{% trans "Crop" %}</label> + <select name="cropOrganismId" id="cropOrganismId" class="form-control" onblur="validateField(this);"> + </select> + <span class="help-block" id="{{ form_id }}_cropOrganismId_validation"></span> + </div> + <div class="form-group"> + <input type="checkbox" name="sameCropAsLastSeason"/> + <label for="sameCropAsLastSeason">{% trans "Same crop as last season" %}</label><br/> + <span class="help-block" id="{{ form_id }}_sameCropAsLastSeason_validation"></span> + </div> + <div class="form-group"> + <input type="checkbox" name="plowed"/> + <label for="plowed"><span>{% trans "Plowed" %}</span></label><br/> + <span class="help-block" id="{{ form_id }}_plowed_validation"></span> + </div> </div> <div class="col-md-6 form-group"> - <label for="observationDate">{% trans "Observation date" %}</label> - <input type="date" name="observationDate" class="form-control"/> - <label for ="observationValue">{% trans "% Infected leaves" %}</label> - <input type="number" name="observationValue" class="form-control"/> - <label for="sprayingDate">{% trans "Spraying date" %}</label> - <input type="date" name="sprayingDate" class="form-control"/> - <label for="preparationId">{% trans "Preparation" %}</label> - <select name="preparationId" id="preparationId" class="form-control"> - </select> - <label for="preparationDose">{% trans "Preparation dose" %} (ml/daa)</label> - <input type="number" name="preparationDose" class="form-control"/> + <div class="form-group"> + <label for="observationDate">{% trans "Observation date" %}</label> + <input type="date" name="observationDate" class="form-control"/> + <span class="help-block" id="{{ form_id }}_observationDate_validation"></span> + </div> + <div class="form-group"> + <label for ="observationValue">{% trans "% Infected leaves" %}</label> + <input type="number" name="observationValue" class="form-control" min="0" max="100"/> + <span class="help-block" id="{{ form_id }}_observationValue_validation"></span> + </div> + <div class="form-group"> + <label for="sprayingDate">{% trans "Spraying date" %}</label> + <input type="date" name="sprayingDate" class="form-control"/> + <span class="help-block" id="{{ form_id }}_sprayingDate_validation"></span> + </div> + <div class="form-group"> + <label for="preparationId">{% trans "Preparation" %}</label> + <select name="preparationId" id="preparationId" class="form-control"> + </select> + <span class="help-block" id="{{ form_id }}_preparationId_validation"></span> + </div> + <div class="form-group"> + <label for="preparationDose">{% trans "Preparation dose" %} (ml/daa)</label> + <input type="number" name="preparationDose" class="form-control"/> + <span class="help-block" id="{{ form_id }}_preparationDose_validation"></span> + </div> </div> </div> <div class="row"> <div class="col-md-6 form-group"> - <button type="button" class="btn btn-default" onclick="runModel();">{% trans "Run model" %}</button> + <button type="button" class="btn btn-default" onclick="if(validateForm(document.getElementById('{{ form_id }}')) & validateFormExtra()){runModel();}">{% trans "Run model" %}</button> </div> </div> @@ -88,20 +118,46 @@ <table id="resultsTable" class="table table-striped"></table> </div> </div> + {% endblock %} {% block customJS %} +<script type="text/javascript" src="{% url "django.views.i18n.javascript_catalog" %}"></script> <script type="text/javascript" src="{% url "views.settings_js" %}"></script> <script type="text/javascript" src="{% static "js/3rdparty/moment.min.js" %}"></script> <script type="text/javascript" src="{% static "js/3rdparty/highcharts.js" %}"></script> <script type="text/javascript" src="{% static "js/util.js" %}"></script> +<script type="text/javascript" src="{% static "js/validateForm.js" %}"></script> <script type="text/javascript" src="{% static "organisms/organismsUtil.js" %}"></script> <script type="text/javascript" src="{% static "forecasts/js/forecasts.js" %}"></script> <script type="text/javascript"> + function validateFormExtra() + { + var theForm = document.getElementById("{{ form_id }}"); + // Observation: Either no fields or all fields must be set + if(theForm["observationDate"].value.trim() == "" ^ theForm["observationValue"].value.trim() == "") + { + alert(gettext("Missing observation information")); + return false; + } + // Spraying: Either no fields or all fields must be set + var truthies = (theForm["sprayingDate"].value.trim() == "" ? 1 : 0) + + (theForm["preparationId"].options[theForm["preparationId"].selectedIndex].value == "-1" ? 1 : 0) + + (theForm["preparationDose"].value.trim() == "" ? 1 : 0); + if(truthies != 0 && truthies != 3) + { + alert(gettext("Missing spraying information")); + return false; + } + return true; + } + $(document).ready(function() { initWeatherStations(); initCrops(); initPreparations(); + // Init form validation + loadFormDefinition("{{ form_id }}","/static/cerealblotchmodels/formdefinitions/") }); var VIPSOrganizationId = {{vips_organization_id}}; @@ -111,7 +167,8 @@ document.getElementById("errorMessageContainer").style.display="none"; document.getElementById("runningModelMessage").style.display="block"; // TODO: Validate form - var formStr = $("#barleynetblotchForm").serialize(); + // + var formStr = $("#{{ form_id }}").serialize(); var request = $.ajax({ type:"GET", url: "http://" + settings.vipslogicServerName + "/rest/barleynetblotchmodel/runmodel/" + VIPSOrganizationId + "?" + formStr, @@ -126,8 +183,19 @@ //console.log(formStr); } + var paramDict = { + "BARLEYNETB.DG2410" : "DG2410", + "WEATHER.TMD": "{% trans "Temperature, daily mean" %}", + "WEATHER.RR28" : "{% trans "Rain last 28 days" %}", + "WEATHER.RRD" : "{% trans "Rain, daily" %}", + "WEATHER.TMDD0C" :"{% trans "Day degrees since sowing" %}", + "BARLEYNETB.Y" : "{% trans "Threshold" %}", + "BARLEYNETB.X_ADJUSTED" : "{% trans "Disease" %}", + }; + var renderResults = function(data,textStatus, jqXHR) { + data.sort(compareForecastResults).reverse(); // First attempt: A table! var headingLine = "<tr><td style=\"font-weight: bold;\">{% trans "Time" %}</td>"; if(data.length > 0) @@ -135,7 +203,7 @@ var allKeys = data[0].keys; for(var i in allKeys) { - headingLine +="<td style=\"font-weight: bold;\">" + allKeys[i] + "</td>"; + headingLine +="<td style=\"font-weight: bold;\">" + paramDict[allKeys[i]] + "</td>"; } headingLine +="<td style=\"font-weight: bold;\">{% trans "Warning status" %}</td>"; headingLine += "</tr>"; @@ -143,10 +211,15 @@ var table = ["<table border=\"1\">",headingLine]; for(item in data) { - var resultLine = "<tr><td>" + new Date(data[item].resultValidTime) + "</td>"; + var resultLine = "<tr><td>" + moment(data[item].resultValidTime).format() + "</td>"; for(var i in allKeys) { - resultLine +="<td>" + data[item].allValues[allKeys[i]] + "</td>"; + var value = data[item].allValues[allKeys[i]]; + if(value != null && $.isNumeric(value)) + { + value = parseFloat(value).toFixed(2); + } + resultLine +="<td>" + value + "</td>"; } resultLine +="<td style=\"background-color:"; var st = data[item].warningStatus; @@ -167,6 +240,8 @@ document.getElementById("errorMessageContainer").style.display="none"; document.getElementById("results").style.display="block"; // Then: The chart + // We must sort data ascending again + data.reverse(); var warningStatusPlotBandData = getWarningStatusPlotBandData(data); //console.log(warningStatusPlotBandData); var data = getHighChartsSeries( @@ -192,6 +267,7 @@ 200: function(data,textStatus, jqXHR){ // Building result HTML var wsHTML=["<option value=\"-1\">-- {% trans "Select weather station" %} --</option>"]; + data.sort(compareWeatherStations); for(var i in data) { var ws = data[i]; @@ -218,12 +294,13 @@ 200: function(data,textStatus, jqXHR){ // Building result HTML var cropHTML=["<option value=\"-1\">-- {% trans "Select crop" %} --</option>"]; + data.sort(compareOrganisms); for(var i in data) { var crop = data[i]; //console.log(ws); - cropHTML.push("<option value=\"" + crop["cropOrganismId"] + "\">" + getOrganismLocalNameWithFallback(crop,settings.currentLanguage,settings.defaultLanguage) + "</option>"); + cropHTML.push("<option value=\"" + crop["organismId"] + "\">" + getOrganismLocalNameWithFallback(crop,settings.currentLanguage,settings.defaultLanguage) + "</option>"); } document.getElementById("cropOrganismId").innerHTML = cropHTML.join(""); }, @@ -245,10 +322,10 @@ 200: function(data,textStatus, jqXHR){ // Building result HTML var preparationHTML=["<option value=\"-1\">-- {% trans "Select preparation" %} --</option>"]; + data.sort(comparePreparations); for(var i in data) { var preparation = data[i]; - //console.log(ws); preparationHTML.push("<option value=\"" + preparation["preparationId"] + "\">" + preparation["preparationName"] + "</option>"); } diff --git a/cerealblotchmodels/views.py b/cerealblotchmodels/views.py index 74001dab53b58bb5977660e7f77be34f911fc42e..120ffaa0806e8ab0dedbc3e774aaf446e10cd198 100644 --- a/cerealblotchmodels/views.py +++ b/cerealblotchmodels/views.py @@ -18,5 +18,8 @@ def barleynetblotchform(request): vips_organization_id = settings.VIPS_ORGANIZATION_ID if request.GET.get("organizationId") != None: vips_organization_id = request.GET.get("organizationId") - context = {"vips_organization_id": vips_organization_id} + context = { + "vips_organization_id": vips_organization_id, + "form_id" : "barleyNetBlotchForm" + } return render(request, 'cerealblotchmodels/barleynetblotchform.html', context) \ No newline at end of file diff --git a/forecasts/static/forecasts/js/forecasts.js b/forecasts/static/forecasts/js/forecasts.js index 0ad33f12ee434d2383bebce5870afd018af7fc63..d3dd3bef2ce357307fea0b3c3fac95482767da3c 100644 --- a/forecasts/static/forecasts/js/forecasts.js +++ b/forecasts/static/forecasts/js/forecasts.js @@ -215,7 +215,7 @@ function getWarningStatusPlotBandData(forecastResults) var previousForecastResult = null; var plotBand = null; var bandOffset = 0; - if (forecastResults.length >=2) + if (forecastResults != null && forecastResults.length >=2) { // Replace with Moment.js var start = forecastResults[0]["resultValidTime"]; diff --git a/organisms/static/organisms/organismsUtil.js b/organisms/static/organisms/organismsUtil.js index a64454e609040a64354067c399a183ace1c0b985..a64bff3a0a15f9b999144f92701985f44d7839f3 100644 --- a/organisms/static/organisms/organismsUtil.js +++ b/organisms/static/organisms/organismsUtil.js @@ -45,3 +45,10 @@ function getOrganismLocalNameWithFallback(organism, currentLanguage, defaultLang } return organism["latinName"]; } + +var compareOrganisms = function(a,b) +{ + var aName = getOrganismLocalNameWithFallback(a,settings.currentLanguage,settings.defaultLanguage); + var bName = getOrganismLocalNameWithFallback(b,settings.currentLanguage,settings.defaultLanguage); + return compareStrings(aName,bName); +}