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

Issue 423 improve dashboard and asset listing for admins #461

Merged
merged 14 commits into from Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -9,6 +9,7 @@ New features
-------------
* The asset page now shows the most relevant sensor data for the asset [see `PR #449 <http://www.github.com/FlexMeasures/flexmeasures/pull/449>`_]
* Individual sensor charts show available annotations [see `PR #428 <http://www.github.com/FlexMeasures/flexmeasures/pull/428>`_]
* Admins can group assets by account on dashboard & assets page [see `PR #461 <http://www.github.com/FlexMeasures/flexmeasures/pull/461>`_]
* Collapsible sidepanel (hover/swipe) used for date selection on sensor charts, and various styling improvements [see `PR #447 <http://www.github.com/FlexMeasures/flexmeasures/pull/447>`_ and `PR #448 <http://www.github.com/FlexMeasures/flexmeasures/pull/448>`_]
* Switched from 12-hour AM/PM to 24-hour clock notation for time series chart axis labels [see `PR #446 <http://www.github.com/FlexMeasures/flexmeasures/pull/446>`_]
* Get data in a given resolution [see `PR #458 <http://www.github.com/FlexMeasures/flexmeasures/pull/458>`_]
Expand Down
8 changes: 4 additions & 4 deletions flexmeasures/data/queries/generic_assets.py
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy.orm import Query

from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.queries.utils import potentially_limit_query_to_account_assets
from flexmeasures.data.queries.utils import potentially_limit_assets_query_to_account


def query_assets_by_type(
Expand All @@ -28,7 +28,7 @@ def query_assets_by_type(
query = query.filter(GenericAssetType.name == type_names)
else:
query = query.filter(GenericAssetType.name.in_(type_names))
query = potentially_limit_query_to_account_assets(query, account_id)
query = potentially_limit_assets_query_to_account(query, account_id)
return query


Expand All @@ -52,7 +52,7 @@ def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
:param account_id: Pass in an account ID if you want to query an account other than your own. This only works for admins. Public assets are always queried.
"""
asset_queries = {}
all_assets = potentially_limit_query_to_account_assets(
all_assets = potentially_limit_assets_query_to_account(
GenericAsset.query, account_id
).all()
loc_groups = group_assets_by_location(all_assets)
Expand All @@ -71,7 +71,7 @@ def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
location_query = GenericAsset.query.filter(
GenericAsset.name.in_([asset.name for asset in loc_group])
)
asset_queries[location_name] = potentially_limit_query_to_account_assets(
asset_queries[location_name] = potentially_limit_assets_query_to_account(
location_query, account_id
)
return asset_queries
Expand Down
6 changes: 3 additions & 3 deletions flexmeasures/data/queries/sensors.py
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy.orm import Query

from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.queries.utils import potentially_limit_query_to_account_assets
from flexmeasures.data.queries.utils import potentially_limit_assets_query_to_account


def query_sensor_by_name_and_generic_asset_type_name(
Expand All @@ -30,7 +30,7 @@ def query_sensor_by_name_and_generic_asset_type_name(
.filter(GenericAsset.generic_asset_type_id == GenericAssetType.id)
.filter(Sensor.generic_asset_id == GenericAsset.id)
)
query = potentially_limit_query_to_account_assets(query, account_id)
query = potentially_limit_assets_query_to_account(query, account_id)
return query


Expand Down Expand Up @@ -59,7 +59,7 @@ def query_sensors_by_proximity(
closest_sensor_query = closest_sensor_query.order_by(
GenericAsset.great_circle_distance(lat=latitude, lng=longitude).asc()
)
closest_sensor_query = potentially_limit_query_to_account_assets(
closest_sensor_query = potentially_limit_assets_query_to_account(
closest_sensor_query, account_id
)
return closest_sensor_query
7 changes: 4 additions & 3 deletions flexmeasures/data/queries/utils.py
Expand Up @@ -42,8 +42,9 @@ def create_beliefs_query(
return query


def potentially_limit_query_to_account_assets(
query: Query, account_id: Optional[int]
def potentially_limit_assets_query_to_account(
query: Query,
account_id: Optional[int] = None,
) -> Query:
"""Filter out all assets that are not in the current user's account.
For admins and CLI users, no assets are filtered out, unless an account_id is set.
Expand All @@ -55,7 +56,7 @@ def potentially_limit_query_to_account_assets(
user_is_admin = (
running_as_cli()
or current_user.has_role(ADMIN_ROLE)
or current_user.has_role(ADMIN_READER_ROLE)
or (query.statement.is_select and current_user.has_role(ADMIN_READER_ROLE))
nhoening marked this conversation as resolved.
Show resolved Hide resolved
)
if account_id is None and user_is_admin:
return query # allow admins to query assets across all accounts
Expand Down
45 changes: 31 additions & 14 deletions flexmeasures/data/services/asset_grouping.py
Expand Up @@ -5,16 +5,19 @@

from __future__ import annotations
from typing import List, Dict, Optional
from flask_login import current_user
import inflect

from sqlalchemy.orm import Query
from flexmeasures.auth.policy import user_has_admin_access

from flexmeasures.utils.flexmeasures_inflection import parameterize, pluralize
from flexmeasures.data.models.generic_assets import (
GenericAssetType,
GenericAsset,
assets_share_location,
)
from flexmeasures.data.models.user import Account
from flexmeasures.data.queries.generic_assets import (
query_assets_by_type,
get_location_queries,
Expand All @@ -24,35 +27,49 @@


def get_asset_group_queries(
custom_additional_groups: Optional[Dict[str, List[str]]] = None,
group_by_type: bool = True,
group_by_account: bool = False,
group_by_location: bool = False,
custom_aggregate_type_groups: Optional[Dict[str, List[str]]] = None,
) -> Dict[str, Query]:
"""
An asset group is defined by Asset queries. Each query has a name, and we prefer pluralised names.
They still need an executive call, like all(), count() or first().
An asset group is defined by Asset queries, which this function can generate.

Each query has a name (for the asset group it represents).
These queries still need an executive call, like all(), count() or first().

This function limits the assets to be queried to the current user's account,
if the user is not an admin.

Note: Make sure the current user has the "read" permission on his account (on GenericAsset.__class__?? See https://github.com/FlexMeasures/flexmeasures/issues/200).
Note: Make sure the current user has the "read" permission on their account (on GenericAsset.__class__?? See https://github.com/FlexMeasures/flexmeasures/issues/200) or is an admin.

:param custom_additional_groups: dict of asset type groupings (mapping group names to names of asset types). See also the setting FLEXMEASURES_ASSET_TYPE_GROUPS.
:param group_by_type: If True, groups will be made for assets with the same type. We prefer pluralised group names here. Defaults to True.
:param group_by_account: If True, groups will be made for assets within the same account. This makes sense for admins, as they can query across accounts.
:param group_by_location: If True, groups will be made for assets at the same location. Naming of the location currently supports charge points (for EVSEs).
:param custom_aggregate_type_groups: dict of asset type groupings (mapping group names to names of asset types). See also the setting FLEXMEASURES_ASSET_TYPE_GROUPS.
"""
asset_queries = {}

# 1. Custom asset groups by combinations of asset types
if custom_additional_groups:
for asset_type_group_name, asset_types in custom_additional_groups.items():
if custom_aggregate_type_groups:
for asset_type_group_name, asset_types in custom_aggregate_type_groups.items():
asset_queries[asset_type_group_name] = query_assets_by_type(asset_types)

# 2. We also include a group per asset type - using the pluralised asset type name
for asset_type in GenericAssetType.query.all():
asset_queries[pluralize(asset_type.name)] = query_assets_by_type(
asset_type.name
)

# 3. Finally, we group assets by location
# 2. Include a group per asset type - using the pluralised asset type name
if group_by_type:
for asset_type in GenericAssetType.query.all():
asset_queries[pluralize(asset_type.name)] = query_assets_by_type(
asset_type.name
)

# 3. Include a group per account (admins only) # TODO: we can later adjust this for accounts who admin certain others, not all
if group_by_account and user_has_admin_access(current_user, "read"):
for account in Account.query.all():
asset_queries[account.name] = GenericAsset.query.filter(
GenericAsset.account_id == account.id
)

# 4. Finally, we can group assets by location
if group_by_location:
asset_queries.update(get_location_queries())

Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/ui/__init__.py
Expand Up @@ -12,6 +12,7 @@
from flexmeasures.utils.flexmeasures_inflection import (
capitalize,
parameterize,
pluralize,
)
from flexmeasures.utils.time_utils import (
localized_datetime_str,
Expand Down Expand Up @@ -128,6 +129,7 @@ def add_jinja_filters(app):
app.jinja_env.filters["naturalized_datetime"] = naturalized_datetime_str
app.jinja_env.filters["naturalized_timedelta"] = naturaldelta
app.jinja_env.filters["capitalize"] = capitalize
app.jinja_env.filters["pluralize"] = pluralize
app.jinja_env.filters["parameterize"] = parameterize
app.jinja_env.filters["isnull"] = pd.isnull
app.jinja_env.filters["hide_nan_if_desired"] = (
Expand Down
45 changes: 31 additions & 14 deletions flexmeasures/ui/crud/assets.py
@@ -1,4 +1,4 @@
from typing import Union, Optional, Tuple
from typing import Union, Optional, List, Tuple
import copy

from flask import url_for, current_app
Expand All @@ -7,7 +7,7 @@
from flask_security import login_required, current_user
from wtforms import StringField, DecimalField, SelectField
from wtforms.validators import DataRequired
from flexmeasures.auth.policy import ADMIN_ROLE
from flexmeasures.auth.policy import user_has_admin_access

from flexmeasures.data import db
from flexmeasures.auth.error_handling import unauthorized_handler
Expand Down Expand Up @@ -112,6 +112,8 @@ def process_internal_api_response(
"""
Turn data from the internal API into something we can use to further populate the UI.
Either as an asset object or a dict for form filling.

If we add other data by querying the database, we make sure the asset is not in the session afterwards.
"""

def expunge_asset():
nhoening marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -127,12 +129,14 @@ def expunge_asset():
asset.generic_asset_type = GenericAssetType.query.get(
asset.generic_asset_type_id
)
expunge_asset()
asset.owner = Account.query.get(asset_data["account_id"])
expunge_asset()
if "id" in asset_data:
expunge_asset()
asset.sensors = Sensor.query.filter(
Sensor.generic_asset_id == asset_data["id"]
).all()
expunge_asset()
expunge_asset()
return asset
return asset_data

Expand All @@ -149,16 +153,29 @@ class AssetCrudUI(FlaskView):

@login_required
def index(self, msg=""):
"""/assets"""
get_assets_response = InternalApi().get(
url_for("AssetAPI:index"), query={"account_id": current_user.account_id}
)
assets = [
process_internal_api_response(ad, make_obj=True)
for ad in get_assets_response.json()
]
"""GET from /assets

List the user's assets. For admins, list across all accounts.
"""
assets = []

def get_asset_by_account(account_id) -> List[GenericAsset]:
get_assets_response = InternalApi().get(
url_for("AssetAPI:index"), query={"account_id": account_id}
)
return [
process_internal_api_response(ad, make_obj=True)
for ad in get_assets_response.json()
]

if user_has_admin_access(current_user, "read"):
for account in Account.query.all():
assets += get_asset_by_account(account.id)
nhoening marked this conversation as resolved.
Show resolved Hide resolved
else:
assets = get_asset_by_account(current_user.account_id)

return render_flexmeasures_template(
"crud/assets.html", account=current_user.account, assets=assets, message=msg
"crud/assets.html", assets=assets, message=msg
)

@login_required
Expand Down Expand Up @@ -347,7 +364,7 @@ def _set_account(asset_form: NewAssetForm) -> Tuple[Optional[Account], Optional[
account_error = None

if asset_form.account_id.data == -1:
if current_user.has_role(ADMIN_ROLE):
if user_has_admin_access(current_user, "update"):
return None, None # Account can be None (public asset)
else:
account_error = "Please pick an existing account."
Expand Down
55 changes: 55 additions & 0 deletions flexmeasures/ui/static/css/flexmeasures.css
Expand Up @@ -343,6 +343,61 @@ p.error {
/* --- End Nav Bar --- */


/* --- Switch button --- */

.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}

.switch input {
opacity: 0;
width: 0;
height: 0;
}

.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}

.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}

input:checked + .slider {
background-color: var(--secondary-color);
}

input:focus + .slider {
box-shadow: 0 0 1px var(--secondary-hover-color);
}

input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}

/* --- End Switch button --- */


/* --- Footer --- */

Expand Down
8 changes: 4 additions & 4 deletions flexmeasures/ui/templates/crud/assets.html
Expand Up @@ -10,14 +10,14 @@
<div class="row">
<div class="col-sm-12">

<h3>All assets owned by account {{account.name}}</h3>
<h3>Asset overview</h3>
<table class="table table-striped table-responsive paginate">
<thead>
<tr>
<th>Name</th>
<th class="text-right no-sort">Location</th>
<th class="text-right">Asset id</th>
<th class="text-right">Account id</th>
<th class="text-right">Account</th>
<th class="text-right no-sort">Sensors</th>
<th class="text-right no-sort">
{% if user_is_admin %}
Expand All @@ -34,7 +34,7 @@ <h3>All assets owned by account {{account.name}}</h3>
<tr>
<td>
<i class="{{ asset.generic_asset_type.name | asset_icon }} left-icon"><a
href="/assets/{{ asset.id }}" alt="Edit this asset">{{ asset.name }}</a></i>
href="/assets/{{ asset.id }}" alt="View this asset">{{ asset.name }}</a></i>
</td>
<td class="text-right">
{% if asset.latitude and asset.longitude %}
Expand All @@ -46,7 +46,7 @@ <h3>All assets owned by account {{account.name}}</h3>
{{ asset.id }}
</td>
<td class="text-right">
{{ asset.account_id }}
{{ asset.owner.name }}
</td>
<td class="text-right">
{{ asset.sensors | length }}
Expand Down