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

Allow editing asset attributes in the UI #474

Merged
merged 13 commits into from Aug 25, 2022
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -29,6 +29,7 @@ Infrastructure / Support
* Allow access tokens to be passed as env vars as well [see `PR #443 <http://www.github.com/FlexMeasures/flexmeasures/pull/443>`_]
* Queue workers can get initialised without a custom name and name collisions are handled [see `PR #455 <http://www.github.com/FlexMeasures/flexmeasures/pull/455>`_]
* New API endpoint to get public assets [see `PR #461 <http://www.github.com/FlexMeasures/flexmeasures/pull/461>`_]
* Allow editing an asset's JSON attributes through the UI [see `PR #474 <http://www.github.com/FlexMeasures/flexmeasures/pull/474>`_]


v0.10.1 | June XX, 2022
Expand Down
9 changes: 6 additions & 3 deletions documentation/views/asset-data.rst
Expand Up @@ -4,7 +4,10 @@
Assets & data
**************

The asset page allows to see data from the asset's sensors, and also to edit meta information, like its location:
The asset page allows to see data from the asset's sensors, and also to edit attributes of the asset, like its location.
Other attributes are stored as a JSON string, which can be edited here as well.
This is meant for meta information that may be used to customize views or functionality, e.g. by plugins.
This includes the possibility to specify which sensors the asset page should show. For instance, here we include a price sensor from a public asset, by setting ``{"sensor_to_show": [3, 2]}`` (sensor 3 on top, followed by sensor 2 below).


.. image:: https://github.com/FlexMeasures/screenshots/raw/main/screenshot_asset.png
Expand All @@ -14,6 +17,6 @@ The asset page allows to see data from the asset's sensors, and also to edit met
|
|

.. note:: It is possible to determine which sensors this page should show. For instance, here we include a price sensor from a public asset.
For this, set the `sensors_to_show` attribute with the CLI command ``flexmeasures edit attribute``.
.. note:: While it is possible to show an arbitrary number of sensors this way, we recommend showing only the most crucial ones for faster loading, less page scrolling, and generally, a quick grasp of what the asset is up to.
.. note:: Asset attributes can be edited through the CLI as well, with the CLI command ``flexmeasures edit attribute``.

46 changes: 46 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
@@ -1,3 +1,5 @@
import json

from flask import url_for
import pytest

Expand Down Expand Up @@ -144,6 +146,50 @@ def test_alter_an_asset(client, setup_api_test_data, setup_accounts):
assert asset_edit_response.status_code == 200


@pytest.mark.parametrize(
"bad_json_str",
[
None,
"{",
'{"hallo": world}',
],
)
def test_alter_an_asset_with_bad_json_attributes(
client, setup_api_test_data, setup_accounts, bad_json_str
):
"""Check whether updating an asset's attributes with a badly structured JSON fails."""
with UserContext("test_prosumer_user@seita.nl") as prosumer1:
auth_token = prosumer1.get_auth_token()
with AccountContext("Test Prosumer Account") as prosumer:
prosumer_asset = prosumer.generic_assets[0]
asset_edit_response = client.patch(
url_for("AssetAPI:patch", id=prosumer_asset.id),
headers={"content-type": "application/json", "Authorization": auth_token},
json={"attributes": bad_json_str},
)
print(f"Editing Response: {asset_edit_response.json}")
assert asset_edit_response.status_code == 422


def test_alter_an_asset_with_json_attributes(
client, setup_api_test_data, setup_accounts
):
"""Check whether updating an asset's attributes with a properly structured JSON succeeds."""
with UserContext("test_prosumer_user@seita.nl") as prosumer1:
auth_token = prosumer1.get_auth_token()
with AccountContext("Test Prosumer Account") as prosumer:
prosumer_asset = prosumer.generic_assets[0]
asset_edit_response = client.patch(
url_for("AssetAPI:patch", id=prosumer_asset.id),
headers={"content-type": "application/json", "Authorization": auth_token},
json={
"attributes": json.dumps(prosumer_asset.attributes)
}, # we're not changing values to keep other tests clean here
)
print(f"Editing Response: {asset_edit_response.json}")
assert asset_edit_response.status_code == 200


def test_post_an_asset_with_existing_name(client, setup_api_test_data):
"""Catch DB error (Unique key violated) correctly"""
with UserContext("test_admin_user@seita.nl") as admin_user:
Expand Down
13 changes: 13 additions & 0 deletions flexmeasures/data/schemas/generic_assets.py
@@ -1,4 +1,5 @@
from typing import Optional
import json

from marshmallow import validates, validates_schema, ValidationError, fields

Expand All @@ -12,6 +13,17 @@
)


class JSON(fields.Field):
def _deserialize(self, value, attr, data, **kwargs):
try:
return json.loads(value)
except ValueError:
raise ValidationError("Not a valid JSON string.")

def _serialize(self, value, attr, data, **kwargs):
return json.dumps(value)


class GenericAssetSchema(ma.SQLAlchemySchema):
"""
GenericAsset schema, with validations.
Expand All @@ -23,6 +35,7 @@ class GenericAssetSchema(ma.SQLAlchemySchema):
latitude = ma.auto_field()
longitude = ma.auto_field()
generic_asset_type_id = fields.Integer(required=True)
attributes = JSON(required=False)

class Meta:
model = GenericAsset
Expand Down
26 changes: 19 additions & 7 deletions flexmeasures/ui/crud/assets.py
@@ -1,5 +1,6 @@
from typing import Union, Optional, List, Tuple
import copy
import json

from flask import url_for, current_app
from flask_classful import FlaskView
Expand Down Expand Up @@ -47,6 +48,7 @@ class AssetForm(FlaskForm):
places=4,
render_kw={"placeholder": "--Click the map or enter a longitude--"},
)
attributes = StringField("Other attributes (JSON)")

def validate_on_submit(self):
if (
Expand Down Expand Up @@ -125,7 +127,12 @@ def expunge_asset():
if asset_id:
asset_data["id"] = asset_id
if make_obj:
asset = GenericAsset(**asset_data) # TODO: use schema?
asset = GenericAsset(
**{
**asset_data,
**{"attributes": json.loads(asset_data.get("attributes", "{}"))},
}
) # TODO: use schema?
asset.generic_asset_type = GenericAssetType.query.get(
asset.generic_asset_type_id
)
Expand Down Expand Up @@ -285,11 +292,14 @@ def post(self, id: str):
f"Internal asset API call unsuccessful [{post_asset_response.status_code}]: {post_asset_response.text}"
)
asset_form.process_api_validation_errors(post_asset_response.json())
if (
"message" in post_asset_response.json()
and "json" in post_asset_response.json()["message"]
):
error_msg = str(post_asset_response.json()["message"]["json"])
if "message" in post_asset_response.json():
asset_form.process_api_validation_errors(
post_asset_response.json()["message"]
)
if "json" in post_asset_response.json()["message"]:
error_msg = str(
post_asset_response.json()["message"]["json"]
)
if asset is None:
msg = "Cannot create asset. " + error_msg
return render_flexmeasures_template(
Expand Down Expand Up @@ -339,7 +349,9 @@ def post(self, id: str):
f"Internal asset API call unsuccessful [{patch_asset_response.status_code}]: {patch_asset_response.text}"
)
msg = "Cannot edit asset."
asset_form.process_api_validation_errors(patch_asset_response.json())
asset_form.process_api_validation_errors(
patch_asset_response.json().get("message")
)
asset = GenericAsset.query.get(id)

latest_measurement_time_str, asset_plot_html = _get_latest_power_plot(asset)
Expand Down
10 changes: 9 additions & 1 deletion flexmeasures/ui/templates/crud/asset.html
Expand Up @@ -86,14 +86,22 @@ <h3>Edit {{ asset.name }}</h3>
value="{{ asset.generic_asset_type.name }}" disabled></input>
</div>
</div>

<div class="form-group">
<label for="asset-id" class="col-sm-6 control-label">Asset id</label>
<div class="col-sm-6">
<input class="form-control" id="asset-id" name="asset-id" type="text" value="{{ asset.id }}"
disabled></input>
</div>
</div>
<div class="form-group">
{{ asset_form.attributes.label(class="col-sm-3 control-label") }}
<div class="col-sm-3">
{{ asset_form.attributes(class_="form-control") }}
{% for error in asset_form.errors.attributes %}
<span style="color: red;">[{{error}}]</span>
{% endfor %}
</div>
</div>
<label class="control-label">Location</label>
<small>(Click map to edit latitude and longitude in form)</small>
<div id="mapid"></div>
Expand Down
9 changes: 9 additions & 0 deletions flexmeasures/ui/templates/crud/asset_new.html
Expand Up @@ -70,6 +70,15 @@ <h2> Creating a new asset </h2>
{% endfor %}
</div>
</div>
<div class="form-group">
{{ asset_form.attributes.label(class="col-sm-6 control-label") }}
<div class="col-sm-6">
{{ asset_form.attributes(class_="form-control") }}
{% for error in asset_form.errors.attributes %}
<span style="color: red;">[{{error}}]</span>
{% endfor %}
</div>
</div>
<div class="col-sm-6"></div>
<div class="col-sm-6">
<input type="submit" value="Create">
Expand Down