diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 686db20ff0..b961927e9d 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -5,6 +5,10 @@ API change log .. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL (e.g. `/api/v3_0`), allowing developers to upgrade at their own pace. +v3.0-36 | September 11, 2026 +"""""""""""""""""""""""""""" +- ``GET /api/v3_0/assets`` now applies the ``root`` and ``depth`` constraints to ``num-records``, too, so a listing scoped to an asset subtree reports how many assets that subtree holds. Previously, ``num-records`` counted every asset in the account and asset-type scope, which made a paginated client report the assets outside the subtree as having been filtered out by the search term. + v3.0-35 | September 9, 2026 """"""""""""""""""""""""""" - The ``resolution`` field is now rejected with a ``422 (Unprocessable Entity)`` response unless it spans a positive amount of time. This applies wherever the API accepts one: as a query parameter on ``GET /api/v3_0/sensors//data`` and on the ``chart_data`` endpoints under ``api/dev``, and in the request body of the ``POST`` schedule trigger endpoints. Previously, a zero resolution (such as ``PT0S``) either crashed the request with a ``500`` or was silently ignored, and a negative resolution returned an empty set of values. diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 813fb1f7c1..73bdbbe4c6 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -32,6 +32,7 @@ New features * Both tabs of an asset's status page now name the asset each row belongs to, and the jobs tab also lists the jobs of the asset's sub-assets, so a site asset shows what happened anywhere below it, which you can switch off per session [see `PR #2500 `_] * Changing the selected time range on an asset or sensor chart now only loads the data that is actually new, instead of reloading the whole range, which makes stepping through or extending a long period much faster; reloading the page, or leaving it open for five minutes, still fetches everything afresh [see `PR #2433 `_] * The statistics table on a sensor page now shows all data sources together by default, as the graph does [see `PR #2462 `_] +* The asset lists on an organisation's page and on the asset overview now show only top-level assets per default, so the sites you are looking for are no longer buried among their sub-assets; untick *Top-level only* to see the whole tree again [see `PR #2523 `_] Infrastructure / Support ------------------------- diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index 7fab5ec99e..a88e5ce490 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -631,9 +631,16 @@ def index( select_pagination: SelectPagination = db.paginate( query, per_page=per_page, page=page ) - num_records = db.session.scalar( - select(func.count(GenericAsset.id)).filter(filter_statement) + # `num-records` reports the size of the scope the search filter was applied to, + # so it must respect the same subtree constraint as the paginated query itself. + num_records_query = select(func.count(GenericAsset.id)).filter( + filter_statement ) + if root_asset is not None or max_depth is not None: + num_records_query = filter_assets_under_root( + query=num_records_query, root_asset=root_asset, max_depth=max_depth + ) + num_records = db.session.scalar(num_records_query) response = { "data": response_schema.dump(select_pagination.items, many=True), "num-records": num_records, diff --git a/flexmeasures/api/v3_0/tests/test_assets_api.py b/flexmeasures/api/v3_0/tests/test_assets_api.py index a99618766f..2fbf0df812 100644 --- a/flexmeasures/api/v3_0/tests/test_assets_api.py +++ b/flexmeasures/api/v3_0/tests/test_assets_api.py @@ -442,6 +442,58 @@ def test_get_asset_with_children(client, add_asset_with_children, requesting_use assert len(get_assets_response.json["child_assets"]) == 2 +@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True) +def test_get_assets_top_level_only(client, add_asset_with_children, requesting_user): + """ + Listing assets with `depth=0` returns only assets without a parent asset. + The unfiltered listing is checked as well, to show that the children would otherwise be included. + """ + parent = add_asset_with_children["parent"] + child_ids = {add_asset_with_children[f"child_{i}"].id for i in (1, 2)} + + full_response = client.get( + url_for("AssetAPI:index"), + query_string={"all_accessible": "true"}, + ) + assert full_response.status_code == 200 + full_asset_ids = {asset["id"] for asset in full_response.json} + assert parent.id in full_asset_ids + assert child_ids <= full_asset_ids + + top_level_response = client.get( + url_for("AssetAPI:index"), + query_string={"all_accessible": "true", "depth": 0}, + ) + print("Server responded with:\n%s" % top_level_response.json) + assert top_level_response.status_code == 200 + top_level_asset_ids = {asset["id"] for asset in top_level_response.json} + assert parent.id in top_level_asset_ids + assert not child_ids & top_level_asset_ids + + +@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True) +def test_get_assets_top_level_only_record_counts( + client, add_asset_with_children, requesting_user +): + """ + `num-records` reports the size of the scope that the search filter is applied to, so it respects `depth` just like the paginated query does. + Without that, a client showing the listing would report the descendants as having been filtered out by the search. + """ + response = client.get( + url_for("AssetAPI:index"), + query_string={ + "all_accessible": "true", + "depth": 0, + "page": 1, + "per_page": 100, + }, + ) + print("Server responded with:\n%s" % response.json) + assert response.status_code == 200 + assert response.json["num-records"] == len(response.json["data"]) + assert response.json["num-records"] == response.json["filtered-records"] + + @pytest.mark.parametrize("requesting_user", [None], indirect=True) def test_get_public_assets_noauth( client, setup_api_test_data, setup_accounts, requesting_user diff --git a/flexmeasures/ui/templates/accounts/account.html b/flexmeasures/ui/templates/accounts/account.html index 4abf7e79ba..3842c12abd 100644 --- a/flexmeasures/ui/templates/accounts/account.html +++ b/flexmeasures/ui/templates/accounts/account.html @@ -412,6 +412,17 @@

Assets {% endif %}

+
+ +