Skip to content

Commit

Permalink
Merge branch 'main' into sensorstoshow-edit-form
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuaunity authored Nov 6, 2024
2 parents 031eb89 + 3da3560 commit 1145eb1
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 45 deletions.
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.
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:
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

0 comments on commit 1145eb1

Please sign in to comment.