diff --git a/epdb/logic.py b/epdb/logic.py index a5f7ded4..6d5b930f 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -1,5 +1,6 @@ import re import logging +import json from typing import Union, List, Optional, Set, Dict, Any from django.contrib.auth import get_user_model @@ -552,11 +553,13 @@ class PackageManager(object): try: res = AdditionalInformationConverter.convert(name, addinf_data) + res_cls_name = res.__class__.__name__ + ai_data = json.loads(res.model_dump_json()) + ai_data['uuid'] = f"{uuid4()}" + new_add_inf[res_cls_name].append(ai_data) except: logger.error(f"Failed to convert {name} with {addinf_data}") - new_add_inf[name].append(res.model_dump_json()) - scen.additional_information = new_add_inf scen.save() diff --git a/epdb/models.py b/epdb/models.py index 6ce37918..37d73c61 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -20,6 +20,7 @@ from django.db import models, transaction from django.db.models import JSONField, Count, Q, QuerySet from django.utils import timezone from django.utils.functional import cached_property +from envipy_additional_information import EnviPyModel from model_utils.models import TimeStampedModel from polymorphic.models import PolymorphicModel from sklearn.metrics import precision_score, recall_score, jaccard_score @@ -2292,7 +2293,8 @@ class ApplicabilityDomain(EnviPathModel): transformation = { 'rule': rule_data, 'reliability': rule_reliabilities[rule_idx], - # TODO + # We're setting it here to False, as we don't know whether "assess" is called during pathway + # prediction or from Model Page. For persisted Nodes this field will be overwritten at runtime 'is_predicted': False, 'local_compatibility': local_compatibilities[rule_idx], 'probability': preds[rule_idx].probability, @@ -2407,27 +2409,88 @@ class Scenario(EnviPathModel): @staticmethod @transaction.atomic - def create(package, name, description, date, type, additional_information): + def create(package: 'Package', name:str, description:str, scenario_date:str, scenario_type:str, additional_information: List['EnviPyModel']): s = Scenario() s.package = package + + if name is None or name.strip() == '': + name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}" + s.name = name - s.description = description - s.date = date - s.type = type - s.additional_information = additional_information + + if description is not None and description.strip() != '': + s.description = description + + if scenario_date is not None and scenario_date.strip() != '': + s.scenario_date = scenario_date + + if scenario_type is not None and scenario_type.strip() != '': + s.scenario_type = scenario_type + + add_inf = defaultdict(list) + + for info in additional_information: + cls_name = info.__class__.__name__ + ai_data = json.loads(info.model_dump_json()) + ai_data['uuid'] = f"{uuid4()}" + add_inf[cls_name].append(ai_data) + + + s.additional_information = add_inf s.save() return s - def add_additional_information(self, data): - pass + @transaction.atomic + def add_additional_information(self, data: 'EnviPyModel'): + cls_name = data.__class__.__name__ + ai_data = json.loads(data.model_dump_json()) + ai_data['uuid'] = f"{uuid4()}" - def remove_additional_information(self, data): - pass + if cls_name not in self.additional_information: + self.additional_information[cls_name] = [] - def set_additional_information(self, data): - pass + self.additional_information[cls_name].append(ai_data) + self.save() + + + @transaction.atomic + def remove_additional_information(self, ai_uuid): + found_type = None + found_idx = -1 + + for k, vals in self.additional_information.items(): + for i, v in enumerate(vals): + if v['uuid'] == ai_uuid: + found_type = k + found_idx = i + break + + if found_type is not None and found_idx >= 0: + if len(self.additional_information[found_type]) == 1: + del self.additional_information[k] + else: + self.additional_information[k].pop(found_idx) + self.save() + else: + raise ValueError(f"Could not find additional information with uuid {ai_uuid}") + + @transaction.atomic + def set_additional_information(self, data: Dict[str, 'EnviPyModel']): + new_ais = defaultdict(list) + for k, vals in data.items(): + for v in vals: + ai_data = json.loads(v.model_dump_json()) + if hasattr(v, 'uuid'): + ai_data['uuid'] = str(v.uuid) + else: + ai_data['uuid'] = str(uuid4()) + + new_ais[k].append(ai_data) + + self.additional_information = new_ais + self.save() def get_additional_information(self): from envipy_additional_information import NAME_MAPPING @@ -2437,7 +2500,14 @@ class Scenario(EnviPathModel): continue for v in vals: - yield NAME_MAPPING[k](**json.loads(v)) + # Per default additional fields are ignored + MAPPING = {c.__name__: c for c in NAME_MAPPING.values()} + inst = MAPPING[k](**v) + # Add uuid to uniquely identify objects for manipulation + if 'uuid' in v: + inst.__dict__['uuid'] = v['uuid'] + + yield inst class UserSettingPermission(Permission): diff --git a/epdb/views.py b/epdb/views.py index 72ff30b8..d9c8fed9 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -4,15 +4,14 @@ from typing import List, Dict, Any from django.conf import settings as s from django.contrib.auth import get_user_model -from django.db.models import F, Value -from django.db.models.fields import CharField -from django.db.models.functions import Concat from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest from django.shortcuts import render, redirect from django.views.decorators.csrf import csrf_exempt +from envipy_additional_information import NAME_MAPPING from utilities.chem import FormatConverter, IndigoUtils from utilities.decorators import package_permission_required +from utilities.misc import HTMLGenerator from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \ EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \ @@ -1726,7 +1725,7 @@ def package_scenarios(request, package_uuid): if request.method == 'GET': - if 'application/json' in request.META.get('HTTP_ACCEPT') and not request.GET.get('all', None): + if 'application/json' in request.META.get('HTTP_ACCEPT') and not request.GET.get('all', False): scens = Scenario.objects.filter(package=current_package).order_by('name') res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens] return JsonResponse(res, safe=False) @@ -1757,8 +1756,57 @@ def package_scenarios(request, package_uuid): context['reviewed_objects'] = reviewed_scenario_qs context['unreviewed_objects'] = unreviewed_scenario_qs - return render(request, 'collections/objects_list.html', context) + from envipy_additional_information import SLUDGE_ADDITIONAL_INFORMATION, SOIL_ADDITIONAL_INFORMATION, \ + SEDIMENT_ADDITIONAL_INFORMATION + context['scenario_types'] = { + 'Soil Data': { + 'name': 'soil', + 'widgets': [HTMLGenerator.generate_html(ai, prefix=f'soil_{0}') for ai in + [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]] + }, + 'Sludge Data': { + 'name': 'sludge', + 'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sludge_{0}') for ai in + [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]] + }, + 'Water-Sediment System Data': { + 'name': 'sediment', + 'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sediment_{0}') for ai in + [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]] + } + } + 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/objects_list.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 s in additional_information.values() for x in s] + + s = Scenario.create(current_package, name=scenario_name, description=scenario_description, + scenario_date=scenario_date, scenario_type=scenario_type, + additional_information=additional_information) + + return redirect(s.url) else: return HttpResponseNotAllowed(['GET', ]) @@ -1779,10 +1827,63 @@ 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())] + return render(request, 'objects/scenario.html', context) + elif request.method == 'POST': + + log_post_params(request) + + if hidden := request.POST.get('hidden', None): + + if hidden == 'delete': + current_scenario.delete() + return redirect(current_package.url + '/scenario') + elif hidden == 'delete-additional-information': + uuid = request.POST.get('uuid') + current_scenario.remove_additional_information(uuid) + return redirect(current_scenario.url) + elif hidden == 'delete-all-additional-information': + current_scenario.additional_information = dict() + 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) + 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) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() else: - return HttpResponseNotAllowed(['GET', ]) + return HttpResponseNotAllowed(['GET', 'POST']) ############## diff --git a/pyproject.toml b/pyproject.toml index c1d11d46..0402bbae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,4 +29,4 @@ dependencies = [ [tool.uv.sources] enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } -envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" } +envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"} diff --git a/templates/actions/collections/scenario.html b/templates/actions/collections/scenario.html index f9ccc08e..25b46b7f 100644 --- a/templates/actions/collections/scenario.html +++ b/templates/actions/collections/scenario.html @@ -1,6 +1,6 @@ {% if meta.can_edit %}
| Property | Value | Unit | + {% if meta.can_edit %}Remove | + {% endif %} {% for ai in scenario.get_additional_information %}||
|---|---|---|---|---|---|
| {{ ai.property_name|safe }} | +{{ ai.property_name|safe }} | {{ ai.property_data|safe }} | {{ ai.property_unit|safe }} | -+ {% if meta.can_edit %} + |
+ |
+ {% endif %}
| + | + | Delete all | +
+ |
+
{{obj.title}} |
- |
| {{ ai.name }} | - - {% for c in "1 2 3"|make_list %} -- {% for form in ai.forms %} - - {% if form.type == 'select' %} - - {% else %} - - {% endif %} - {% endfor %} - | - {% endfor %} - {% endif %} - {% endfor %} -
{{subtype.title}} |
- |
| {{ ai.name }} | - - {% for c in "1 2 3"|make_list %} -- {% for form in ai.forms %} - - {% if form.type == 'select' %} - - {% else %} - - {% endif %} - {% endfor %} - | - {% endfor %} - {% endif %} -