From bb5cb5b43646fb7fe35a96b1f1fb9c3076f718f3 Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Wed, 2 Sep 2026 15:56:37 +0200 Subject: [PATCH 1/5] Handle accidental headers in file upload --- app/projects/helpers.py | 19 ++++++++++++++++++- app/static/js/traceplot.js | 17 ++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) 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)); } From bf9ae5bdd4a82d30b3ed2313f97acc9399163e5e Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Thu, 3 Sep 2026 11:31:16 +0200 Subject: [PATCH 2/5] Add validation logic for conditional field --- app/projects/forms.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/projects/forms.py b/app/projects/forms.py index 6cf1fadd..45a7b696 100644 --- a/app/projects/forms.py +++ b/app/projects/forms.py @@ -1368,7 +1368,6 @@ def __init__(self, *args, **kwargs): ), ) - # TODO: here also check the validation of when the field is required building_year = forms.FloatField( label=_("Building Year"), widget=forms.NumberInput( @@ -1396,6 +1395,22 @@ def __init__(self, *args, **kwargs): ), ) + 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 From f552f772fb09c41ca02dbf376c15c05faa4397d1 Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Thu, 3 Sep 2026 15:05:32 +0200 Subject: [PATCH 3/5] Add translations --- app/locale/de/LC_MESSAGES/django.po | 81 +++++++++++++++++++++++++++++ app/projects/forms.py | 28 +++++----- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index 7124ffc4..5bbd9cd1 100644 --- a/app/locale/de/LC_MESSAGES/django.po +++ b/app/locale/de/LC_MESSAGES/django.po @@ -5075,3 +5075,84 @@ 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" diff --git a/app/projects/forms.py b/app/projects/forms.py index 45a7b696..e8146915 100644 --- a/app/projects/forms.py +++ b/app/projects/forms.py @@ -1322,20 +1322,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): From e9a40c8dabcb1fdc8a6dff770b219e30dca95036 Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Thu, 3 Sep 2026 15:31:10 +0200 Subject: [PATCH 4/5] Format all help texts consistently --- app/projects/forms.py | 80 +++++++++++++------------------------------ 1 file changed, 24 insertions(+), 56 deletions(-) diff --git a/app/projects/forms.py b/app/projects/forms.py index e8146915..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, } ), @@ -1343,40 +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"), } ), ) 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, @@ -1385,14 +1357,10 @@ 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): From d3de89352b53ab81641c3d0376a8440f495da3f1 Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Thu, 3 Sep 2026 15:39:34 +0200 Subject: [PATCH 5/5] Fix error message translation --- app/locale/de/LC_MESSAGES/django.po | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index 5bbd9cd1..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 @@ -5156,3 +5155,6 @@ msgstr "Zeitreihe aus Eingabeparametern erstellen" msgid "Compute Timeseries" msgstr "Zeitreihe berechnen" + +msgid "Constant Temperature or Timeseries" +msgstr "Konstante Temperatur oder Temperaturzeitreihe"