[Feature] Dynamic additional information rendering in frontend (#282)

This implements a version of #274, relying on Pydantics built in JSON schema and JSON rendering.
Requires additional UI tagging in the ai model repo but will remove HTML tags.

Example scenario with filled information: 5882df9c-dae1-4d80-a40e-db4724271456/scenario/3a4d395a-6a6d-4154-8ce3-ced667fceec0

Reviewed-on: enviPath/enviPy#282
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
2026-01-31 00:44:03 +13:00
committed by jebus
parent 9f63a9d4de
commit d80dfb5ee3
42 changed files with 3732 additions and 609 deletions

View File

@ -3822,11 +3822,21 @@ class Scenario(EnviPathModel):
return new_s
@transaction.atomic
def add_additional_information(self, data: "EnviPyModel"):
def add_additional_information(self, data: "EnviPyModel") -> str:
"""
Add additional information to this scenario.
Args:
data: EnviPyModel instance to add
Returns:
str: UUID of the created item
"""
cls_name = data.__class__.__name__
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}"
generated_uuid = str(uuid4())
ai_data["uuid"] = generated_uuid
if cls_name not in self.additional_information:
self.additional_information[cls_name] = []
@ -3834,6 +3844,51 @@ class Scenario(EnviPathModel):
self.additional_information[cls_name].append(ai_data)
self.save()
return generated_uuid
@transaction.atomic
def update_additional_information(self, ai_uuid: str, data: "EnviPyModel") -> None:
"""
Update existing additional information by UUID.
Args:
ai_uuid: UUID of the item to update
data: EnviPyModel instance with new data
Raises:
ValueError: If item with given UUID not found or type mismatch
"""
found_type = None
found_idx = -1
# Find the item by UUID
for type_name, items in self.additional_information.items():
for idx, item_data in enumerate(items):
if item_data.get("uuid") == ai_uuid:
found_type = type_name
found_idx = idx
break
if found_type:
break
if found_type is None:
raise ValueError(f"Additional information with UUID {ai_uuid} not found")
# Verify the model type matches (prevent type changes)
new_type = data.__class__.__name__
if new_type != found_type:
raise ValueError(
f"Cannot change type from {found_type} to {new_type}. "
f"Delete and create a new item instead."
)
# Update the item data, preserving UUID
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data["uuid"] = ai_uuid
self.additional_information[found_type][found_idx] = ai_data
self.save()
@transaction.atomic
def remove_additional_information(self, ai_uuid):
found_type = None
@ -3848,9 +3903,9 @@ class Scenario(EnviPathModel):
if found_type is not None and found_idx >= 0:
if len(self.additional_information[found_type]) == 1:
del self.additional_information[k]
del self.additional_information[found_type]
else:
self.additional_information[k].pop(found_idx)
self.additional_information[found_type].pop(found_idx)
self.save()
else:
raise ValueError(f"Could not find additional information with uuid {ai_uuid}")
@ -3873,7 +3928,7 @@ class Scenario(EnviPathModel):
self.save()
def get_additional_information(self):
from envipy_additional_information import NAME_MAPPING
from envipy_additional_information import registry
for k, vals in self.additional_information.items():
if k == "enzyme":
@ -3881,7 +3936,7 @@ class Scenario(EnviPathModel):
for v in vals:
# Per default additional fields are ignored
MAPPING = {c.__name__: c for c in NAME_MAPPING.values()}
MAPPING = {c.__name__: c for c in registry.list_models().values()}
inst = MAPPING[k](**v)
# Add uuid to uniquely identify objects for manipulation
if "uuid" in v: