diff --git a/epapi/v1/endpoints/additional_information.py b/epapi/v1/endpoints/additional_information.py index 47b169f9..c5cb9124 100644 --- a/epapi/v1/endpoints/additional_information.py +++ b/epapi/v1/endpoints/additional_information.py @@ -9,8 +9,8 @@ from envipy_additional_information import registry from envipy_additional_information.groups import GroupEnum from epapi.utils.schema_transformers import build_rjsf_output from epapi.utils.validation_errors import handle_validation_error -from epdb.models import AdditionalInformation -from ..dal import get_scenario_for_read, get_scenario_for_write +from epdb.models import AdditionalInformation, Scenario, Node +from ..dal import get_scenario_for_read, get_scenario_for_write, get_package_for_write logger = logging.getLogger(__name__) @@ -58,6 +58,61 @@ def list_scenario_info(request, scenario_uuid: UUID): return result +@router.post("/information/{model_name}/") +def add_object_info(request, model_name: str, payload: Dict[str, Any] = Body(...)): + from epdb.views import EPDBURLParser + + cls = registry.get_model(model_name.lower()) + if not cls: + raise HttpError(404, f"Unknown model: {model_name}") + + try: + instance = cls(**payload) # Pydantic validates + except ValidationError as e: + handle_validation_error(e) + + if "attach_obj_url" in payload: + url_parser = EPDBURLParser(payload["attach_obj_url"]) + + if url_parser.contains_package_url(): + package = get_package_for_write(request.user, url_parser.get_objects()[0].uuid) + attach_obj = url_parser.get_object() + + if "scenario_uuid" in payload: + scenario = get_scenario_for_read(request.user, payload["scenario_uuid"]) + else: + scenario = Scenario.create( + package, + name=f"Scenario {Scenario.objects.filter(package=package).count() + 1}", + description="no description", + scenario_date=None, + scenario_type=None, + additional_information=[], + ) + + if isinstance(attach_obj, Node): + ai = add_info_to_node(package, instance, scenario, attach_obj) + else: + raise HttpError(404, f"Bad request - Not implemented for {type(attach_obj)}!") + + return {"status": "created", "uuid": ai.uuid} + + raise HttpError(404, "Bad request!") + + +def add_info_to_node(package, add_inf, scenario, node): + ai = AdditionalInformation.create( + package, + add_inf, + scenario=scenario, + content_object=node, + ) + + node.pathway.scenarios.add(scenario) + + return ai + + @router.post("/scenario/{uuid:scenario_uuid}/information/{model_name}/") def add_scenario_info( request, scenario_uuid: UUID, model_name: str, payload: Dict[str, Any] = Body(...) diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index 0936ee10..a6f58ef2 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -1620,7 +1620,7 @@ class PathwayNode(Schema): image: str = Field(None, alias="image") imageSize: int = Field(None, alias="image_size") name: str = Field(None, alias="name") - proposed: List[Dict[str, str]] = [] + proposed: List[Dict[str, Any]] = [] smiles: str = Field(None, alias="smiles") pseudo: bool = Field(False, alias="pseudo") diff --git a/epdb/models.py b/epdb/models.py index 04387347..947acf28 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -2455,7 +2455,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin) "name": self.get_name(), "plain_name": self.get_name(include_suffix=False), "smiles": self.default_node_label.smiles, - "scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()], + "scenarios": [{"name": s.get_name(), "url": s.url} for s in self.get_scenarios()], "app_domain": { "inside_app_domain": app_domain_data["assessment"]["inside_app_domain"] if app_domain_data @@ -2583,6 +2583,15 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin) return list(collected.values()) + def get_scenarios(self): + qs = self.scenarios.all() + qs |= Scenario.objects.filter( + id__in=self.additional_information.filter(scenario__isnull=False) + .values_list("scenario", flat=True) + .distinct() + ) + return qs.distinct() + class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin): pathway = models.ForeignKey( diff --git a/static/js/api/additional-information.js b/static/js/api/additional-information.js index 616a3cc6..858720d8 100644 --- a/static/js/api/additional-information.js +++ b/static/js/api/additional-information.js @@ -186,6 +186,40 @@ window.AdditionalInformationApi = { return this._handleResponse(response, "createItem"); }, + /** + * Create new additional information and attach it to an object. + + * @param {string} modelName - Name/type of the additional information model + * @param {Object} data - Data for the new item + * @param {string} attachObjectUrl - UUID of the object this data should be attached to + * @param {string} scenarioUuid - UUID of the scenario + * @returns {Promise<{status: string, uuid: string}>} + */ + async createItemOnNonScenarioObject(modelName, data, attachObjectUrl, scenarioUuid) { + const sanitizedData = this.sanitizePayload(data); + this._log("createItemOnNonScenarioObject", { modelName, data: sanitizedData, attachObjectUrl, scenarioUuid }); + + sanitizedData.attach_obj_url = attachObjectUrl; + if (scenarioUuid) { + sanitizedData.scenario_uuid = scenarioUuid; + } + + + // Normalize model name to lowercase + const normalizedName = modelName.toLowerCase(); + + const response = await fetch( + `/api/v1/information/${normalizedName}/`, + { + method: "POST", + headers: this._buildHeaders(), + body: JSON.stringify(sanitizedData), + }, + ); + + return this._handleResponse(response, "createItemOnNonScenarioObject"); + }, + /** * Delete additional information from a scenario * @param {string} scenarioUuid - UUID of the scenario diff --git a/templates/actions/objects/pathway.html b/templates/actions/objects/pathway.html index 91c861d6..a30a7284 100644 --- a/templates/actions/objects/pathway.html +++ b/templates/actions/objects/pathway.html @@ -83,6 +83,19 @@ Edit Pathway +