-
Notifications
You must be signed in to change notification settings - Fork 31
/
generic_assets.py
161 lines (133 loc) · 5.84 KB
/
generic_assets.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from __future__ import annotations
import json
from marshmallow import validates, ValidationError, fields, validates_schema
from flask_security import current_user
from sqlalchemy import select
from flexmeasures.data import ma, db
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.schemas.locations import LatitudeField, LongitudeField
from flexmeasures.data.schemas.utils import (
FMValidationError,
MarshmallowClickMixin,
with_appcontext_if_needed,
)
from flexmeasures.auth.policy import user_has_admin_access
from flexmeasures.cli import is_running as running_as_cli
from flexmeasures.utils.coding_utils import flatten_unique
class JSON(fields.Field):
def _deserialize(self, value, attr, data, **kwargs) -> dict:
try:
return json.loads(value)
except ValueError:
raise ValidationError("Not a valid JSON string.")
def _serialize(self, value, attr, data, **kwargs) -> str:
return json.dumps(value)
class GenericAssetSchema(ma.SQLAlchemySchema):
"""
GenericAsset schema, with validations.
"""
id = ma.auto_field(dump_only=True)
name = fields.Str(required=True)
account_id = ma.auto_field()
latitude = LatitudeField(allow_none=True)
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)
child_assets = ma.Nested("GenericAssetSchema", many=True, dump_only=True)
class Meta:
model = GenericAsset
@validates_schema(skip_on_field_errors=False)
def validate_name_is_unique_under_parent(self, data, **kwargs):
if "name" in data:
asset = db.session.scalars(
select(GenericAsset)
.filter_by(
name=data["name"],
parent_asset_id=data.get("parent_asset_id"),
account_id=data.get("account_id"),
)
.limit(1)
).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):
generic_asset_type = db.session.get(GenericAssetType, generic_asset_type_id)
if not generic_asset_type:
raise ValidationError(
f"GenericAssetType with id {generic_asset_type_id} doesn't exist."
)
@validates("parent_asset_id")
def validate_parent_asset(self, parent_asset_id: int | None):
if parent_asset_id is not None:
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."
)
@validates("account_id")
def validate_account(self, account_id: int | None):
if account_id is None and (
running_as_cli() or user_has_admin_access(current_user, "update")
):
return
account = db.session.get(Account, account_id)
if not account:
raise ValidationError(f"Account with Id {account_id} doesn't exist.")
if not running_as_cli() and (
not user_has_admin_access(current_user, "update")
and account_id != current_user.account_id
):
raise ValidationError(
"User is not allowed to create assets for this account."
)
@validates("attributes")
def validate_attributes(self, attributes: dict):
sensors_to_show = attributes.get("sensors_to_show", [])
# Check type
if not isinstance(sensors_to_show, list):
raise ValidationError("sensors_to_show should be a list.")
for sensor_listing in sensors_to_show:
if not isinstance(sensor_listing, (int, list)):
raise ValidationError(
"sensors_to_show should only contain sensor IDs (integers) or lists thereof."
)
if isinstance(sensor_listing, list):
for sensor_id in sensor_listing:
if not isinstance(sensor_id, int):
raise ValidationError(
"sensors_to_show should only contain sensor IDs (integers) or lists thereof."
)
# Check whether IDs represent accessible sensors
from flexmeasures.data.schemas import SensorIdField
sensor_ids = flatten_unique(sensors_to_show)
for sensor_id in sensor_ids:
SensorIdField().deserialize(sensor_id)
class GenericAssetTypeSchema(ma.SQLAlchemySchema):
"""
GenericAssetType schema, with validations.
"""
id = ma.auto_field()
name = fields.Str()
description = ma.auto_field()
class Meta:
model = GenericAssetType
class GenericAssetIdField(MarshmallowClickMixin, fields.Int):
"""Field that deserializes to a GenericAsset and serializes back to an integer."""
@with_appcontext_if_needed()
def _deserialize(self, value, attr, obj, **kwargs) -> GenericAsset:
"""Turn a generic asset id into a GenericAsset."""
generic_asset = db.session.get(GenericAsset, value)
if generic_asset is None:
raise FMValidationError(f"No asset found with id {value}.")
# lazy loading now (asset is somehow not in session after this)
generic_asset.generic_asset_type
return generic_asset
def _serialize(self, asset, attr, data, **kwargs):
"""Turn a GenericAsset into a generic asset id."""
return asset.id