Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be4cdd8
Add asset copy functionality with tabbed interface in asset creation …
joshuaunity May 22, 2026
f5aa38b
feat: Enhance asset copy tab with infinite scroll and loading indicators
joshuaunity May 22, 2026
d2c94fe
Update flexmeasures/ui/templates/assets/asset_new.html
joshuaunity May 25, 2026
609425b
feat: Add asset type filtering to asset API and update UI parameters
joshuaunity May 25, 2026
3336c0c
chore: add changelog entry
joshuaunity May 25, 2026
a1c3380
ref: make card title clickable
joshuaunity May 25, 2026
28329b9
feat: Set external_id to None in copy_asset to prevent unique constra…
joshuaunity May 25, 2026
4b472a4
chore: code linting
joshuaunity May 25, 2026
339f8ae
feat: Remove unused parentAssetId variable and related logic from ass…
joshuaunity May 25, 2026
967b378
Merge branch 'main' into feat/asset-create-copy-tab-ui-flow
joshuaunity May 29, 2026
5f4838f
feat: Update asset type handling and add AssetTypeIdField for better …
joshuaunity May 29, 2026
6573504
chore: code formatting
joshuaunity May 29, 2026
e5749e3
feat: Add parent asset ID handling for asset copy functionality
joshuaunity Jun 1, 2026
14b2902
feat: Set external_id to None when copying assets to avoid unique con…
joshuaunity Jun 1, 2026
b407e22
Update flexmeasures/ui/templates/assets/asset_new.html
joshuaunity Jun 2, 2026
5ce1008
fix: fixed issue were public assets are always added
joshuaunity Jun 2, 2026
5f3bdc2
feat: enhance asset copy UI with pagination and improved loading state
joshuaunity Jun 2, 2026
8a94c30
feat: update asset copy functionality to handle None values for accou…
joshuaunity Jun 12, 2026
26a042f
chore; set search input to search template keyword by default
joshuaunity Jun 12, 2026
e022ca4
Merge branch 'main' into feat/asset-create-copy-tab-ui-flow
nhoening Jul 1, 2026
75f242f
use all_accessible param to look up assets, show 15 per page
nhoening Jul 1, 2026
80b993f
add a doc section on how to create a child asset
nhoening Jul 1, 2026
25c75a1
no pre-fill of search term, let's do that in the next PR
nhoening Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ New features
* New ``GET /api/v3_0/sources`` endpoint to list accessible data sources and defined types, with ``only_latest=true`` by default to return only the most recent version per source [see `PR #2126 <https://www.github.com/FlexMeasures/flexmeasures/pull/2126>`_]
* Add support for filtering sensor data GET requests by ``source-type`` on ``/api/v3_0/sensors/<id>/data`` [see `PR #2127 <https://www.github.com/FlexMeasures/flexmeasures/pull/2127>`_]
* Making monitoring alerts more flexible: allow ``flexmeasures monitor`` alerts to target one or more user IDs or email addresses with ``--recipient``; ``flexmeasures monitor last-seen`` can now narrow monitored users to one or more accounts with ``--account`` or to client accounts with ``--consultancy`` [see `PR #2158 <https://www.github.com/FlexMeasures/flexmeasures/pull/2158>`_]
* Support for creating new assets by using another asset as a template from the UI. [see `PR #2195 <https://www.github.com/FlexMeasures/flexmeasures/pull/2195>`_]
* Improve LightGBM daily seasonal lag handling for sub-hourly forecasting sensors [see `PR #2157 <https://www.github.com/FlexMeasures/flexmeasures/pull/2157>`_]

Infrastructure / Support
Expand Down
13 changes: 12 additions & 1 deletion flexmeasures/api/common/schemas/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from flexmeasures.api.common.schemas.generic_schemas import PaginationSchema
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.data.schemas import AssetIdField
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema
from flexmeasures.data.schemas.generic_assets import (
AssetTypeIdField,
GenericAssetSchema,
)


default_response_fields = ["id", "name", "account_id", "generic_asset_type"]
Expand Down Expand Up @@ -92,6 +95,14 @@ class AssetAPIQuerySchema(PaginationSchema):
example=False,
),
)
asset_type = AssetTypeIdField(
data_key="asset_type",
required=False,
metadata=dict(
description="Filter assets by generic asset type ID.",
example=2,
),
)


class PublicAssetAPISchema(Schema):
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/api/common/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ def _copy_asset_subtree(
)
asset_kwargs["account_id"] = destination_account_id
asset_kwargs["parent_asset_id"] = destination_parent_asset_id
# set external_id to None to avoid conflicts with unique constraint on (account_id, external_id)
asset_kwargs["external_id"] = None
asset_kwargs = convert_asset_json_fields(asset_kwargs)

copied_asset = GenericAsset(**asset_kwargs)
Expand Down
6 changes: 6 additions & 0 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def index(
fields_in_response: list[str] | None,
all_accessible: bool,
include_public: bool,
asset_type: GenericAssetType | None = None,
account: Account | None = None,
root_asset: GenericAsset | None = None,
max_depth: int | None = None,
Expand All @@ -287,6 +288,7 @@ def index(
- The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.
- Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.
- The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.
- The `asset_type` query parameter can be used to filter by generic asset type ID.
- The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).
- The `depth` query parameter can be used to search only a max number of descendant generations from the root.

Expand Down Expand Up @@ -363,6 +365,10 @@ def index(
filter_statement = GenericAsset.account_id.in_(account_ids)
if include_public:
filter_statement = filter_statement | GenericAsset.account_id.is_(None)
if asset_type is not None:
filter_statement = filter_statement & (
GenericAsset.generic_asset_type_id == asset_type.id
)

query = query_assets_by_search_terms(
search_terms=filter,
Expand Down
31 changes: 31 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,37 @@ def test_get_assets(
assert turbine["account_id"] == setup_accounts["Supplier"].id


@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
def test_get_assets_filtered_by_asset_type(
client, setup_api_test_data, setup_accounts, requesting_user
):
supplier_account = setup_accounts["Supplier"]
supplier_account_id = supplier_account.id
supplier_assets = supplier_account.generic_assets

requested_type_id = supplier_assets[0].generic_asset_type_id
expected_assets = [
asset
for asset in supplier_assets
if asset.generic_asset_type_id == requested_type_id
]

response = client.get(
url_for("AssetAPI:index"),
query_string={
"account_id": supplier_account_id,
"asset_type": requested_type_id,
},
)

assert response.status_code == 200
assert len(response.json) == len(expected_assets)
assert all(
asset["generic_asset_type"]["id"] == requested_type_id
for asset in response.json
)


@pytest.mark.parametrize(
"requesting_user, sort_by, sort_dir, expected_name_of_first_sensor",
[
Expand Down
18 changes: 18 additions & 0 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,24 @@ class Meta:
model = GenericAssetType


class AssetTypeIdField(MarshmallowClickMixin, fields.Int):
"""Field that deserializes to a GenericAssetType and serializes back to an integer."""

def _deserialize(self, value: Any, attr, data, **kwargs) -> GenericAssetType:
"""Turn a generic asset type id into a GenericAssetType."""
asset_type_id: int = super()._deserialize(value, attr, data, **kwargs)
asset_type = db.session.get(GenericAssetType, asset_type_id)
if asset_type is None:
raise FMValidationError(
f"No generic asset type found with id {asset_type_id}."
)
return asset_type

def _serialize(self, value: GenericAssetType, attr, obj, **kwargs):
"""Turn a GenericAssetType into a generic asset type id."""
return value.id


class GenericAssetIdField(MarshmallowClickMixin, fields.Int):
"""Field that deserializes to a GenericAsset and serializes back to an integer."""

Expand Down
17 changes: 16 additions & 1 deletion flexmeasures/ui/static/openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3456,7 +3456,7 @@
"/api/v3_0/assets": {
"get": {
"summary": "List assets accessible by the user.",
"description": "This endpoint returns all assets that are accessible by the user after applying optional filters.\n\n - The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.\n - Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.\n - The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.\n - The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).\n - The `depth` query parameter can be used to search only a max number of descendant generations from the root.\n\nThe endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.\n - If the `page` parameter is not provided, all assets are returned, without pagination information. The result will be a list of assets.\n - If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).\n - If a search 'filter' such as 'solar \"ACME corp\"' is provided, the response will filter out assets where each search term is either present in their name or account name.\n The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)\n\nPer default, the response only includes a limited set of asset fields (id, name, account_id, generic_asset_type).\nYou can use the `fields` query parameter to specify a custom set of fields to include in the response.\n",
"description": "This endpoint returns all assets that are accessible by the user after applying optional filters.\n\n - The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.\n - Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.\n - The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.\n - The `asset_type` query parameter can be used to filter by generic asset type ID.\n - The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).\n - The `depth` query parameter can be used to search only a max number of descendant generations from the root.\n\nThe endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.\n - If the `page` parameter is not provided, all assets are returned, without pagination information. The result will be a list of assets.\n - If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).\n - If a search 'filter' such as 'solar \"ACME corp\"' is provided, the response will filter out assets where each search term is either present in their name or account name.\n The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)\n\nPer default, the response only includes a limited set of asset fields (id, name, account_id, generic_asset_type).\nYou can use the `fields` query parameter to specify a custom set of fields to include in the response.\n",
"security": [
{
"ApiKeyAuth": []
Expand Down Expand Up @@ -3581,6 +3581,16 @@
"example": false
},
"required": false
},
{
"in": "query",
"name": "asset_type",
"description": "Filter assets by generic asset type ID.",
"schema": {
"type": "integer",
"example": 2
},
"required": false
}
],
"responses": {
Expand Down Expand Up @@ -4845,6 +4855,11 @@
"default": false,
"description": "Whether to include public assets. Ignored if an `account_id` is set. To fetch only public assets, use [/assets/public/](#/Assets/get_api_v3_0_assets_public) instead.",
"example": false
},
"asset_type": {
"type": "integer",
"description": "Filter assets by generic asset type ID.",
"example": 2
}
},
"additionalProperties": false
Expand Down
Loading
Loading