Skip to content

Commit

Permalink
Merge branch 'feature/planning/adapt-scheduler-multiple-output' into …
Browse files Browse the repository at this point in the history
…feature/planning/update-tests-multiple-output
  • Loading branch information
victorgarcia98 committed Oct 19, 2023
2 parents 83caea3 + 9aafbbb commit 8a2ec2a
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 36 deletions.
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ New features
Infrastructure / Support
----------------------

- Introduces a new one-to-many relation between assets, allowing the definition of an asset's parent (which is also an asset). This hierarchical relationship enables assets to be related in a structured manner. [see `PR #855 <https://github.com/FlexMeasures/flexmeasures/pull/855>`_]
- Introduces a new one-to-many relation between assets, allowing the definition of an asset's parent (which is also an asset). This hierarchical relationship enables assets to be related in a structured manner. [see `PR #855 <https://github.com/FlexMeasures/flexmeasures/pull/855>`_ and `PR #874 <https://github.com/FlexMeasures/flexmeasures/pull/874>`_]


v0.16.1 | October 2, 2023
Expand Down
35 changes: 35 additions & 0 deletions flexmeasures/api/v3_0/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ def keep_scheduling_queue_empty(app):
app.queues["scheduling"].empty()


@pytest.fixture(scope="module")
def add_asset_with_children(db, setup_roles_users):
test_supplier_user = setup_roles_users["Test Supplier User"]
parent_type = GenericAssetType(
name="parent",
)
child_type = GenericAssetType(name="child")

db.session.add_all([parent_type, child_type])

parent = GenericAsset(
name="parent",
generic_asset_type=parent_type,
account_id=test_supplier_user,
)
db.session.flush() # assign sensor ids

assets = [
GenericAsset(
name=f"child_{i}",
generic_asset_type=child_type,
parent_asset_id=parent.id,
account_id=test_supplier_user,
)
for i in range(1, 3)
]

db.session.add_all(assets)
db.session.flush() # assign sensor ids

assets.append(parent)

return {a.name: a for a in assets}


def add_incineration_line(db, test_supplier_user) -> dict[str, Sensor]:
incineration_type = GenericAssetType(
name="waste incinerator",
Expand Down
56 changes: 56 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,59 @@ def test_delete_an_asset(client, setup_api_test_data, requesting_user):
assert delete_asset_response.status_code == 204
deleted_asset = GenericAsset.query.filter_by(id=existing_asset_id).one_or_none()
assert deleted_asset is None


@pytest.mark.parametrize(
"parent_name, child_name, fails",
[
("parent", "child_4", False),
(None, "child_1", False),
(None, "child_1", True),
("parent", "child_1", True),
],
)
@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
def test_post_an_asset_with_existing_name(
client, add_asset_with_children, parent_name, child_name, fails, requesting_user
):
"""Catch DB error (Unique key violated) correctly.
Cases:
1) Create a child asset
2) Create an orphan asset with a name that already exists under a parent asset
3) Create an orphan asset with an existing name.
4) Create a child asset with a name that already exists among its siblings.
"""

post_data = get_asset_post_data()

def get_asset_with_name(asset_name):
return GenericAsset.query.filter(GenericAsset.name == asset_name).one_or_none()

parent = get_asset_with_name(parent_name)

post_data["name"] = child_name
post_data["account_id"] = requesting_user.account_id

if parent:
post_data["parent_asset_id"] = parent.parent_asset_id

asset_creation_response = client.post(
url_for("AssetAPI:post"),
json=post_data,
)

if fails:
assert asset_creation_response.status_code == 422
assert (
"already exists"
in asset_creation_response.json["message"]["json"]["name"][0]
)
else:
assert asset_creation_response.status_code == 201

for key, val in post_data.items():
assert asset_creation_response.json[key] == val

# check that the asset exists
assert GenericAsset.query.get(asset_creation_response.json["id"]) is not None
4 changes: 3 additions & 1 deletion flexmeasures/cli/data_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ def delete_asset_and_data(asset: GenericAsset, force: bool):
Delete an asset & also its sensors and data.
"""
if not force:
prompt = f"Delete {asset.__repr__()}, including all its sensors and data?"
prompt = (
f"Delete {asset.__repr__()}, including all its sensors, data and children?"
)
click.confirm(prompt, abort=True)
db.session.delete(asset)
db.session.commit()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Delete children assets on cascade when deleting an asset.
Revision ID: a60cc43aef5e
Revises: ac5e340cccea
Create Date: 2023-10-11 14:04:19.447773
"""
from alembic import op


# revision identifiers, used by Alembic.
revision = "a60cc43aef5e"
down_revision = "ac5e340cccea"
branch_labels = None
depends_on = None


def upgrade():
op.drop_constraint(
"generic_asset_parent_asset_id_generic_asset_fkey",
"generic_asset",
type_="foreignkey",
)
op.create_foreign_key(
None,
"generic_asset",
"generic_asset",
["parent_asset_id"],
["id"],
ondelete="CASCADE",
)


def downgrade():
op.drop_constraint(
"generic_asset_parent_asset_id_generic_asset_fkey",
"generic_asset",
type_="foreignkey",
)
op.create_foreign_key(
None,
"generic_asset",
"generic_asset",
["parent_asset_id"],
["id"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Change the scope of the unique (name, account_id) constraint to
restricts the name to be unique among the sibling assets.
Drop generic_asset_name_account_id_key constraint
Revision ID: ac5e340cccea
Revises: 40d6c8e4be94
Create Date: 2023-10-05 15:13:36.641051
"""
from alembic import op


# revision identifiers, used by Alembic.
revision = "ac5e340cccea"
down_revision = "40d6c8e4be94"
branch_labels = None
depends_on = None


def upgrade():
op.create_unique_constraint(
"generic_asset_name_parent_asset_id_key",
"generic_asset",
["name", "parent_asset_id"],
)

op.drop_constraint(
"generic_asset_name_account_id_key", "generic_asset", type_="unique"
)


def downgrade():
op.create_unique_constraint(
"generic_asset_name_account_id_key", "generic_asset", ["name", "account_id"]
)
op.drop_constraint(
"generic_asset_name_parent_asset_id_key", "generic_asset", type_="unique"
)
30 changes: 17 additions & 13 deletions flexmeasures/data/models/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.sql.expression import func
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.schema import UniqueConstraint
from timely_beliefs import BeliefsDataFrame, utils as tb_utils

from flexmeasures.data import db
Expand Down Expand Up @@ -49,7 +48,16 @@ class GenericAsset(db.Model, AuthModelMixin):
Examples of intangible assets: a market, a country, a copyright.
"""

__table_args__ = db.CheckConstraint("parent_asset_id != id")
__table_args__ = (
db.CheckConstraint(
"parent_asset_id != id", name="generic_asset_self_reference_ck"
),
db.UniqueConstraint(
"name",
"parent_asset_id",
name="generic_asset_name_parent_asset_id_key",
),
)

# No relationship
id = db.Column(db.Integer, primary_key=True)
Expand All @@ -60,14 +68,18 @@ class GenericAsset(db.Model, AuthModelMixin):

# One-to-many (or many-to-one?) relationships
parent_asset_id = db.Column(
db.Integer, db.ForeignKey("generic_asset.id"), nullable=True
db.Integer, db.ForeignKey("generic_asset.id", ondelete="CASCADE"), nullable=True
)
generic_asset_type_id = db.Column(
db.Integer, db.ForeignKey("generic_asset_type.id"), nullable=False
)
parent_asset = db.relationship(
"GenericAsset", remote_side=[id], backref="child_assets"

child_assets = db.relationship(
"GenericAsset",
cascade="all",
backref=db.backref("parent_asset", remote_side="GenericAsset.id"),
)

generic_asset_type = db.relationship(
"GenericAssetType",
foreign_keys=[generic_asset_type_id],
Expand All @@ -81,14 +93,6 @@ class GenericAsset(db.Model, AuthModelMixin):
backref=db.backref("assets", lazy="dynamic"),
)

__table_args__ = (
UniqueConstraint(
"name",
"account_id",
name="generic_asset_name_account_id_key",
),
)

def __acl__(self):
"""
All logged-in users can read if the asset is public.
Expand Down
36 changes: 15 additions & 21 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json

from marshmallow import validates, validates_schema, ValidationError, fields
from marshmallow import validates, ValidationError, fields, validates_schema
from flask_security import current_user

from flexmeasures.data import ma
Expand Down Expand Up @@ -42,32 +42,26 @@ class GenericAssetSchema(ma.SQLAlchemySchema):
longitude = LongitudeField(allow_none=True)
generic_asset_type_id = fields.Integer(required=True)
attributes = JSON(required=False)
parent_asset_id = fields.Int(required=False, allow_none=True)

class Meta:
model = GenericAsset

@validates_schema(skip_on_field_errors=False)
def validate_name_is_unique_in_account(self, data, **kwargs):
def validate_name_is_unique_under_parent(self, data, **kwargs):
if "name" in data:
if data.get("account_id") is None:
asset = GenericAsset.query.filter(
GenericAsset.name == data["name"], GenericAsset.account_id.is_(None)
).first()
if asset:
raise ValidationError(
f"A public asset with the name {data['name']} already exists.",
"name",
)
else:
asset = GenericAsset.query.filter(
GenericAsset.name == data["name"],
GenericAsset.account_id == data["account_id"],
).first()
if asset:
raise ValidationError(
f"An asset with the name {data['name']} already exists in this account.",
"name",
)

asset = GenericAsset.query.filter(
GenericAsset.name == data["name"],
GenericAsset.parent_asset_id == data.get("parent_asset_id"),
GenericAsset.account_id == data.get("account_id"),
).first()

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

@validates("generic_asset_type_id")
def validate_generic_asset_type(self, generic_asset_type_id: int):
Expand Down

0 comments on commit 8a2ec2a

Please sign in to comment.