From c397dea578498add2ccd7e34e77d33b42bf1dc33 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Fri, 6 Feb 2026 08:31:13 +0100 Subject: [PATCH 01/15] feat: alloow update of parent asst on UI Signed-off-by: joshuaunity --- .../ui/templates/assets/asset_properties.html | 105 ++++++++++-------- flexmeasures/ui/views/assets/forms.py | 5 + flexmeasures/ui/views/assets/views.py | 5 + 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/flexmeasures/ui/templates/assets/asset_properties.html b/flexmeasures/ui/templates/assets/asset_properties.html index c09b950b8e..225f7468e8 100644 --- a/flexmeasures/ui/templates/assets/asset_properties.html +++ b/flexmeasures/ui/templates/assets/asset_properties.html @@ -56,6 +56,20 @@

Edit {{ asset.name }}

{% endfor %} +
+ {{ asset_form.parent_asset_id.label(class="col-sm-6 control-label") }} +
+ + +
+
@@ -103,7 +117,7 @@

Edit {{ asset.name }}

{% endfor %}
- +
(Click map to edit latitude and longitude in form) @@ -120,12 +134,13 @@

Edit {{ asset.name }}

{% endif %} - +
{% if user_can_delete_asset %}
-
@@ -279,11 +294,8 @@

All child assets for {{ asset.name }}

- +
@@ -353,7 +365,8 @@
- +
@@ -412,7 +425,7 @@ // This variable is used to prevent reRenderForm from running on initial load let hasInitialized = false; - + const schemaSpecs = {{ flex_model_schema | tojson | safe }}; const assetFlexModelSchema = {}; const FlexModelFieldValidTypes = {}; @@ -436,7 +449,7 @@ FlexModelFieldValidTypes[key] = validTypes; } - const [assetFlexModelRawJSON, extraFields] = processResourceRawJSON( {...assetFlexModelSchema }, '{{ asset_flexmodel | safe }}', true); + const [assetFlexModelRawJSON, extraFields] = processResourceRawJSON({ ...assetFlexModelSchema }, '{{ asset_flexmodel | safe }}', true); document.addEventListener('DOMContentLoaded', async function () { const flexOptionsContainer = document.getElementById('flexOptionsContainer'); @@ -450,7 +463,7 @@ saveChangesBtn.onclick = async function () { await updateFlexModel(); }; - + const [getFlexModel, setFlexModel] = createReactiveState(assetFlexModelRawJSON, reRenderForm); const [activeCard, setActiveCard] = createReactiveState(null, () => { senSearchResEle.style.display = 'none'; }); const [selectedIndex, setSelectedIndex] = createReactiveState(null, renderFlexInputOptions); @@ -611,7 +624,7 @@ // Clean null values for (const [key, value] of Object.entries(data)) { - if ( + if ( value === null || value === "" || value === "Not Set" || @@ -1051,7 +1064,7 @@ input.className = 'form-control'; input.id = 'flexStringInput'; input.placeholder = 'Enter a value'; - input.value = `${(typeof value === 'string' && value !== "Not Set")? value : ''}`; + input.value = `${(typeof value === 'string' && value !== "Not Set") ? value : ''}`; input.onkeydown = function (event) { if (event.key === 'Enter') { event.preventDefault(); @@ -1164,7 +1177,7 @@ renderFlexModelForm(); // Initial render of flex model form renderFlexFieldOptions(assetFlexModelSchema, getFlexModel()); }); - + flexSelect.addEventListener('change', (event) => { const selectedKey = event.target.value; renderSelectInfoCards(selectedKey); @@ -1221,21 +1234,21 @@ } $(document).ready(function () { - let unit = ""; - // Initialize the DataTable - const table = $("#sensorsTable").dataTable({ - order: [[0, "asc"]], - pageLength: 5, - lengthMenu: [5, 10, 25, 50, 75, 100], - serverSide: true, - // make the table row vertically aligned with header - columns: [ - { data: "id", title: "ID", orderable: true }, - { data: "name", title: "Name", orderable: true }, - { data: "unit", title: "Unit", orderable: false }, - { data: "resolution", title: "Resolution", orderable: true }, - { data: "url", title: "URL", className: "d-none" }, - ], + let unit = ""; + // Initialize the DataTable + const table = $("#sensorsTable").dataTable({ + order: [[0, "asc"]], + pageLength: 5, + lengthMenu: [5, 10, 25, 50, 75, 100], + serverSide: true, + // make the table row vertically aligned with header + columns: [ + { data: "id", title: "ID", orderable: true }, + { data: "name", title: "Name", orderable: true }, + { data: "unit", title: "Unit", orderable: false }, + { data: "resolution", title: "Resolution", orderable: true }, + { data: "url", title: "URL", className: "d-none" }, + ], ajax: function (data, callback, settings) { const basePath = window.location.origin; @@ -1261,22 +1274,22 @@ - $.ajax({ - type: "get", - url: url, - success: function (response, text) { - let clean_response = []; - response["data"].forEach((element) => - clean_response.push( - new Sensor( - element["id"], - element["name"], - element["unit"], - element["event_resolution"], - element["active"] + $.ajax({ + type: "get", + url: url, + success: function (response, text) { + let clean_response = []; + response["data"].forEach((element) => + clean_response.push( + new Sensor( + element["id"], + element["name"], + element["unit"], + element["event_resolution"], + element["active"] + ) ) - ) - ); + ); callback({ data: clean_response, diff --git a/flexmeasures/ui/views/assets/forms.py b/flexmeasures/ui/views/assets/forms.py index 5e2107ab15..4cdd8039f1 100644 --- a/flexmeasures/ui/views/assets/forms.py +++ b/flexmeasures/ui/views/assets/forms.py @@ -37,6 +37,11 @@ class AssetForm(FlaskForm): places=None, render_kw={"placeholder": "--Click the map or enter a longitude--"}, ) + parent_asset_id = IntegerField( + "Parent Asset Id", + validators=[optional()], + render_kw={"placeholder": "--Enter parent asset id--"}, + ) attributes = StringField( "Other attributes (JSON)", default="{}", diff --git a/flexmeasures/ui/views/assets/views.py b/flexmeasures/ui/views/assets/views.py index daec0ed067..da7f5f6a2f 100644 --- a/flexmeasures/ui/views/assets/views.py +++ b/flexmeasures/ui/views/assets/views.py @@ -399,9 +399,14 @@ def properties(self, id: str): from flexmeasures.data.schemas.scheduling import UI_FLEX_MODEL_SCHEMA + account_assets = ( + db.session.query(GenericAsset).filter_by(account_id=asset.account_id).all() + ) + return render_flexmeasures_template( "assets/asset_properties.html", asset=asset, + account_assets=account_assets, site_asset=site_asset, flex_model_schema=UI_FLEX_MODEL_SCHEMA, asset_flexmodel=json.dumps(asset.flex_model), From 0dfca8896a5a8e534e57176e42c2105e5ae9ec00 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Mon, 9 Feb 2026 10:56:23 +0100 Subject: [PATCH 02/15] refactor: add order to query for fetching all accounts assets to properties page Signed-off-by: joshuaunity --- flexmeasures/ui/views/assets/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flexmeasures/ui/views/assets/views.py b/flexmeasures/ui/views/assets/views.py index da7f5f6a2f..4e791274e7 100644 --- a/flexmeasures/ui/views/assets/views.py +++ b/flexmeasures/ui/views/assets/views.py @@ -399,8 +399,12 @@ def properties(self, id: str): from flexmeasures.data.schemas.scheduling import UI_FLEX_MODEL_SCHEMA + # order by ID in asc account_assets = ( - db.session.query(GenericAsset).filter_by(account_id=asset.account_id).all() + db.session.query(GenericAsset) + .filter_by(account_id=asset.account_id) + .order_by(GenericAsset.id.asc()) + .all() ) return render_flexmeasures_template( From 53d87c53a2942e8a4c78ec8f012ff8e7c429b21d Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Mon, 9 Feb 2026 15:47:45 +0100 Subject: [PATCH 03/15] fix: fixed issue where you cant update an asset properties if the use the same name of the asset itself Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 6 +++++- flexmeasures/ui/views/assets/forms.py | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index 16ca0a1ba6..a6f0a410b0 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -209,7 +209,11 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): Here, we can only check if we have all information (a full form), which usually is at creation time. """ - if "name" in data and "parent_asset_id" in data: + if ( + "name" in data + and "parent_asset_id" in data + and data["parent_asset_id"] is not None + ): asset = db.session.scalars( select(GenericAsset) .filter_by( diff --git a/flexmeasures/ui/views/assets/forms.py b/flexmeasures/ui/views/assets/forms.py index 4cdd8039f1..0d7e91be5d 100644 --- a/flexmeasures/ui/views/assets/forms.py +++ b/flexmeasures/ui/views/assets/forms.py @@ -40,7 +40,6 @@ class AssetForm(FlaskForm): parent_asset_id = IntegerField( "Parent Asset Id", validators=[optional()], - render_kw={"placeholder": "--Enter parent asset id--"}, ) attributes = StringField( "Other attributes (JSON)", @@ -127,7 +126,6 @@ class NewAssetForm(AssetForm): "Asset type", coerce=int, validators=[DataRequired()] ) account_id = SelectField("Account", coerce=int, validators=[optional()]) - parent_asset_id = IntegerField("Parent Asset Id", validators=[optional()]) def set_account(self) -> tuple[Account | None, str | None]: """Set an account for the to-be-created asset. From ac916ebc86cc42decf849e07f0c2c8ee09d3891f Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Wed, 11 Feb 2026 18:36:44 +0100 Subject: [PATCH 04/15] fix: fixed validation logic with edgecase causing test to fail Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 29 +++++++++++---------- flexmeasures/ui/views/assets/views.py | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index a6f0a410b0..02b7720fe3 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -209,21 +209,22 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): Here, we can only check if we have all information (a full form), which usually is at creation time. """ - if ( - "name" in data - and "parent_asset_id" in data - and data["parent_asset_id"] is not None - ): - asset = db.session.scalars( - select(GenericAsset) - .filter_by( - name=data["name"], - parent_asset_id=data.get("parent_asset_id"), - ) - .limit(1) - ).first() + if "name" in data: + parent_id = data.get("parent_asset_id") + + query = select(GenericAsset).filter_by( + name=data["name"], + parent_asset_id=parent_id, + ) + + current_editing_id = self.context.get("asset_id") + + if current_editing_id is not None: + query = query.filter(GenericAsset.id != current_editing_id) + + existing_asset = db.session.scalars(query).first() - if asset: + if existing_asset: err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}" raise ValidationError(err_msg, "name") diff --git a/flexmeasures/ui/views/assets/views.py b/flexmeasures/ui/views/assets/views.py index 4e791274e7..20d8a33c28 100644 --- a/flexmeasures/ui/views/assets/views.py +++ b/flexmeasures/ui/views/assets/views.py @@ -292,6 +292,7 @@ def post(self, id: str): # noqa: C901 session["msg"] = f"Cannot edit asset: {asset_form.errors}" return redirect(url_for("AssetCrudUI:properties", id=id)) try: + patch_asset_schema.context = {"asset_id": asset.id} loaded_asset_data = patch_asset_schema.load(asset_form.to_json()) patch_asset(asset, loaded_asset_data) db.session.commit() From 7dc4678c204a5679e3bf4f3db660303c22317eb9 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Wed, 11 Feb 2026 18:51:06 +0100 Subject: [PATCH 05/15] fix: fixed another edge case with existing name validator Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index 02b7720fe3..a3ec160c4e 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -212,21 +212,22 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): if "name" in data: parent_id = data.get("parent_asset_id") - query = select(GenericAsset).filter_by( - name=data["name"], - parent_asset_id=parent_id, - ) + if parent_id is not None: + query = select(GenericAsset).filter_by( + name=data["name"], + parent_asset_id=parent_id, + ) - current_editing_id = self.context.get("asset_id") + current_editing_id = self.context.get("asset_id") - if current_editing_id is not None: - query = query.filter(GenericAsset.id != current_editing_id) + if current_editing_id is not None: + query = query.filter(GenericAsset.id != current_editing_id) - existing_asset = db.session.scalars(query).first() + existing_asset = db.session.scalars(query).first() - if existing_asset: - err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}" - raise ValidationError(err_msg, "name") + if existing_asset: + err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}" + raise ValidationError(err_msg, "name") @validates("generic_asset_type_id") def validate_generic_asset_type(self, generic_asset_type_id: int, **kwargs): From eafa38a80bbbfd84907ea9e5348e61d4a726087f Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Wed, 11 Feb 2026 20:04:23 +0100 Subject: [PATCH 06/15] fix: final fix v1.0 Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 25 +++++++++---------- .../ui/templates/assets/asset_properties.html | 5 ++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index a3ec160c4e..3098ebf908 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -210,24 +210,23 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): which usually is at creation time. """ if "name" in data: - parent_id = data.get("parent_asset_id") + parent_id = data["parent_asset_id"] - if parent_id is not None: - query = select(GenericAsset).filter_by( - name=data["name"], - parent_asset_id=parent_id, - ) + query = select(GenericAsset).filter_by( + name=data["name"], + parent_asset_id=parent_id, + ) - current_editing_id = self.context.get("asset_id") + current_editing_id = getattr(self, "context", {}).get("asset_id") - if current_editing_id is not None: - query = query.filter(GenericAsset.id != current_editing_id) + if current_editing_id is not None: + query = query.filter(GenericAsset.id != current_editing_id) - existing_asset = db.session.scalars(query).first() + existing_asset = db.session.scalars(query).first() - if existing_asset: - err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}" - raise ValidationError(err_msg, "name") + if existing_asset: + err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}" + raise ValidationError(err_msg, "name") @validates("generic_asset_type_id") def validate_generic_asset_type(self, generic_asset_type_id: int, **kwargs): diff --git a/flexmeasures/ui/templates/assets/asset_properties.html b/flexmeasures/ui/templates/assets/asset_properties.html index 225f7468e8..120a022631 100644 --- a/flexmeasures/ui/templates/assets/asset_properties.html +++ b/flexmeasures/ui/templates/assets/asset_properties.html @@ -59,9 +59,8 @@

Edit {{ asset.name }}

{{ asset_form.parent_asset_id.label(class="col-sm-6 control-label") }}
- + {% for account_asset in account_assets %} From 72a916ee95c743e201df94d739eb112af02dee67 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Wed, 11 Feb 2026 20:24:57 +0100 Subject: [PATCH 07/15] fix: final fix v2.0 Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index 3098ebf908..3a50938283 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -210,7 +210,7 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): which usually is at creation time. """ if "name" in data: - parent_id = data["parent_asset_id"] + parent_id = data.get("parent_asset_id", None) query = select(GenericAsset).filter_by( name=data["name"], From 5c140d2c417ab9964da1b8faf3ed5262dc421eac Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Thu, 12 Feb 2026 10:40:59 +0100 Subject: [PATCH 08/15] fix: final fix v2.1 Signed-off-by: joshuaunity --- flexmeasures/ui/tests/test_asset_crud.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/ui/tests/test_asset_crud.py b/flexmeasures/ui/tests/test_asset_crud.py index ada0b4f022..6d053fbf17 100644 --- a/flexmeasures/ui/tests/test_asset_crud.py +++ b/flexmeasures/ui/tests/test_asset_crud.py @@ -129,6 +129,8 @@ def test_add_asset(db, client, setup_assets, as_admin): def test_edit_asset(db, client, setup_assets, as_admin): mock_asset = mock_asset_data_with_kpis(db=db, as_list=False) + mock_asset["name"] = "Edited name" + response = client.post( url_for("AssetCrudUI:post", id=1), follow_redirects=True, From e07e02fd7d70f1479d9382060a670b9720de1668 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Wed, 18 Feb 2026 16:18:49 +0100 Subject: [PATCH 09/15] feat: remove current asset form parent list Signed-off-by: joshuaunity --- flexmeasures/data/schemas/generic_assets.py | 1 + flexmeasures/ui/templates/assets/asset_properties.html | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index 3a50938283..cb022b0f5d 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -209,6 +209,7 @@ def validate_name_is_unique_under_parent(self, data, **kwargs): Here, we can only check if we have all information (a full form), which usually is at creation time. """ + if "name" in data: parent_id = data.get("parent_asset_id", None) diff --git a/flexmeasures/ui/templates/assets/asset_properties.html b/flexmeasures/ui/templates/assets/asset_properties.html index 120a022631..e012fc2494 100644 --- a/flexmeasures/ui/templates/assets/asset_properties.html +++ b/flexmeasures/ui/templates/assets/asset_properties.html @@ -59,11 +59,15 @@

Edit {{ asset.name }}

{{ asset_form.parent_asset_id.label(class="col-sm-6 control-label") }}
- + {% for account_asset in account_assets %} - + {% endif %} {% endfor %} From 49d58135922ab5f29c9915b559b042cc1efb2e05 Mon Sep 17 00:00:00 2001 From: joshuaunity Date: Fri, 20 Feb 2026 11:29:06 +0100 Subject: [PATCH 10/15] feat: validation for parent asset id while updating Signed-off-by: joshuaunity --- flexmeasures/api/v3_0/assets.py | 20 ++++++++++++++++---- flexmeasures/data/schemas/generic_assets.py | 21 +++++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index d63bbc2dda..a388dcd57f 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -662,7 +662,6 @@ def fetch_one(self, id, asset): return asset_schema.dump(asset), 200 @route("/", methods=["PATCH"]) - @use_args(patch_asset_schema) @use_kwargs( { "db_asset": AssetIdField( @@ -673,7 +672,7 @@ def fetch_one(self, id, asset): ) @permission_required_for_context("update", ctx_arg_name="db_asset") @as_json - def patch(self, asset_data: dict, id: int, db_asset: GenericAsset): + def patch(self, id: int, db_asset: GenericAsset): """ .. :quickref: Assets; Update an asset given its identifier. --- @@ -731,10 +730,23 @@ def patch(self, asset_data: dict, id: int, db_asset: GenericAsset): tags: - Assets """ + asset_data = request.get_json() + if not asset_data: + return unprocessable_entity("No JSON data provided.") + + asset_schema = AssetSchema(partial=True) + asset_schema.context = {"asset": db_asset} + + try: + validated_data = asset_schema.load(asset_data) + except ValidationError as e: + return unprocessable_entity(e.messages) + try: - db_asset = patch_asset(db_asset, asset_data) + db_asset = patch_asset(db_asset, validated_data) except ValidationError as e: - return unprocessable_entity(str(e.messages)) + return unprocessable_entity(e.messages) + db.session.add(db_asset) db.session.commit() return asset_schema.dump(db_asset), 200 diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index cb022b0f5d..9003ac6200 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -239,11 +239,24 @@ def validate_generic_asset_type(self, generic_asset_type_id: int, **kwargs): @validates("parent_asset_id") def validate_parent_asset(self, parent_asset_id: int | None, **kwargs): - if parent_asset_id is not None: - parent_asset = db.session.get(GenericAsset, parent_asset_id) - if not parent_asset: + if parent_asset_id is None: + return + + parent_asset = db.session.get(GenericAsset, parent_asset_id) + if not parent_asset: + raise ValidationError( + f"Parent GenericAsset with id {parent_asset_id} doesn't exist." + ) + + # Check account consistency (using context) + # Safely get the asset from context, defaulting to None if creating new + current_asset = self.context.get("asset") + + # If editing an existing asset (context exists) + if current_asset and current_asset.account_id: + if parent_asset.account_id != current_asset.account_id: raise ValidationError( - f"Parent GenericAsset with id {parent_asset_id} doesn't exist." + "Parent asset must belong to the same account as the child asset." ) @validates("account_id") From d20dad431047f3f58c941a93b6817f36a0112b3b Mon Sep 17 00:00:00 2001 From: Joshua Edward Date: Mon, 23 Feb 2026 23:01:38 +0100 Subject: [PATCH 11/15] chore: final phase of changes Signed-off-by: Joshua Edward --- documentation/changelog.rst | 3 ++- flexmeasures/api/v3_0/assets.py | 3 ++- flexmeasures/data/schemas/generic_assets.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d530e24b26..c006715883 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -24,6 +24,7 @@ New features * Added ``root`` and ``depth`` fields to the `[GET] /assets` endpoint for listing assets, to allow selecting descendants of a given root asset up to a given depth [see `PR #1874 `_] * Give ability to edit sensor timezone from the UI [see `PR #1900 `_] * Support creating schedules with only information known prior to some time, now also via the CLI (the API already supported it) [see `PR #1871 `_]. +* Added capability to upate an asset' parent from the UI [`PR #1957 `_] * Add ``fields`` param to the asset-listing endpoints, to save bandwidth in response data [see `PR #1884 `_] .. note:: For backwards-compatibility, the new ``fields`` parameter will only be fully active, i.e. also returning less fields per default, in v0.32. Set ``FLEXMEASURES_API_SUNSET_ACTIVE=True`` to test the full effect now. @@ -51,7 +52,7 @@ Infrastructure / Support * Support and document data container in docker compose stack better [see `PR #1790 `_] * Improve reported test coverage by including doctests and bringing back CLI tests to GitHub Actions [see `PR #1914 `_ and `PR #778 `_, respectively] * Add DEFAULT_DATASOURCE_TYPES and use it in CLI and status page [see `PR #1938 `_] -* Fix README badges [see `PR #1913 `_] +* Fix README badges [see `PR s#1913 `_] * Allow seeing complete datetimes in the audit log [see `PR #1949 `_] * Add latest dependabot alert suggestions [see `PR #1959 `_] diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index a388dcd57f..dfc0e3cb97 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -730,12 +730,13 @@ def patch(self, id: int, db_asset: GenericAsset): tags: - Assets """ + # For us to be able to add our own context, we need to validate tehdata ourselves asset_data = request.get_json() if not asset_data: return unprocessable_entity("No JSON data provided.") asset_schema = AssetSchema(partial=True) - asset_schema.context = {"asset": db_asset} + asset_schema.context = {"asset": db_asset} # context for validating fields like parent_asset_id try: validated_data = asset_schema.load(asset_data) diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py index 9003ac6200..06adba5553 100644 --- a/flexmeasures/data/schemas/generic_assets.py +++ b/flexmeasures/data/schemas/generic_assets.py @@ -239,6 +239,24 @@ def validate_generic_asset_type(self, generic_asset_type_id: int, **kwargs): @validates("parent_asset_id") def validate_parent_asset(self, parent_asset_id: int | None, **kwargs): + """ + Validate the `parent_asset_id`. + + Ensures the referenced parent GenericAsset exists. When editing an existing + asset (available in `self.context.get("asset")`), also ensures the parent + belongs to the same `Account` as the asset being edited. + + Parameters + ---------- + parent_asset_id : int | None + ID of the parent GenericAsset, or None. + + Raises + ------ + ValidationError + If the parent asset does not exist, or if the parent belongs to a different + account than the current asset. + """ if parent_asset_id is None: return From e56635aa11b5f07bcc3a271e2363756112b00586 Mon Sep 17 00:00:00 2001 From: JDev <45713692+joshuaunity@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:11:45 +0100 Subject: [PATCH 12/15] Update documentation/changelog.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Höning Signed-off-by: JDev <45713692+joshuaunity@users.noreply.github.com> --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index c006715883..6d37ee042b 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -24,7 +24,7 @@ New features * Added ``root`` and ``depth`` fields to the `[GET] /assets` endpoint for listing assets, to allow selecting descendants of a given root asset up to a given depth [see `PR #1874 `_] * Give ability to edit sensor timezone from the UI [see `PR #1900 `_] * Support creating schedules with only information known prior to some time, now also via the CLI (the API already supported it) [see `PR #1871 `_]. -* Added capability to upate an asset' parent from the UI [`PR #1957 `_] +* Added capability to upate an asset's parent from the UI [`PR #1957 `_] * Add ``fields`` param to the asset-listing endpoints, to save bandwidth in response data [see `PR #1884 `_] .. note:: For backwards-compatibility, the new ``fields`` parameter will only be fully active, i.e. also returning less fields per default, in v0.32. Set ``FLEXMEASURES_API_SUNSET_ACTIVE=True`` to test the full effect now. From 7addba29c4fe73f9c36f1d2be10bae504e7b5884 Mon Sep 17 00:00:00 2001 From: JDev <45713692+joshuaunity@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:12:10 +0100 Subject: [PATCH 13/15] Update documentation/changelog.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Höning Signed-off-by: JDev <45713692+joshuaunity@users.noreply.github.com> --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6d37ee042b..98d675ef3e 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -52,7 +52,7 @@ Infrastructure / Support * Support and document data container in docker compose stack better [see `PR #1790 `_] * Improve reported test coverage by including doctests and bringing back CLI tests to GitHub Actions [see `PR #1914 `_ and `PR #778 `_, respectively] * Add DEFAULT_DATASOURCE_TYPES and use it in CLI and status page [see `PR #1938 `_] -* Fix README badges [see `PR s#1913 `_] +* Fix README badges [see `PR #1913 `_] * Allow seeing complete datetimes in the audit log [see `PR #1949 `_] * Add latest dependabot alert suggestions [see `PR #1959 `_] From 990418552af9a62b27aa885fe9c1d76b801f955c Mon Sep 17 00:00:00 2001 From: JDev <45713692+joshuaunity@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:13:13 +0100 Subject: [PATCH 14/15] Update flexmeasures/ui/views/assets/views.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Höning Signed-off-by: JDev <45713692+joshuaunity@users.noreply.github.com> --- flexmeasures/ui/views/assets/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/ui/views/assets/views.py b/flexmeasures/ui/views/assets/views.py index 20d8a33c28..ef616e8e4c 100644 --- a/flexmeasures/ui/views/assets/views.py +++ b/flexmeasures/ui/views/assets/views.py @@ -400,7 +400,7 @@ def properties(self, id: str): from flexmeasures.data.schemas.scheduling import UI_FLEX_MODEL_SCHEMA - # order by ID in asc + # suggestions for the parent asset selection account_assets = ( db.session.query(GenericAsset) .filter_by(account_id=asset.account_id) From 4b28d15520906ccf25b0968d7dac4ee0060337de Mon Sep 17 00:00:00 2001 From: JDev <45713692+joshuaunity@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:13:23 +0100 Subject: [PATCH 15/15] Update flexmeasures/api/v3_0/assets.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Höning Signed-off-by: JDev <45713692+joshuaunity@users.noreply.github.com> --- flexmeasures/api/v3_0/assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index dfc0e3cb97..2116986aac 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -730,7 +730,7 @@ def patch(self, id: int, db_asset: GenericAsset): tags: - Assets """ - # For us to be able to add our own context, we need to validate tehdata ourselves + # For us to be able to add our own context, we need to validate the data ourselves asset_data = request.get_json() if not asset_data: return unprocessable_entity("No JSON data provided.")