forked from enviPath/enviPy
[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:
@ -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:
|
||||
|
||||
114
epdb/views.py
114
epdb/views.py
@ -11,13 +11,11 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAll
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from envipy_additional_information import NAME_MAPPING
|
||||
from oauth2_provider.decorators import protected_resource
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from utilities.misc import HTMLGenerator
|
||||
|
||||
from .logic import (
|
||||
EPDBURLParser,
|
||||
@ -2455,72 +2453,7 @@ def package_scenarios(request, package_uuid):
|
||||
}
|
||||
)
|
||||
|
||||
from envipy_additional_information import (
|
||||
SEDIMENT_ADDITIONAL_INFORMATION,
|
||||
SLUDGE_ADDITIONAL_INFORMATION,
|
||||
SOIL_ADDITIONAL_INFORMATION,
|
||||
)
|
||||
|
||||
context["scenario_types"] = {
|
||||
"Soil Data": {
|
||||
"name": "soil",
|
||||
"widgets": [
|
||||
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
|
||||
for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv]
|
||||
],
|
||||
},
|
||||
"Sludge Data": {
|
||||
"name": "sludge",
|
||||
"widgets": [
|
||||
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
|
||||
for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv]
|
||||
],
|
||||
},
|
||||
"Water-Sediment System Data": {
|
||||
"name": "sediment",
|
||||
"widgets": [
|
||||
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
|
||||
for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv]
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
context["sludge_additional_information"] = SLUDGE_ADDITIONAL_INFORMATION
|
||||
context["soil_additional_information"] = SOIL_ADDITIONAL_INFORMATION
|
||||
context["sediment_additional_information"] = SEDIMENT_ADDITIONAL_INFORMATION
|
||||
|
||||
return render(request, "collections/scenarios_paginated.html", context)
|
||||
elif request.method == "POST":
|
||||
log_post_params(request)
|
||||
|
||||
scenario_name = request.POST.get("scenario-name")
|
||||
scenario_description = request.POST.get("scenario-description")
|
||||
|
||||
scenario_date_year = request.POST.get("scenario-date-year")
|
||||
scenario_date_month = request.POST.get("scenario-date-month")
|
||||
scenario_date_day = request.POST.get("scenario-date-day")
|
||||
|
||||
scenario_date = scenario_date_year
|
||||
if scenario_date_month is not None and scenario_date_month.strip() != "":
|
||||
scenario_date += f"-{int(scenario_date_month):02d}"
|
||||
if scenario_date_day is not None and scenario_date_day.strip() != "":
|
||||
scenario_date += f"-{int(scenario_date_day):02d}"
|
||||
|
||||
scenario_type = request.POST.get("scenario-type")
|
||||
|
||||
additional_information = HTMLGenerator.build_models(request.POST.dict())
|
||||
additional_information = [x for sv in additional_information.values() for x in sv]
|
||||
|
||||
new_scen = Scenario.create(
|
||||
current_package,
|
||||
name=scenario_name,
|
||||
description=scenario_description,
|
||||
scenario_date=scenario_date,
|
||||
scenario_type=scenario_type,
|
||||
additional_information=additional_information,
|
||||
)
|
||||
|
||||
return redirect(new_scen.url)
|
||||
else:
|
||||
return HttpResponseNotAllowed(
|
||||
[
|
||||
@ -2547,21 +2480,9 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
|
||||
context["scenario"] = current_scenario
|
||||
|
||||
available_add_infs = []
|
||||
for add_inf in NAME_MAPPING.values():
|
||||
available_add_infs.append(
|
||||
{
|
||||
"display_name": add_inf.property_name(None),
|
||||
"name": add_inf.__name__,
|
||||
"widget": HTMLGenerator.generate_html(add_inf, prefix=f"{0}"),
|
||||
}
|
||||
)
|
||||
context["available_additional_information"] = available_add_infs
|
||||
|
||||
context["update_widgets"] = [
|
||||
HTMLGenerator.generate_html(ai, prefix=f"{i}")
|
||||
for i, ai in enumerate(current_scenario.get_additional_information())
|
||||
]
|
||||
# Note: Modals now fetch schemas and data from API endpoints
|
||||
# Keeping these for backwards compatibility if needed elsewhere
|
||||
# They are no longer used by the main scenario template
|
||||
|
||||
return render(request, "objects/scenario.html", context)
|
||||
|
||||
@ -2581,28 +2502,15 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
current_scenario.save()
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == "set-additional-information":
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.set_additional_information(ais)
|
||||
return redirect(current_scenario.url)
|
||||
# Legacy POST handler - no longer used, modals use API endpoints
|
||||
return HttpResponseBadRequest(
|
||||
"This endpoint is deprecated. Please use the API endpoints."
|
||||
)
|
||||
elif hidden == "add-additional-information":
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if len(ais.keys()) != 1:
|
||||
raise ValueError(
|
||||
"Only one additional information field can be added at a time."
|
||||
)
|
||||
|
||||
ai = list(ais.values())[0][0]
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.add_additional_information(ai)
|
||||
return redirect(current_scenario.url)
|
||||
# Legacy POST handler - no longer used, modals use API endpoints
|
||||
return HttpResponseBadRequest(
|
||||
"This endpoint is deprecated. Please use the API endpoints."
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user