Skip to content
Merged
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Infrastructure / Support

Bugfixes
-----------
* Fix string length exceeding the 255-character limit in the `event` field of `AssetAuditLog` by truncating long updates and logging each field or attribute change individually. [see `PR #1162 <https://github.com/FlexMeasures/flexmeasures/pull/1162>`_]
* Fix image carousel on the login page [see `PR #1154 <https://github.com/FlexMeasures/flexmeasures/pull/1154>`_]
* Fix styling for User and Documentation menu items [see `PR #1140 <https://github.com/FlexMeasures/flexmeasures/pull/1140>`_]
* Fix styling of sensor page, especially the graph chart dropdown [see `PR #1148 <https://github.com/FlexMeasures/flexmeasures/pull/1148>`_]
Expand Down
9 changes: 7 additions & 2 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,13 @@ def patch(self, asset_data: dict, id: int, db_asset: GenericAsset):
)
continue
audit_log_data.append(f"Field: {k}, From: {getattr(db_asset, k)}, To: {v}")
audit_log_event = f"Updated asset '{db_asset.name}': {db_asset.id} fields: {'; '.join(audit_log_data)}"
AssetAuditLog.add_record(db_asset, audit_log_event)

# Iterate over each field or attribute updates and create a separate audit log entry for each.
for event in audit_log_data:
audit_log_event = (
f"Updated asset '{db_asset.name}': {db_asset.id}; fields: {event}"
)
AssetAuditLog.add_record(db_asset, audit_log_event)

for k, v in asset_data.items():
setattr(db_asset, k, v)
Expand Down
12 changes: 11 additions & 1 deletion flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,17 @@ def test_alter_an_asset(
print(f"Editing Response: {asset_edit_response.json}")
assert asset_edit_response.status_code == 200

audit_log_event = f"Updated asset '{prosumer_asset.name}': {prosumer_asset.id} fields: Field: name, From: {name}, To: other; Field: latitude, From: {latitude}, To: 11.1"
audit_log_event = f"Updated asset '{prosumer_asset.name}': {prosumer_asset.id}; fields: Field: name, From: {name}, To: other"
assert db.session.execute(
select(AssetAuditLog).filter_by(
event=audit_log_event,
active_user_id=requesting_user.id,
active_user_name=requesting_user.username,
affected_asset_id=prosumer_asset.id,
)
).scalar_one_or_none()

audit_log_event = f"Updated asset '{prosumer_asset.name}': {prosumer_asset.id}; fields: Field: latitude, From: {latitude}, To: 11.1"
assert db.session.execute(
select(AssetAuditLog).filter_by(
event=audit_log_event,
Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/cli/tests/test_data_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_add_one_sensor_attribute(app, db, setup_markets):
result = runner.invoke(edit_attribute, to_flags(cli_input))
assert result.exit_code == 0 and "Success" in result.output, result.exception

event = f"Updated sensor '{sensor.name}': {sensor.id} Attr 'some new attribute' To 3.0 From None"
event = f"Updated sensor '{sensor.name}': {sensor.id}; Attr 'some new attribute' To 3.0 From None"
assert db.session.execute(
select(AssetAuditLog).filter_by(
affected_asset_id=sensor.generic_asset_id,
Expand Down Expand Up @@ -61,7 +61,7 @@ def test_update_one_asset_attribute(app, db, setup_generic_assets):
result = runner.invoke(edit_attribute, to_flags(cli_input))
assert result.exit_code == 0 and "Success" in result.output, result.exception

event = f"Updated asset '{asset.name}': {asset.id} Attr 'some-attribute' To some-new-value From some-value"
event = f"Updated asset '{asset.name}': {asset.id}; Attr 'some-attribute' To some-new-value From some-value"
assert db.session.execute(
select(AssetAuditLog).filter_by(
affected_asset_id=asset.id,
Expand Down
47 changes: 6 additions & 41 deletions flexmeasures/data/models/audit_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,18 @@ def add_record_for_attribute_update(

old_value = asset_or_sensor.attributes.get(attribute_key)
if entity_type == "sensor":
event = f"Updated sensor '{asset_or_sensor.name}': {asset_or_sensor.id} "
event = f"Updated sensor '{asset_or_sensor.name}': {asset_or_sensor.id}; "
affected_asset_id = (asset_or_sensor.generic_asset_id,)
else:
event = f"Updated asset '{asset_or_sensor.name}': {asset_or_sensor.id} "
event = f"Updated asset '{asset_or_sensor.name}': {asset_or_sensor.id}; "
affected_asset_id = asset_or_sensor.id
event += f"Attr '{attribute_key}' To {attribute_value} From {old_value}"

audit_log = cls(
event_datetime=server_now(),
event=truncate_string(
event, 250
), # we truncate the event string to 250 characters by adding ellipses in the middle
event, 255
), # we truncate the event string if it 255 characters by adding ellipses in the middle
active_user_id=current_user_id,
active_user_name=current_user_name,
affected_asset_id=affected_asset_id,
Expand All @@ -176,50 +176,15 @@ def add_record(
audit_log = AssetAuditLog(
event_datetime=server_now(),
event=truncate_string(
event, 250
), # we truncate the event string to 250 characters by adding ellipses in the middle
event, 255
), # we truncate the event string if it exceed 255 characters by adding ellipses in the middle
active_user_id=current_user_id,
active_user_name=current_user_name,
affected_asset_id=asset.id,
)
db.session.add(audit_log)


def truncate_event(
event: str, attr_key: str, old_value: str, new_value: str, max_length: int = 250
) -> str:
"""
Truncate the event string and add ellipses if it exceeds max_length.

Args:
event (str): The event message to be truncated.
attr_key (str): The attribute key related to the change.
old_value (str): The old value of the attribute.
new_value (str): The new value of the attribute.
max_length (int): The maximum length for the truncated event string.

Notes:
The default max_length of 250 characters helps ensure the entire log entry stays within a 255-character limit.
The 60-character limit for old_value and new_value ensures that the change_summary remains readable while fitting within it's maximum allowed length.
This choice is based on an assumption that other parts of the log message will not exceed 130 characters combined.
"""
# Truncate values only if event length exceeds max_length
if len(event) > max_length:
truncated_new_value = truncate_string(str(new_value), 60)
truncated_old_value = truncate_string(str(old_value), 60)
else:
truncated_new_value = new_value
truncated_old_value = old_value

# Construct the log entry
change_summary = (
f"Attr: {attr_key}, From: {truncated_old_value}, To: {truncated_new_value}"
)

# Ensure the change summary fits within the maximum length
return truncate_string(change_summary, max_length)


def truncate_string(value: str, max_length: int) -> str:
"""Truncate a string and add ellipses in the middle if it exceeds max_length."""
if len(value) <= max_length:
Expand Down