Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search sensors in API by asset #1219

Merged
merged 25 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2d00ca5
feat: limit sensor api search to asset
joshuaunity Oct 22, 2024
e03759f
chore: added to changelog
joshuaunity Oct 23, 2024
7db6d45
chore: modify changelog
joshuaunity Oct 28, 2024
a69a924
refactor: removed extra permission checks and added extra filter layer
joshuaunity Oct 28, 2024
90e2c77
chore: added test for filtering by asset id
joshuaunity Oct 28, 2024
23ac6e2
refactor: all_accessible sesnor logic revamp
joshuaunity Oct 28, 2024
954962d
Merge branch 'main' into limit-sensors-search
joshuaunity Oct 29, 2024
37be8a8
refactor: updated query param name, logic and docstring
joshuaunity Oct 29, 2024
e9ce0b0
refactor: modified child asset query to be recursive
joshuaunity Oct 30, 2024
0d95d98
refactor: more concise query
joshuaunity Oct 30, 2024
c9dc708
refactor: extend test case
joshuaunity Oct 31, 2024
366568b
chore: recommiting
joshuaunity Oct 31, 2024
44a8aff
chore: update doc string
joshuaunity Oct 31, 2024
613e776
refactor: update test case for post sensor
joshuaunity Nov 1, 2024
9a391e9
refactor: update test case for post sensor v2
joshuaunity Nov 1, 2024
4bb8394
chore: pushing a suggestion
joshuaunity Nov 1, 2024
55bcf65
refactor: updated access logic for sensor index API
joshuaunity Nov 1, 2024
6dd2073
chore: updated test case due to change in sensor index API
joshuaunity Nov 1, 2024
49950b2
chore: update doc string and logic hierachy
joshuaunity Nov 1, 2024
e6cd1ea
refactor: updated logic and test case extentsion
joshuaunity Nov 4, 2024
5d0a626
chore: update docstring and added extra testcase
joshuaunity Nov 4, 2024
c794929
chore: added else statement
joshuaunity Nov 4, 2024
0c21942
chore: update testcase
joshuaunity Nov 4, 2024
6a71600
chore: test logic update
joshuaunity Nov 4, 2024
def41a6
Merge branch 'main' into limit-sensors-search
nhoening Nov 5, 2024
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
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ New features
* The data chart on the asset page splits up its color-coded sensor legend when showing more than 7 sensors, becoming a legend per subplot [see `PR #1176 <https://github.com/FlexMeasures/flexmeasures/pull/1176>`_ and `PR #1193 <https://github.com/FlexMeasures/flexmeasures/pull/1193>`_]
* Speed up loading the users page, by making the pagination backend-based and adding support for that in the API [see `PR #1160 <https://github.com/FlexMeasures/flexmeasures/pull/1160>`_]
* X-axis labels in CLI plots show datetime values in a readable and informative format [see `PR #1172 <https://github.com/FlexMeasures/flexmeasures/pull/1172>`_]
* Enhanced API for listing sensors: Added filtering and pagination on sensor index endpoint and created new endpoint to get all sensors under an asset [see `PR #1191 <https://github.com/FlexMeasures/flexmeasures/pull/1191>`_ ]
* Enhanced API for listing sensors: Added filtering and pagination on sensor index endpoint and created new endpoint to get all sensors under an asset [see `PR #1191 <https://github.com/FlexMeasures/flexmeasures/pull/1191>`_ and `PR #1219 <https://github.com/FlexMeasures/flexmeasures/pull/1219>`_]
* Speed up loading the accounts page,by making the pagination backend-based and adding support for that in the API [see `PR #1196 <https://github.com/FlexMeasures/flexmeasures/pull/1196>`_]
* Speed up loading the account detail page by by switching to server-side pagination for assets, replacing client-side pagination [see `PR #1202 <https://github.com/FlexMeasures/flexmeasures/pull/1202>`_]
* Simplify and Globalize toasts in the flexmeasures project [see `PR #1207 <https://github.com/FlexMeasures/flexmeasures/pull/1207>_`]
Expand Down
112 changes: 87 additions & 25 deletions flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import isodate
from datetime import datetime, timedelta

from werkzeug.exceptions import Unauthorized
from flask import current_app, url_for
from flask_classful import FlaskView, route
from flask_json import as_json
from flask_security import auth_required
from flask_security import auth_required, current_user
from marshmallow import fields, ValidationError
import marshmallow.validate as validate
from rq.job import Job, NoSuchJobError
Expand Down Expand Up @@ -39,6 +40,7 @@
from flexmeasures.data.models.time_series import Sensor, TimedBelief
from flexmeasures.data.queries.utils import simplify_index
from flexmeasures.data.schemas.sensors import SensorSchema, SensorIdField
from flexmeasures.data.schemas import AssetIdField
from flexmeasures.api.common.schemas.search import SearchFilterField
from flexmeasures.api.common.schemas.sensors import UnitField
from flexmeasures.data.schemas.times import AwareDateTimeField, PlanningDurationField
Expand Down Expand Up @@ -66,27 +68,30 @@ class SensorAPI(FlaskView):
@route("", methods=["GET"])
@use_kwargs(
{
"account": AccountIdField(
data_key="account_id", load_default=AccountIdField.load_current
"account": AccountIdField(data_key="account_id", required=False),
"asset": AssetIdField(data_key="asset_id", required=False),
"include_consultancy_clients": fields.Boolean(
required=False, load_default=False
),
"all_accessible": fields.Boolean(required=False, missing=False),
"include_public_assets": fields.Boolean(required=False, load_default=False),
"page": fields.Int(
required=False, validate=validate.Range(min=1), default=1
required=False, validate=validate.Range(min=1), load_default=None
),
"per_page": fields.Int(
required=False, validate=validate.Range(min=1), default=10
required=False, validate=validate.Range(min=1), load_default=10
),
"filter": SearchFilterField(required=False, default=None),
"unit": UnitField(required=False, default=None),
"filter": SearchFilterField(required=False, load_default=None),
"unit": UnitField(required=False, load_default=None),
},
location="query",
)
@permission_required_for_context("read", ctx_arg_name="account")
@as_json
def index(
self,
account: Account,
all_accessible: bool = False,
account: Account | None = None,
asset: GenericAsset | None = None,
include_consultancy_clients: bool = False,
include_public_assets: bool = False,
page: int | None = None,
per_page: int | None = None,
filter: list[str] | None = None,
Expand All @@ -97,11 +102,28 @@ def index(
.. :quickref: Sensor; Download sensor list

This endpoint returns all accessible sensors.
Accessible sensors are sensors in the same account as the current user.
Alternatively, you can use the `all_accessible` query parameter to list sensors from all assets that the `current_user` has read access to, as well as all public assets. The default value is `false`.
By default, "accessible sensors" means all sensors in the same account as the current user (if they have read permission to the account).

You can also specify an `account` (an ID parameter), if the user has read access to that account. In this case, all assets under the
specified account will be retrieved, and the sensors associated with these assets will be returned.

Alternatively, you can filter by asset hierarchy by providing the `asset` parameter (ID). When this is set, all sensors on the specified
asset and its sub-assets are retrieved, provided the user has read access to the asset.

NOTE: You can't set both account and asset at the same time, you can only have one set. The only exception is if the asset being specified is
part of the account that was set, then we allow to see sensors under that asset but then ignore the account (account = None).

Finally, you can use the `include_consultancy_clients` parameter to include sensors from accounts for which the current user account is a consultant.
joshuaunity marked this conversation as resolved.
Show resolved Hide resolved
This is only possible if the user has the role of a consultant.

Only admins can use this endpoint to fetch sensors from a different account (by using the `account_id` query parameter).

The `filter` parameter allows you to search for sensors by name or account name.
The `unit` parameter allows you to filter by unit.

For the pagination of the sensor list, you can use the `page` and `per_page` query parameters, the `page` parameter is used to trigger
pagination, and the `per_page` parameter is used to specify the number of records per page. The default value for `page` is 1 and for `per_page` is 10.

**Example response**

An example of one sensor being returned:
Expand Down Expand Up @@ -135,19 +157,58 @@ def index(
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
if isinstance(account, list):
accounts = account
else:
accounts: list = [account] if account else []
account_ids: list = [acc.id for acc in accounts]
if account is None and asset is None:
if current_user.is_anonymous:
raise Unauthorized
account = current_user.account

if account is not None and asset is not None:
if asset.account_id != account.id:
return {
"message": "Please provide either an account or an asset ID, not both"
}, 422
else:
account = None

if asset is not None:
joshuaunity marked this conversation as resolved.
Show resolved Hide resolved
check_access(asset, "read")

asset_tree = (
db.session.query(GenericAsset.id, GenericAsset.parent_asset_id)
.filter(GenericAsset.id == asset.id)
.cte(name="asset_tree", recursive=True)
)

recursive_part = db.session.query(
GenericAsset.id, GenericAsset.parent_asset_id
).join(asset_tree, GenericAsset.parent_asset_id == asset_tree.c.id)

asset_tree = asset_tree.union(recursive_part)

child_assets = db.session.query(asset_tree).all()

filter_statement = GenericAsset.id.in_(
[asset.id] + [a.id for a in child_assets]
)
elif account is not None:
check_access(account, "read")

account_ids: list = [account.id]

filter_statement = GenericAsset.account_id.in_(account_ids)
if include_consultancy_clients:
if current_user.has_role("consultant"):
consultancy_accounts = (
db.session.query(Account)
.filter(Account.consultancy_account_id == account.id)
.all()
)
account_ids.extend([acc.id for acc in consultancy_accounts])

filter_statement = GenericAsset.account_id.in_(account_ids)
else:
filter_statement = None

if all_accessible is not None:
consultancy_account_ids: list = [
acc.consultancy_account_id for acc in accounts
]
account_ids.extend(consultancy_account_ids)
if include_public_assets:
filter_statement = or_(
filter_statement,
GenericAsset.account_id.is_(None),
Expand All @@ -156,7 +217,7 @@ def index(
sensor_query = (
select(Sensor)
.join(GenericAsset, Sensor.generic_asset_id == GenericAsset.id)
.join(Account, GenericAsset.owner)
.outerjoin(Account, GenericAsset.owner)
.filter(filter_statement)
)

Expand All @@ -167,6 +228,7 @@ def index(
or_(
Sensor.name.ilike(f"%{term}%"),
Account.name.ilike(f"%{term}%"),
GenericAsset.name.ilike(f"%{term}%"),
)
for term in filter
)
Expand Down
Loading
Loading