Skip to content

Commit

Permalink
Merge branch 'main' into feature/show-beliefs-by-source-type
Browse files Browse the repository at this point in the history
  • Loading branch information
Flix6x committed Feb 13, 2024
2 parents d97a71d + 3046f11 commit c9297f1
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 8 deletions.
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ v0.19.0 | February xx, 2024
New features
-------------

* List child assets on the asset page [see `PR #967 <https://github.com/FlexMeasures/flexmeasures/pull/967>`_]
* Expand the UI's breadcrumb functionality with the ability to navigate directly to sibling assets and sensors using their child-parent relationship [see `PR #977 <https://github.com/FlexMeasures/flexmeasures/pull/977>`_]
* Enable the use of QuantityOrSensor fields for the ``flexmeasures add schedule for-storage`` CLI command [see `PR #966 <https://github.com/FlexMeasures/flexmeasures/pull/966>`_]
* CLI support for showing/savings time series data for a given type of source only, with the new ``--source-type`` option of ``flexmeasures show beliefs``, which let's you filter out schedules, forecasts, or data POSTed by users (through the API), which each have a different source type [see `PR #976 <https://github.com/FlexMeasures/flexmeasures/pull/976>`_]
* Support for defining the storage efficiency as a sensor or quantity for the ``StorageScheduler`` [see `PR #965 <https://github.com/FlexMeasures/flexmeasures/pull/965>`_]
Expand Down
17 changes: 17 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ 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_asset_with_children(client, add_asset_with_children, requesting_user):
"""
Get asset `parent` with children `child_1` and `child_2`.
We expect the response to be the serialized asset including its
child assets in the field `child_assets`.
"""

parent = add_asset_with_children["parent"]
get_assets_response = client.get(
url_for("AssetAPI:fetch_one", id=parent.id),
)
print("Server responded with:\n%s" % get_assets_response.json)
assert get_assets_response.status_code == 200
assert len(get_assets_response.json["child_assets"]) == 2


@pytest.mark.parametrize("requesting_user", [None], indirect=True)
def test_get_public_assets_noauth(
client, setup_api_test_data, setup_accounts, requesting_user
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class GenericAssetSchema(ma.SQLAlchemySchema):
generic_asset_type_id = fields.Integer(required=True)
attributes = JSON(required=False)
parent_asset_id = fields.Int(required=False, allow_none=True)
child_assets = ma.Nested("GenericAssetSchema", many=True, dumb_only=True)

class Meta:
model = GenericAsset
Expand Down
11 changes: 11 additions & 0 deletions flexmeasures/ui/crud/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ def expunge_asset():
if asset_id:
asset_data["id"] = asset_id
if make_obj:
children = asset_data.pop("child_assets", [])

asset = GenericAsset(
**{
**asset_data,
Expand All @@ -157,6 +159,15 @@ def expunge_asset():
GenericAsset.id == asset_data["parent_asset_id"]
).one_or_none()
expunge_asset()

child_assets = []
for child in children:
child.pop("child_assets")
child_asset = process_internal_api_response(child, child["id"], True)
child_assets.append(child_asset)
asset.child_assets = child_assets
expunge_asset()

return asset
return asset_data

Expand Down
8 changes: 5 additions & 3 deletions flexmeasures/ui/static/css/flexmeasures.css
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ p.error {

.dropdown-menu {
border: none !important;
z-index: 2000;
}

.navbar-default .navbar-nav>li>a:focus,
Expand Down Expand Up @@ -1515,12 +1516,13 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {


/* ---- Sensors on asset page ---- */
/* [id^=DataTables_Table] [id$=filter] matches all the ids starting with DataTables_Table and ending with filter */

.sensors-asset #DataTables_Table_0_wrapper {
.sensors-asset .dataTables_wrapper {
margin-top: 30px;
}

.sensors-asset #DataTables_Table_0_filter input {
.sensors-asset .dataTables_filter input {
border: var(--light-gray) 1px solid;
padding: 10px;
outline: none;
Expand All @@ -1531,7 +1533,7 @@ body .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
-o-transition: .4s;
}

.sensors-asset #DataTables_Table_0_filter input:focus {
.sensors-asset .dataTables_filter input:focus {
box-shadow: 0 0 10px rgba(0,0,0,.1);
border-color: var(--secondary-color);
}
Expand Down
57 changes: 55 additions & 2 deletions flexmeasures/ui/templates/crud/asset.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for breadcrumb in breadcrumb_info["ancestors"] %}
<li class="breadcrumb-item{% if loop.last %} active{% endif %}" {% if loop.last %}aria-current="page"{% endif %}>
<li class="breadcrumb-item{% if loop.last %} dropdown active{% endif %}" {% if loop.last %}aria-current="page"{% endif %}>
{% if breadcrumb["url"] is not none and not loop.last %}
<a href="{{ breadcrumb['url'] }}">{{ breadcrumb['name'] }}</a>
{% else %}
{{ breadcrumb['name'] }}
<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{ breadcrumb['name'] }}<span class="caret"></span></a>
<ul class="dropdown-menu">
{% for sibling in breadcrumb_info["siblings"] %}
<li><a href="{{ sibling['url'] }}">{{ sibling["name"] }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
Expand Down Expand Up @@ -174,6 +179,54 @@ <h3>All sensors for {{ asset.name }}</h3>
</tbody>
</table>
</div>

<div class="sensors-asset card">
<h3>All child assets for {{ asset.name }}</h3>

<table class="table table-striped table-responsive paginate nav-on-click" title="View this asset">
<thead>
<tr>
<th><i class="left-icon">Name</i></th>
<th>Location</th>
<th>Asset ID</th>
<th>Account</th>
<th>Sensors</th>
<th class="hidden">URL</th>
</tr>
</thead>
<tbody>
{% for child in asset.child_assets %}
<tr>
<td>
<i class="{{ child.generic_asset_type.name | asset_icon }} left-icon">{{ child.name }}</i>
</td>
<td>
{% if child.latitude and child.longitude %}
LAT: {{ "{:,.4f}".format( child.latitude ) }} LONG:
{{ "{:,.4f}".format( child.longitude ) }}
{% endif %}
</td>
<td>
{{ child.id }}
</td>
<td>
{% if child.owner %}
{{ child.owner.name }}
{% else %}
PUBLIC
{% endif %}
</td>
<td>
{{ child.sensors | length }}
</td>
<td class="hidden">
/assets/{{ child.id }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-sm-2">
<div class="replay-container">
Expand Down
9 changes: 7 additions & 2 deletions flexmeasures/ui/templates/views/sensors.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for breadcrumb in breadcrumb_info["ancestors"] %}
<li class="breadcrumb-item{% if loop.last %} active{% endif %}" {% if loop.last %}aria-current="page"{% endif %}>
<li class="breadcrumb-item{% if loop.last %} dropdown active{% endif %}" {% if loop.last %}aria-current="page"{% endif %}>
{% if breadcrumb["url"] is not none and not loop.last %}
<a href="{{ breadcrumb['url'] }}">{{ breadcrumb['name'] }}</a>
{% else %}
{{ breadcrumb['name'] }}
<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{ breadcrumb['name'] }}<span class="caret"></span></a>
<ul class="dropdown-menu">
{% for sibling in breadcrumb_info["siblings"] %}
<li><a href="{{ sibling['url'] }}">{{ sibling["name"] }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
Expand Down
31 changes: 30 additions & 1 deletion flexmeasures/ui/utils/breadcrumb_utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from __future__ import annotations

from flexmeasures import Sensor, Asset, Account
from flexmeasures.utils.flexmeasures_inflection import human_sorted
from flask import url_for


def get_breadcrumb_info(entity: Sensor | Asset | Account | None) -> dict:
return {
"ancestors": get_ancestry(entity),
"siblings": get_siblings(entity),
}


def get_ancestry(entity: Sensor | Asset | Account | None) -> list[dict]:

# Public account
if entity is None:
return [{"url": None, "name": "PUBLIC", "type": "Account"}]
Expand Down Expand Up @@ -55,3 +56,31 @@ def get_ancestry(entity: Sensor | Asset | Account | None) -> list[dict]:
return get_ancestry(entity.parent_asset) + current_entity_info

return []


def get_siblings(entity: Sensor | Asset | Account | None) -> list[dict]:
siblings = []
if isinstance(entity, Sensor):
siblings = [
{
"url": url_for("SensorUI:get", id=sensor.id),
"name": sensor.name,
"type": "Sensor",
}
for sensor in entity.generic_asset.sensors
]
if isinstance(entity, Asset):
if entity.parent_asset is not None:
sibling_assets = entity.parent_asset.child_assets
else:
sibling_assets = entity.owner.generic_assets
siblings = [
{
"url": url_for("AssetCrudUI:get", id=asset.id),
"name": asset.name,
"type": "Asset",
}
for asset in sibling_assets
]
siblings = human_sorted(siblings, attr="name")
return siblings
50 changes: 50 additions & 0 deletions flexmeasures/utils/flexmeasures_inflection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
""" FlexMeasures way of handling inflection """

from __future__ import annotations

import re
from typing import Any

import inflect
import inflection
Expand Down Expand Up @@ -61,3 +63,51 @@ def titleize(word):

def join_words_into_a_list(words: list[str]) -> str:
return p.join(words, final_sep="")


def atoi(text):
"""Utility method for the `natural_keys` method."""
return int(text) if text.isdigit() else text


def natural_keys(text: str):
"""Support for human sorting.
`alist.sort(key=natural_keys)` sorts in human order.
https://stackoverflow.com/a/5967539/13775459
"""
return [atoi(c) for c in re.split(r"(\d+)", text)]


def human_sorted(alist: list, attr: Any | None = None, reverse: bool = False):
"""Human sort a list (for example, a list of strings or dictionaries).
:param alist: List to be sorted.
:param attr: Optionally, pass a dictionary key or attribute name to sort by
:param reverse: If True, sorts descending.
Example:
>>> alist = ["PV 10", "CP1", "PV 2", "PV 1", "CP 2"]
>>> sorted(alist)
['CP 2', 'CP1', 'PV 1', 'PV 10', 'PV 2']
>>> human_sorted(alist)
['CP1', 'CP 2', 'PV 1', 'PV 2', 'PV 10']
"""
if attr is None:
# List of strings, to be sorted
sorted_list = sorted(alist, key=lambda k: natural_keys(str(k)), reverse=reverse)
else:
try:
# List of dictionaries, to be sorted by key
sorted_list = sorted(
alist, key=lambda k: natural_keys(k[attr]), reverse=reverse
)
except TypeError:
# List of objects, to be sorted by attribute
sorted_list = sorted(
alist,
key=lambda k: natural_keys(str(getattr(k, attr))),
reverse=reverse,
)
return sorted_list

0 comments on commit c9297f1

Please sign in to comment.