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

Feat: Asset context overview page. #1369

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
13 changes: 7 additions & 6 deletions flexmeasures/ui/crud/assets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@
from flask_security import current_user
from flask_wtf import FlaskForm
from sqlalchemy import select
from wtforms import (
StringField,
DecimalField,
SelectField,
)
from wtforms import StringField, DecimalField, SelectField, IntegerField
from wtforms.validators import DataRequired, optional

from flexmeasures.auth.policy import user_has_admin_access
Expand Down Expand Up @@ -65,7 +61,8 @@ def to_json(self) -> dict:
data["longitude"] = float(data["longitude"])
if data.get("latitude") is not None:
data["latitude"] = float(data["latitude"])

if data.get("parent_asset_id") is not None:
data["parent_asset_id"] = int(data["parent_asset_id"])
if "csrf_token" in data:
del data["csrf_token"]

Expand Down Expand Up @@ -108,10 +105,14 @@ class NewAssetForm(AssetForm):
"Asset type", coerce=int, validators=[DataRequired()]
)
account_id = SelectField("Account", coerce=int)
parent_asset_id = IntegerField(
"Parent Asset Id", validators=[optional()]
) # Add parent_id field

def set_account(self) -> tuple[Account | None, str | None]:
"""Set an account for the to-be-created asset.
Return the account (if available) and an error message"""

account_error = None

if self.account_id.data == -1:
Expand Down
80 changes: 80 additions & 0 deletions flexmeasures/ui/crud/assets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from flexmeasures.auth.policy import check_access
from flexmeasures.data import db
from flexmeasures import Asset
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.time_series import Sensor
Expand All @@ -18,6 +19,7 @@
is_energy_unit,
is_power_unit,
)
from flexmeasures.ui.utils.view_utils import svg_asset_icon_name


def get_allowed_price_sensor_data(account_id: Optional[int]) -> Dict[int, str]:
Expand Down Expand Up @@ -194,3 +196,81 @@ def get_assets_by_account(account_id: int | str | None) -> list[GenericAsset]:
process_internal_api_response(ad, make_obj=True)
for ad in get_assets_response.json()
]


def serialize_asset(asset: Asset, is_head=False) -> dict:
serialized_asset = {
"name": asset.name,
"id": asset.id,
"asset_type": asset.generic_asset_type.name,
"link": url_for("AssetCrudUI:get", id=asset.id),
"icon": svg_asset_icon_name(asset.generic_asset_type.name),
"tooltip": {
"name": asset.name,
"ID": asset.id,
"Asset Type": asset.generic_asset_type.name,
},
"sensors": [
{"name": sensor.name, "unit": sensor.unit, "link": url_for("SensorUI:get", id=sensor.id)} for sensor in asset.sensors
],
}

if asset.parent_asset and not is_head:
serialized_asset["parent"] = asset.parent_asset.id

return serialized_asset


def get_list_assets_chart(
asset: Asset,
base_asset: Asset,
parent_depth=0,
child_depth=0,
look_for_child=False,
is_head=False,
) -> list[dict]:
"""
Recursively builds a tree of assets from a given asset and its parents and children up to a certain depth.

Args:
asset: The asset to start the recursion from
base_asset: The asset that is the base of the chart
parent_depth: The current depth of the parents hierarchy
child_depth: The current depth of the children hierarchy
look_for_child: If True, start looking for children of the current asset
is_head: If True, the current asset is the head of the chart

Returns:
A list of dictionaries representing the assets in the tree
"""
assets = list()
asset_def = serialize_asset(asset, is_head=is_head)

# Fetch parents if there is parent asset and parent_depth is less than 2
if asset.parent_asset and parent_depth < 2 and not look_for_child:
parent_depth += 1
assets += get_list_assets_chart(
asset=asset.parent_asset,
base_asset=base_asset,
parent_depth=parent_depth,
is_head=False if parent_depth < 2 else True,
)
else:
look_for_child = True
parent_depth = (
2 # Auto increase depth in the parents hierarchy is less than two
)

assets.append(asset_def)

if look_for_child and child_depth < 2:
child_depth += 1
for child in base_asset.child_assets:
assets += get_list_assets_chart(
child,
parent_depth=parent_depth,
child_depth=child_depth,
base_asset=child,
)

return assets
56 changes: 53 additions & 3 deletions flexmeasures/ui/crud/assets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from flask_security import login_required, current_user
from webargs.flaskparser import use_kwargs

from flexmeasures.ui.utils.view_utils import svg_asset_icon_name

from flexmeasures.data import db
from flexmeasures.auth.error_handling import unauthorized_handler
from flexmeasures.data.schemas import StartEndTimeSchema
Expand All @@ -13,7 +15,7 @@
GenericAsset,
get_center_location_of_assets,
)
from flexmeasures.ui.utils.view_utils import ICON_MAPPING
from flexmeasures.ui.utils.view_utils import ICON_MAPPING, available_units
from flexmeasures.data.models.user import Account
from flexmeasures.ui.utils.view_utils import render_flexmeasures_template
from flexmeasures.ui.crud.api_wrapper import InternalApi
Expand All @@ -23,13 +25,12 @@
user_can_create_assets,
user_can_delete,
user_can_update,
get_list_assets_chart,
)
from flexmeasures.data.services.sensors import (
build_sensor_status_data,
build_asset_jobs_data,
)
from flexmeasures.ui.utils.view_utils import available_units


"""
Asset crud view.
Expand Down Expand Up @@ -92,6 +93,45 @@ def owned_by(self, account_id: str):
user_can_create_assets=user_can_create_assets(),
)

@login_required
@route("/<id>/context")
def context(self, id: str):
"""/assets/<id>/context"""
asset = db.session.query(GenericAsset).filter_by(id=id).first()
if asset is None:
assets = []
else:
assets = get_list_assets_chart(asset, base_asset=asset)

current_asset_sensors = [
{
"name": sensor.name,
"unit": sensor.unit,
"link": url_for("SensorUI:get", id=sensor.id),
}
for sensor in asset.sensors
]
# Add Extra node to the current asset
add_child_asset = {
"name": "Add Child Asset",
"id": "new",
"asset_type": asset.generic_asset_type.name,
"link": url_for("AssetCrudUI:post", id="new", parent_asset_id=asset.id),
"icon": svg_asset_icon_name("add_asset"),
"tooltip": "",
"sensors": [],
"parent": asset.id,
}

assets.append(add_child_asset)

return render_flexmeasures_template(
"crud/asset_context.html",
assets=assets,
asset=asset,
current_asset_sensors=current_asset_sensors,
)

@use_kwargs(StartEndTimeSchema, location="query")
@login_required
def get(self, id: str, **kwargs):
Expand All @@ -100,18 +140,28 @@ def get(self, id: str, **kwargs):
- start_time: minimum time of the events to be shown
- end_time: maximum time of the events to be shown
"""
parent_asset_id = request.args.get("parent_asset_id", "")
if id == "new":
if not user_can_create_assets():
return unauthorized_handler(None, [])

asset_form = NewAssetForm()
asset_form.with_options()
if parent_asset_id:
parent_asset = db.session.get(GenericAsset, parent_asset_id)
if parent_asset:
asset_form.account_id.data = str(
parent_asset.account_id
) # Pre-set account
parent_asset_name = parent_asset.name
return render_flexmeasures_template(
"crud/asset_new.html",
asset_form=asset_form,
msg="",
map_center=get_center_location_of_assets(user=current_user),
mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""),
parent_asset_name=parent_asset_name,
parent_asset_id=parent_asset_id,
)

get_asset_response = InternalApi().get(url_for("AssetAPI:fetch_one", id=id))
Expand Down
9 changes: 9 additions & 0 deletions flexmeasures/ui/static/css/flexmeasures.css
Original file line number Diff line number Diff line change
Expand Up @@ -1963,3 +1963,12 @@ body.touched [title]:hover:after {
color: #000;
}
/* FlexContext Modal Dialogue - End*/


/* Custom CSS for SVG */
.mark-image image {
transform: translate(1px, 10px) !important;
width: 34px !important;
height: 20px !important;
vertical-align: middle;
}
Loading
Loading