Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ def fetch_one(self, id, asset):
return asset_schema.dump(asset), 200

@route("/<id>", methods=["PATCH"])
@use_args(patch_asset_schema)
@use_kwargs(
{
"db_asset": AssetIdField(
Expand All @@ -673,7 +672,7 @@ def fetch_one(self, id, asset):
)
@permission_required_for_context("update", ctx_arg_name="db_asset")
@as_json
def patch(self, asset_data: dict, id: int, db_asset: GenericAsset):
def patch(self, id: int, db_asset: GenericAsset):
"""
.. :quickref: Assets; Update an asset given its identifier.
---
Expand Down Expand Up @@ -731,10 +730,23 @@ def patch(self, asset_data: dict, id: int, db_asset: GenericAsset):
tags:
- Assets
"""
asset_data = request.get_json()
Copy link
Contributor

Choose a reason for hiding this comment

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

Just add a comment: "validating the path data ourselves, as we want to add context information (current asset).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok sure, I can do that, I should be home in a few hours

if not asset_data:
return unprocessable_entity("No JSON data provided.")

asset_schema = AssetSchema(partial=True)
asset_schema.context = {"asset": db_asset}

try:
validated_data = asset_schema.load(asset_data)
except ValidationError as e:
return unprocessable_entity(e.messages)

try:
db_asset = patch_asset(db_asset, asset_data)
db_asset = patch_asset(db_asset, validated_data)
except ValidationError as e:
return unprocessable_entity(str(e.messages))
return unprocessable_entity(e.messages)

db.session.add(db_asset)
db.session.commit()
return asset_schema.dump(db_asset), 200
Expand Down
47 changes: 33 additions & 14 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,17 +209,23 @@ def validate_name_is_unique_under_parent(self, data, **kwargs):
Here, we can only check if we have all information (a full form),
which usually is at creation time.
"""
if "name" in data and "parent_asset_id" in data:
asset = db.session.scalars(
select(GenericAsset)
.filter_by(
name=data["name"],
parent_asset_id=data.get("parent_asset_id"),
)
.limit(1)
).first()

if asset:
if "name" in data:
parent_id = data.get("parent_asset_id", None)

query = select(GenericAsset).filter_by(
name=data["name"],
parent_asset_id=parent_id,
)

current_editing_id = getattr(self, "context", {}).get("asset_id")

if current_editing_id is not None:
query = query.filter(GenericAsset.id != current_editing_id)

existing_asset = db.session.scalars(query).first()

if existing_asset:
err_msg = f"An asset with the name '{data['name']}' already exists under parent asset {data.get('parent_asset_id')}"
raise ValidationError(err_msg, "name")

Expand All @@ -233,11 +239,24 @@ def validate_generic_asset_type(self, generic_asset_type_id: int, **kwargs):

@validates("parent_asset_id")
def validate_parent_asset(self, parent_asset_id: int | None, **kwargs):
if parent_asset_id is not None:
parent_asset = db.session.get(GenericAsset, parent_asset_id)
if not parent_asset:
if parent_asset_id is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

You added a second condition, so a short docstring would be nice:

"""
If a parent_asset_id is set, the parent asset should

  • exist
  • be in the same account of the context asset (if that is known via schema.context["asset"])

"""

return

parent_asset = db.session.get(GenericAsset, parent_asset_id)
if not parent_asset:
raise ValidationError(
f"Parent GenericAsset with id {parent_asset_id} doesn't exist."
)

# Check account consistency (using context)
# Safely get the asset from context, defaulting to None if creating new
current_asset = self.context.get("asset")

# If editing an existing asset (context exists)
if current_asset and current_asset.account_id:
if parent_asset.account_id != current_asset.account_id:
raise ValidationError(
f"Parent GenericAsset with id {parent_asset_id} doesn't exist."
"Parent asset must belong to the same account as the child asset."
)

@validates("account_id")
Expand Down
108 changes: 62 additions & 46 deletions flexmeasures/ui/templates/assets/asset_properties.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ <h3>Edit {{ asset.name }}</h3>
{% endfor %}
</div>
</div>
<div class="form-group">
{{ asset_form.parent_asset_id.label(class="col-sm-6 control-label") }}
<div class="col-md-6">
<select name="parent_asset_id" id="parent_asset_id" class="form-control">
<option value="" {% if asset.parent_asset_id is none %} selected {% endif
%}>No Parent</option>
{% for account_asset in account_assets %}
{% if account_asset.id != asset.id %}
<option value="{{ account_asset.id }}" {% if
asset.parent_asset_id==account_asset.id %} selected {% endif %}>
{{ account_asset.name }} (ID: {{ account_asset.id }})</option>
{% endif %}
{% endfor %}
</select>

</div>
</div>
<div class="form-group">
<label for="assset-type" class="col-sm-6 control-label">Asset Type</label>
<div class="col-md-6">
Expand Down Expand Up @@ -103,7 +120,7 @@ <h3>Edit {{ asset.name }}</h3>
{% endfor %}
</div>
</div>

<div class="form-group">
<label class="control-label">Location</label>
<small>(Click map to edit latitude and longitude in form)</small>
Expand All @@ -120,12 +137,13 @@ <h3>Edit {{ asset.name }}</h3>
</div>
</div>
{% endif %}

<div class="header-action-button">
{% if user_can_delete_asset %}
<div>
<form action="/assets/delete_with_data/{{ asset.id }}" method="get">
<button id="delete-asset-button" class="btn btn-sm btn-responsive btn-danger" type="submit">Delete this
<button id="delete-asset-button" class="btn btn-sm btn-responsive btn-danger"
type="submit">Delete this
asset
</button>
</form>
Expand Down Expand Up @@ -279,11 +297,8 @@ <h3>All child assets for {{ asset.name }} </h3>
<div class="modal-header">
<h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
<div class="dropdown">
<span class="fa fa-info dropdown-toggle"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
tabindex="0">
<span class="fa fa-info dropdown-toggle" role="button" data-bs-toggle="dropdown"
aria-expanded="false" tabindex="0">
</span>

<div class="dropdown-menu p-3" style="width: 400px;">
Expand Down Expand Up @@ -317,7 +332,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
</div>
</div>
</div>

<div class="d-flex ms-auto align-items-center gap-2">
<button type="button" id="saveChangesBtn" class="btn btn-primary ms-auto">Save</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
Expand Down Expand Up @@ -353,7 +368,8 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
</select>
</div>
<div class="col-2 p-0">
<button id="addModelFieldbtn" class="btn btn-secondary btn-md">Add field</button>
<button id="addModelFieldbtn" class="btn btn-secondary btn-md">Add
field</button>
</div>
</div>

Expand Down Expand Up @@ -412,7 +428,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>

// This variable is used to prevent reRenderForm from running on initial load
let hasInitialized = false;

const schemaSpecs = {{ flex_model_schema | tojson | safe }};
const assetFlexModelSchema = {};
const FlexModelFieldValidTypes = {};
Expand All @@ -436,7 +452,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
FlexModelFieldValidTypes[key] = validTypes;
}

const [assetFlexModelRawJSON, extraFields] = processResourceRawJSON( {...assetFlexModelSchema }, '{{ asset_flexmodel | safe }}', true);
const [assetFlexModelRawJSON, extraFields] = processResourceRawJSON({ ...assetFlexModelSchema }, '{{ asset_flexmodel | safe }}', true);

document.addEventListener('DOMContentLoaded', async function () {
const flexOptionsContainer = document.getElementById('flexOptionsContainer');
Expand All @@ -450,7 +466,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
saveChangesBtn.onclick = async function () {
await updateFlexModel();
};

const [getFlexModel, setFlexModel] = createReactiveState(assetFlexModelRawJSON, reRenderForm);
const [activeCard, setActiveCard] = createReactiveState(null, () => { senSearchResEle.style.display = 'none'; });
const [selectedIndex, setSelectedIndex] = createReactiveState(null, renderFlexInputOptions);
Expand Down Expand Up @@ -611,7 +627,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>

// Clean null values
for (const [key, value] of Object.entries(data)) {
if (
if (
value === null ||
value === "" ||
value === "Not Set" ||
Expand Down Expand Up @@ -1051,7 +1067,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
input.className = 'form-control';
input.id = 'flexStringInput';
input.placeholder = 'Enter a value';
input.value = `${(typeof value === 'string' && value !== "Not Set")? value : ''}`;
input.value = `${(typeof value === 'string' && value !== "Not Set") ? value : ''}`;
input.onkeydown = function (event) {
if (event.key === 'Enter') {
event.preventDefault();
Expand Down Expand Up @@ -1164,7 +1180,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
renderFlexModelForm(); // Initial render of flex model form
renderFlexFieldOptions(assetFlexModelSchema, getFlexModel());
});

flexSelect.addEventListener('change', (event) => {
const selectedKey = event.target.value;
renderSelectInfoCards(selectedKey);
Expand Down Expand Up @@ -1221,21 +1237,21 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>
}

$(document).ready(function () {
let unit = "";
// Initialize the DataTable
const table = $("#sensorsTable").dataTable({
order: [[0, "asc"]],
pageLength: 5,
lengthMenu: [5, 10, 25, 50, 75, 100],
serverSide: true,
// make the table row vertically aligned with header
columns: [
{ data: "id", title: "ID", orderable: true },
{ data: "name", title: "Name", orderable: true },
{ data: "unit", title: "Unit", orderable: false },
{ data: "resolution", title: "Resolution", orderable: true },
{ data: "url", title: "URL", className: "d-none" },
],
let unit = "";
// Initialize the DataTable
const table = $("#sensorsTable").dataTable({
order: [[0, "asc"]],
pageLength: 5,
lengthMenu: [5, 10, 25, 50, 75, 100],
serverSide: true,
// make the table row vertically aligned with header
columns: [
{ data: "id", title: "ID", orderable: true },
{ data: "name", title: "Name", orderable: true },
{ data: "unit", title: "Unit", orderable: false },
{ data: "resolution", title: "Resolution", orderable: true },
{ data: "url", title: "URL", className: "d-none" },
],

ajax: function (data, callback, settings) {
const basePath = window.location.origin;
Expand All @@ -1261,22 +1277,22 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-model</h5>



$.ajax({
type: "get",
url: url,
success: function (response, text) {
let clean_response = [];
response["data"].forEach((element) =>
clean_response.push(
new Sensor(
element["id"],
element["name"],
element["unit"],
element["event_resolution"],
element["active"]
$.ajax({
type: "get",
url: url,
success: function (response, text) {
let clean_response = [];
response["data"].forEach((element) =>
clean_response.push(
new Sensor(
element["id"],
element["name"],
element["unit"],
element["event_resolution"],
element["active"]
)
)
)
);
);

callback({
data: clean_response,
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/ui/tests/test_asset_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def test_add_asset(db, client, setup_assets, as_admin):

def test_edit_asset(db, client, setup_assets, as_admin):
mock_asset = mock_asset_data_with_kpis(db=db, as_list=False)
mock_asset["name"] = "Edited name"

response = client.post(
url_for("AssetCrudUI:post", id=1),
follow_redirects=True,
Expand Down
5 changes: 4 additions & 1 deletion flexmeasures/ui/views/assets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class AssetForm(FlaskForm):
places=None,
render_kw={"placeholder": "--Click the map or enter a longitude--"},
)
parent_asset_id = IntegerField(
"Parent Asset Id",
validators=[optional()],
)
attributes = StringField(
"Other attributes (JSON)",
default="{}",
Expand Down Expand Up @@ -122,7 +126,6 @@ class NewAssetForm(AssetForm):
"Asset type", coerce=int, validators=[DataRequired()]
)
account_id = SelectField("Account", coerce=int, validators=[optional()])
parent_asset_id = IntegerField("Parent Asset Id", validators=[optional()])

def set_account(self) -> tuple[Account | None, str | None]:
"""Set an account for the to-be-created asset.
Expand Down
10 changes: 10 additions & 0 deletions flexmeasures/ui/views/assets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def post(self, id: str): # noqa: C901
session["msg"] = f"Cannot edit asset: {asset_form.errors}"
return redirect(url_for("AssetCrudUI:properties", id=id))
try:
patch_asset_schema.context = {"asset_id": asset.id}
loaded_asset_data = patch_asset_schema.load(asset_form.to_json())
patch_asset(asset, loaded_asset_data)
db.session.commit()
Expand Down Expand Up @@ -399,9 +400,18 @@ def properties(self, id: str):

from flexmeasures.data.schemas.scheduling import UI_FLEX_MODEL_SCHEMA

# order by ID in asc
account_assets = (
db.session.query(GenericAsset)
.filter_by(account_id=asset.account_id)
.order_by(GenericAsset.id.asc())
.all()
)

return render_flexmeasures_template(
"assets/asset_properties.html",
asset=asset,
account_assets=account_assets,
site_asset=site_asset,
flex_model_schema=UI_FLEX_MODEL_SCHEMA,
asset_flexmodel=json.dumps(asset.flex_model),
Expand Down