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
16 changes: 16 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,20 @@
)


class JSON(fields.Field):
def _deserialize(self, value, attr, data, **kwargs):
if value:
try:
return json.loads(value)
except ValueError:
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not raise a validation error, if content is non empty?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this line is a problem. I edited asset attributes with invalid JSON, which got accepted.

In the database, the asset's attributes are actually null now. Maybe this is due to this line, giving None if the input is not valid.

Now, loading the asset page fails:

  File "/home/nicolas/workspace/seita/flexmeasures/flexmeasures/ui/charts/latest_state.py", line 57, in get_latest_power_as_plot
    capacity_in_mw = sensor.get_attribute("capacity_in_mw", latest_power_value)
  File "/home/nicolas/workspace/seita/flexmeasures/flexmeasures/data/models/time_series.py", line 168, in get_attribute
    if attribute in self.generic_asset.attributes:
TypeError: argument of type 'NoneType' is not iterable

The CLI also fails to show the asset now:

  File "/home/nicolas/workspace/seita/flexmeasures/flexmeasures/cli/data_show.py", line 170, in show_generic_asset
    "".join([f"{k}:{v}\n" for k, v in asset.attributes.items()]),
AttributeError: 'NoneType' object has no attribute 'items'


return None

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


class GenericAssetSchema(ma.SQLAlchemySchema):
"""
GenericAsset schema, with validations.
Expand All @@ -23,6 +38,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
9 changes: 8 additions & 1 deletion 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")
Flix6x marked this conversation as resolved.
Show resolved Hide resolved

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
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