diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index 7124ffc4..3e6f2502 100644 --- a/app/locale/de/LC_MESSAGES/django.po +++ b/app/locale/de/LC_MESSAGES/django.po @@ -575,8 +575,7 @@ msgid "" "Please provide either a number within %(boundaries) s or upload a timeseries " "from a file" msgstr "" -"Please provide either a number within %(boundaries) s or upload a timeseries " -"from a file. aus einer Datei" +"Bitte geben Sie entweder eine Zahl innerhalb von %(boundaries) s ein oder laden Sie eine Zeitreihe aus einer Datei hoch." #: projects/helpers.py:287 projects/helpers.py:474 #, python-format @@ -5075,3 +5074,87 @@ msgstr "Aufgrund der laufenden Umstrukturierung funktioniert die Verwendung von msgid "Further information" msgstr "Weitere Informationen" + +msgid "Outdoor Temperature" +msgstr "Außentemperatur" + +msgid "Profile Type" +msgstr "Bedarfstyp" + +msgid "Select from one of the available BDEW heat profiles" +msgstr "Wählen Sie eines der verfügbaren BDEW-Wärmeprofile aus" + +msgid "Annual Heat Demand" +msgstr "Jährlicher Wärmebedarf" + +msgid "Total heat demand in the chosen timeperiod" +msgstr "Gesamtwärmebedarf im ausgewählten Zeitraum" + +msgid "Building Year" +msgstr "Baujahr" + +msgid "Only for residential buildings, used for estimating insulation" +msgstr "Nur für Wohngebäude, zur Berechnung der Wärmedämmung" + +msgid "Wind class" +msgstr "Windklasse" + +msgid "Windy" +msgstr "Windig" + +msgid "Not Windy" +msgstr "Nicht windig" + +msgid "Windy for exposed buildings on free fields, near coast or high ground. Not windy for unexposed buildings in villages/cities" +msgstr "Windig bei exponierten Gebäuden auf freiem Feld, in Küstennähe oder in höher gelegenen Lagen. Nicht windig bei nicht exponierten Gebäuden in Dörfern/Städten" + +msgid "Single-family house" +msgstr "Einfamilienhaus" + +msgid "Apartment building" +msgstr "Mehrfamilienhaus" + +msgid "Commerce/Services general" +msgstr "Handel/Dienstleistungen allgemein" + +msgid "Household-like business enterprises" +msgstr "Haushaltsähnliche Unternehmen" + +msgid "Restaurants" +msgstr "Gastronomie" + +msgid "Retail and wholesale" +msgstr "Einzel- und Großhandel" + +msgid "Metal and automotive" +msgstr "Metall und Automobil" + +msgid "Accommodation" +msgstr "Beherbergung" + +msgid "Local authorities, credit institutions and insurance companies" +msgstr "Kommunen, Kreditinstitute und Versicherungsgesellschaften" + +msgid "Other operational services" +msgstr "Sonstige operative Dienstleistungen" + +msgid "Laundries, dry cleaning" +msgstr "Wäschereien, chemische Reinigung" + +msgid "Horticulture" +msgstr "Gartenbau" + +msgid "Bakery" +msgstr "Bäckerei" + +msgid "Paper and printing" +msgstr "Papier und Druck" + +msgid "Create timeseries from parameters" +msgstr "Zeitreihe aus Eingabeparametern erstellen" + +msgid "Compute Timeseries" +msgstr "Zeitreihe berechnen" + +msgid "Constant Temperature or Timeseries" +msgstr "Konstante Temperatur oder Temperaturzeitreihe" diff --git a/app/projects/forms.py b/app/projects/forms.py index 6cf1fadd..cb1a0d9c 100644 --- a/app/projects/forms.py +++ b/app/projects/forms.py @@ -141,6 +141,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for fieldname, field in self.fields.items(): set_parameter_info(fieldname, field) + add_help_text_icon(field, fieldname, RTD_link=False) class FeedbackForm(ModelForm): @@ -203,114 +204,92 @@ class Meta: class ProjectCreateForm(OpenPlanForm): name = forms.CharField( label=_("Project Name"), + help_text=_("A self explanatory name for the project."), widget=forms.TextInput( attrs={ "placeholder": _("Name..."), - "data-bs-toggle": "tooltip", - "title": _("A self explanatory name for the project."), } ), ) description = forms.CharField( label=_("Project Description"), + help_text=_("A description of what this project objectives or test cases."), widget=forms.Textarea( attrs={ "placeholder": _("More detailed description here..."), "data-bs-toggle": "tooltip", - "title": _( - "A description of what this project objectives or test cases." - ), } ), ) country = forms.ChoiceField( label=_("Country"), + help_text=_("Name of the country where the project is being deployed"), choices=COUNTRY, - widget=forms.Select( - attrs={ - "data-bs-toggle": "tooltip", - "title": _("Name of the country where the project is being deployed"), - } - ), + widget=forms.Select(), ) longitude = forms.FloatField( label=_("Location, longitude"), + help_text=_("Longitude coordinate of the project's geographical location."), widget=forms.NumberInput( attrs={ "placeholder": _("click on the map"), "readonly": "", - "data-bs-toggle": "tooltip", - "title": _( - "Longitude coordinate of the project's geographical location." - ), } ), ) latitude = forms.FloatField( label=_("Location, latitude"), + help_text=_("Latitude coordinate of the project's geographical location."), widget=forms.NumberInput( attrs={ "placeholder": _("click on the map"), "readonly": "", - "data-bs-toggle": "tooltip", - "title": _( - "Latitude coordinate of the project's geographical location." - ), } ), ) duration = forms.IntegerField( label=_("Project Duration"), + help_text=_( + "The number of years the project is intended to be operational. The project duration also sets the installation time of the assets used in the simulation. After the project ends these assets are 'sold' and the refund is charged against the initial investment costs." + ), widget=forms.NumberInput( attrs={ "placeholder": _("eg. 1"), "min": "0", "max": "100", "step": "1", - "data-bs-toggle": "tooltip", - "title": _( - "The number of years the project is intended to be operational. The project duration also sets the installation time of the assets used in the simulation. After the project ends these assets are 'sold' and the refund is charged against the initial investment costs." - ), } ), ) currency = forms.ChoiceField( label=_("Currency"), choices=CURRENCY, - widget=forms.Select( - attrs={ - "data-bs-toggle": "tooltip", - "title": _( - "The currency of the country where the project is implemented." - ), - } - ), + help_text=_("The currency of the country where the project is implemented."), + widget=forms.Select(), ) discount = forms.FloatField( label=_("Discount Factor"), + help_text=_( + "Discount factor is the factor which accounts for the depreciation in the value of money in the future, compared to the current value of the same money. The common method is to calculate the weighted average cost of capital (WACC) and use it as the discount rate." + ), widget=forms.NumberInput( attrs={ "placeholder": _("eg. 0.1"), "min": "0.0", "max": "1.0", "step": "0.0001", - "data-bs-toggle": "tooltip", - "title": _( - "Discount factor is the factor which accounts for the depreciation in the value of money in the future, compared to the current value of the same money. The common method is to calculate the weighted average cost of capital (WACC) and use it as the discount rate." - ), } ), ) tax = forms.FloatField( label=_("Tax"), + help_text=_("Tax factor"), widget=forms.HiddenInput( attrs={ "placeholder": _("eg. 0.3"), "min": "0.0", "max": "1.0", "step": "0.0001", - "data-bs-toggle": "tooltip", - "title": _("Tax factor"), "value": 0, } ), @@ -1322,20 +1301,20 @@ def __init__(self, *args, **kwargs): class CreateHeatDemandForm(OpenPlanForm): profile_type_choices = ( - ("EFH", "Single-family house"), - ("MFH", "Apartment building"), - ("GHD", "Commerce/Services general"), - ("GMF", "Household-like business enterprises"), - ("GGA", "Restaurants"), - ("GBH", "Retail and wholesale"), - ("GMK", "Metal and automotive"), - ("GBH", "Accommodation"), - ("GKO", "Local authorities, credit institutions and insurance companies"), - ("GBD", "Other operational services"), - ("GWA", "Laundries, dry cleaning"), - ("GGB", "Horticulture"), - ("GBA", "Bakery"), - ("GPD", "Paper and printing"), + ("EFH", _("Single-family house")), + ("MFH", _("Apartment building")), + ("GHD", _("Commerce/services general")), + ("GMF", _("Household-like business enterprises")), + ("GGA", _("Restaurants")), + ("GBH", _("Retail and wholesale")), + ("GMK", _("Metal and automotive")), + ("GBH", _("Accommodation")), + ("GKO", _("Local authorities, credit institutions and insurance companies")), + ("GBD", _("Other operational services")), + ("GWA", _("Laundries, dry cleaning")), + ("GGB", _("Horticulture")), + ("GBA", _("Bakery")), + ("GPD", _("Paper and printing")), ) def __init__(self, *args, **kwargs): @@ -1343,41 +1322,33 @@ def __init__(self, *args, **kwargs): outdoor_temperature = DualNumberField( label=_("Outdoor Temperature"), + help_text=_("Constant Temperature or Timeseries"), param_name="outdoor_temperature", ) profile_type = forms.ChoiceField( choices=profile_type_choices, label=_("Profile Type"), - widget=forms.Select( - attrs={ - "data-bs-toggle": "tooltip", - "title": _("Select from one of the available BDEW heat profiles"), - } - ), + help_text=_("Select from one of the available BDEW heat profiles"), + widget=forms.Select(), ) annual_heat_demand = forms.FloatField( label=_("Annual Heat Demand"), + help_text=_("Total heat demand in the chosen timeperiod"), widget=forms.NumberInput( attrs={ "placeholder": _("e.g. 1000"), - "data-bs-toggle": "tooltip", - "title": _("Total heat demand in the chosen timeperiod"), } ), ) - # TODO: here also check the validation of when the field is required building_year = forms.FloatField( label=_("Building Year"), + help_text=_("Only for residential buildings, used for estimating insulation"), widget=forms.NumberInput( attrs={ "placeholder": _("e.g. 1970"), - "data-bs-toggle": "tooltip", - "title": _( - "Only for residential buildings, used for estimating insulation" - ), } ), required=False, @@ -1386,16 +1357,28 @@ def __init__(self, *args, **kwargs): wind_class = forms.ChoiceField( label=_("Wind class"), choices=(("Windy", _("Windy")), ("Not windy", _("Not Windy"))), - widget=forms.Select( - attrs={ - "data-bs-toggle": "tooltip", - "title": _( - "Windy for exposed buildings on free fields, near coast or high ground. Not windy for unexposed buildings in villages/cities" - ), - } + help_text=_( + "Windy for exposed buildings on free fields, near coast or high ground. Not windy for unexposed buildings in villages/cities" ), + widget=forms.Select(), ) + def clean_building_year(self): + building_year = self.cleaned_data["building_year"] + profile_type = self.cleaned_data["profile_type"] + if ( + profile_type + in [ + "EFH", + "MFH", + ] + and not building_year + ): + raise ValidationError( + _("Building year is required for residential buildings") + ) + return building_year + CUSTOM_TIMESERIES_FORMS = { # TODO: re-enable PV timeseries creation when weather data handling is settled diff --git a/app/projects/helpers.py b/app/projects/helpers.py index 9a2369cb..4beedb11 100644 --- a/app/projects/helpers.py +++ b/app/projects/helpers.py @@ -243,6 +243,15 @@ def clean(self, values): answer = json.loads(scalar_value) if not isinstance(answer, list): scalar_value = "" + elif not all(isinstance(v, (int, float)) for v in answer): + self.set_widget_error() + raise ValidationError( + _( + "The uploaded timeseries contains a " + "non-numeric value, likely from a header " + "row. Please remove it and try again." + ), + ) except json.decoder.JSONDecodeError: scalar_value = "" @@ -610,7 +619,7 @@ def parse_csv_timeseries(file_str): # --- decimal normalization --- if is_comma_decimal or ("," in value and "." not in value): value = value.replace(",", ".") - if value.isalpha(): + if not is_number(value): # catch if there is a header, then the file cannot be parsed raise ValidationError(msg) timeseries_values.append(float(value)) @@ -622,6 +631,14 @@ def is_timestamp(values): return ":" in values or "-" in values +def is_number(value): + try: + float(value) + except ValueError: + return False + return True + + def parse_xlsx_timeseries(file_buffer): wb = load_workbook(filename=file_buffer) worksheet = wb.active diff --git a/app/static/js/traceplot.js b/app/static/js/traceplot.js index 02b5a288..e60d59c5 100644 --- a/app/static/js/traceplot.js +++ b/app/static/js/traceplot.js @@ -228,7 +228,6 @@ function uploadDualInputTrace(obj, param_name="") { if (window.FileReader) { var array = []; var flist = obj; - var myfile = flist[0]; if (myfile) { if(myfile.name.includes(".csv") || myfile.name.includes(".txt")){ @@ -266,6 +265,14 @@ function plot_file_trace(obj, plot_id="") { alert('FileReader are not supported in this browser.'); } } +function stripHeaderRow(rows) { + // drop a header row (e.g. "Temperature"): its first cell won't parse as a number + if (rows.length > 0 && !Number.isFinite(Number(rows[0][0]))) { + return rows.slice(1); + } + return rows; +} + function getAsExcel(fileToRead, plot=true){ var reader = new FileReader(); @@ -273,7 +280,7 @@ function getAsExcel(fileToRead, plot=true){ // return a Promise of the file parsed as a d3 csv array return new Promise((resolve, reject) => { reader.onloadend = () => { - resolve(parseExcelData(reader.result)); + resolve(stripHeaderRow(parseExcelData(reader.result))); }; // Read file into memory as UTF-8 reader.readAsBinaryString(fileToRead); @@ -281,7 +288,7 @@ function getAsExcel(fileToRead, plot=true){ } else{ reader.onload = function(e) { - processData(parseExcelData(e.target.result)); + processData(stripHeaderRow(parseExcelData(e.target.result))); }; reader.readAsBinaryString(fileToRead); } @@ -323,7 +330,7 @@ function parseExcelData(data){ // return a Promise of the file parsed as a d3 csv array return new Promise((resolve, reject) => { reader.onloadend = () => { - resolve(d3.csvParseRows(reader.result)); + resolve(stripHeaderRow(d3.csvParseRows(reader.result))); }; // Read file into memory as UTF-8 reader.readAsText(fileToRead); @@ -346,7 +353,7 @@ function parseExcelData(data){ } else { d3array = d3.csvParseRows(csv); } - processData(d3array); + processData(stripHeaderRow(d3array)); }