forked from enviPath/enviPy
[Feature] Scenario Creation (#78)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#78
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from typing import Union, List, Optional, Set, Dict, Any
|
from typing import Union, List, Optional, Set, Dict, Any
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -552,11 +553,13 @@ class PackageManager(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
res = AdditionalInformationConverter.convert(name, addinf_data)
|
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:
|
except:
|
||||||
logger.error(f"Failed to convert {name} with {addinf_data}")
|
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.additional_information = new_add_inf
|
||||||
scen.save()
|
scen.save()
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from django.db import models, transaction
|
|||||||
from django.db.models import JSONField, Count, Q, QuerySet
|
from django.db.models import JSONField, Count, Q, QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from envipy_additional_information import EnviPyModel
|
||||||
from model_utils.models import TimeStampedModel
|
from model_utils.models import TimeStampedModel
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
from sklearn.metrics import precision_score, recall_score, jaccard_score
|
from sklearn.metrics import precision_score, recall_score, jaccard_score
|
||||||
@ -2292,7 +2293,8 @@ class ApplicabilityDomain(EnviPathModel):
|
|||||||
transformation = {
|
transformation = {
|
||||||
'rule': rule_data,
|
'rule': rule_data,
|
||||||
'reliability': rule_reliabilities[rule_idx],
|
'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,
|
'is_predicted': False,
|
||||||
'local_compatibility': local_compatibilities[rule_idx],
|
'local_compatibility': local_compatibilities[rule_idx],
|
||||||
'probability': preds[rule_idx].probability,
|
'probability': preds[rule_idx].probability,
|
||||||
@ -2407,27 +2409,88 @@ class Scenario(EnviPathModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@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 = Scenario()
|
||||||
s.package = package
|
s.package = package
|
||||||
|
|
||||||
|
if name is None or name.strip() == '':
|
||||||
|
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
s.name = name
|
s.name = name
|
||||||
|
|
||||||
|
if description is not None and description.strip() != '':
|
||||||
s.description = description
|
s.description = description
|
||||||
s.date = date
|
|
||||||
s.type = type
|
if scenario_date is not None and scenario_date.strip() != '':
|
||||||
s.additional_information = additional_information
|
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()
|
s.save()
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def add_additional_information(self, data):
|
@transaction.atomic
|
||||||
pass
|
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):
|
if cls_name not in self.additional_information:
|
||||||
pass
|
self.additional_information[cls_name] = []
|
||||||
|
|
||||||
def set_additional_information(self, data):
|
self.additional_information[cls_name].append(ai_data)
|
||||||
pass
|
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):
|
def get_additional_information(self):
|
||||||
from envipy_additional_information import NAME_MAPPING
|
from envipy_additional_information import NAME_MAPPING
|
||||||
@ -2437,7 +2500,14 @@ class Scenario(EnviPathModel):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for v in vals:
|
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):
|
class UserSettingPermission(Permission):
|
||||||
|
|||||||
113
epdb/views.py
113
epdb/views.py
@ -4,15 +4,14 @@ from typing import List, Dict, Any
|
|||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
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.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from envipy_additional_information import NAME_MAPPING
|
||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
|
from utilities.misc import HTMLGenerator
|
||||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
|
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
|
||||||
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
|
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
|
||||||
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
|
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
|
||||||
@ -1726,7 +1725,7 @@ def package_scenarios(request, package_uuid):
|
|||||||
|
|
||||||
if request.method == 'GET':
|
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')
|
scens = Scenario.objects.filter(package=current_package).order_by('name')
|
||||||
res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens]
|
res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens]
|
||||||
return JsonResponse(res, safe=False)
|
return JsonResponse(res, safe=False)
|
||||||
@ -1757,8 +1756,57 @@ def package_scenarios(request, package_uuid):
|
|||||||
context['reviewed_objects'] = reviewed_scenario_qs
|
context['reviewed_objects'] = reviewed_scenario_qs
|
||||||
context['unreviewed_objects'] = unreviewed_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:
|
else:
|
||||||
return HttpResponseNotAllowed(['GET', ])
|
return HttpResponseNotAllowed(['GET', ])
|
||||||
|
|
||||||
@ -1779,10 +1827,63 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
|||||||
|
|
||||||
context['scenario'] = current_scenario
|
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)
|
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:
|
else:
|
||||||
return HttpResponseNotAllowed(['GET', ])
|
return HttpResponseBadRequest()
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
else:
|
||||||
|
return HttpResponseNotAllowed(['GET', 'POST'])
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
|
|||||||
@ -29,4 +29,4 @@ dependencies = [
|
|||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
|
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-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"}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a role="button" data-toggle="modal" data-target="#new_pathway_modal">
|
<a role="button" data-toggle="modal" data-target="#new_scenario_modal">
|
||||||
<span class="glyphicon glyphicon-plus"></span> New Scenario</a>
|
<span class="glyphicon glyphicon-plus"></span> New Scenario</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,2 +1,14 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a class="button" data-toggle="modal" data-target="#add_additional_information_modal">
|
||||||
|
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="button" data-toggle="modal" data-target="#update_scenario_additional_information_modal">
|
||||||
|
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
|
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,115 +1,95 @@
|
|||||||
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="newScenGenMod"
|
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="new_scenario_modal"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal">
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
<span aria-hidden="true">×</span> <span
|
<span aria-hidden="true">×</span>
|
||||||
class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="js-title-step"></h4>
|
<h4 class="modal-title">New Scenario</h4>
|
||||||
</div>
|
</div>
|
||||||
<form id="base-scenario-form" accept-charset="UTF-8" action="" data-remote="true" method="POST">
|
<div class="modal-body">
|
||||||
<div class="modal-body hide" data-step="1" data-title="New Scenario - Step 1">
|
<form id="new_scenario_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/scenario"
|
||||||
<div class="jumbotron">Please enter name, description,
|
data-remote="true" method="post">
|
||||||
and date of scenario. Date should be associated to the
|
{% csrf_token %}
|
||||||
data, not the current date. For example, this could
|
<div class="jumbotron">Please enter name, description, and date of scenario. Date should be
|
||||||
reflect the publishing date of a study. You can leave
|
associated to the data, not the current date. For example, this could reflect the publishing
|
||||||
all fields but the name empty and fill them in
|
date of a study. You can leave all fields but the name empty and fill them in later.
|
||||||
later.
|
<a target="_blank" href="https://wiki.envipath.org/index.php/scenario" role="button">wiki
|
||||||
|
>></a>
|
||||||
</div>
|
</div>
|
||||||
<label for="name">Name</label>
|
<label for="scenario-name">Name</label>
|
||||||
<input id="name" name="studyname" placeholder="Name" class="form-control"/>
|
<input id="scenario-name" name="scenario-name" class="form-control" placeholder="Name"/>
|
||||||
<label for="name">Description</label>
|
<label for="scenario-description">Description</label>
|
||||||
<input id="description" name="studydescription" placeholder="Description" class="form-control"/>
|
<input id="scenario-description" name="scenario-description" class="form-control"
|
||||||
|
placeholder="Description"/>
|
||||||
<label id="dateField" for="dateYear">Date</label>
|
<label id="dateField" for="dateYear">Date</label>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<input type="number" id="dateYear" name="dateYear" class="form-control" placeholder="YYYY">
|
<input type="number" id="dateYear" name="scenario-date-year" class="form-control"
|
||||||
|
placeholder="YYYY">
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<input type="number" id="dateMonth" name="dateMonth" min="1" max="12"
|
<input type="number" id="dateMonth" name="scenario-date-month" min="1" max="12"
|
||||||
class="form-control" placeholder="MM" align="">
|
class="form-control" placeholder="MM" align="">
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<input type="number" id="dateDay" name="dateDay" min="1" max="31" class="form-control"
|
<input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control"
|
||||||
placeholder="DD">
|
placeholder="DD">
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<label for="scenario-type">Scenario Type</label>
|
||||||
|
<select id="scenario-type" name="scenario-type" class="form-control" data-width='100%'>
|
||||||
|
<option value="empty" selected>Empty Scenario</option>
|
||||||
|
{% for k, v in scenario_types.items %}
|
||||||
|
<option value="{{ v.name }}">{{ k }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{% for type in scenario_types.values %}
|
||||||
|
<div id="{{ type.name }}-specific-inputs">
|
||||||
|
{% for widget in type.widgets %}
|
||||||
|
{{ widget|safe }}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2">
|
{% endfor %}
|
||||||
<div class="jumbotron">
|
|
||||||
Do you want to create an empty scenario and fill it
|
|
||||||
with your own set of attributes, or fill in a
|
|
||||||
pre-defined set of attributes for soil, sludge or sediment
|
|
||||||
data?
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" id="radioSludge" value="sludge">Sludge Data
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" id="radioSediment" value="sediment">Water-Sediment System Data
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body hide" data-step="3" data-title="New Scenario - Step 3">
|
|
||||||
<div class="jumbotron" id="finaljumbo">
|
|
||||||
All done! Click Submit!
|
|
||||||
</div>
|
|
||||||
<div style="float: left"><button
|
|
||||||
id="addColumnButton" type="button"
|
|
||||||
class="btn btn-default">Add
|
|
||||||
another Scenario
|
|
||||||
</button></div>
|
|
||||||
<input type="hidden" name="fullScenario" value="true"/>
|
|
||||||
{% include "tables/scenario.html" %}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button>
|
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
|
||||||
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-default js-btn-step"
|
|
||||||
data-orientation="next" id="nextbutton"></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
<p></p>
|
// Initially hide all "specific" forms
|
||||||
<div id="scenariocontent"></div>
|
$("div[id$='-specific-inputs']").each(function () {
|
||||||
|
$(this).hide();
|
||||||
<!--Template index -->
|
|
||||||
<script language="javascript">
|
|
||||||
$(function() {
|
|
||||||
|
|
||||||
// Hide additional columns per default
|
|
||||||
$('.col-2').hide();
|
|
||||||
$('.col-3').hide();
|
|
||||||
|
|
||||||
//TODO just to see modal
|
|
||||||
$('#new_scenario_modal').modalSteps({
|
|
||||||
btnCancelHtml: 'Cancel',
|
|
||||||
btnPreviousHtml: 'Previous',
|
|
||||||
btnNextHtml: 'Next',
|
|
||||||
btnLastStepHtml: 'Submit',
|
|
||||||
disableNextButton: false,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// On change hide all and show only selected
|
||||||
|
$("#scenario-type").change(function () {
|
||||||
|
$("div[id$='-specific-inputs']").each(function () {
|
||||||
|
$(this).hide();
|
||||||
|
});
|
||||||
|
val = $('option:selected', this).val();
|
||||||
|
$("#" + val + "-specific-inputs").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#new_scenario_modal_form_submit').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#new_scenario_form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!-- Add Additional Information-->
|
||||||
|
<div id="add_additional_information_modal" class="modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h3 class="modal-title">Add Additional Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<select id="select-additional-information-type" data-actions-box='true' class="form-control" data-width='100%'>
|
||||||
|
<option selected disabled>Select the type to add</option>
|
||||||
|
{% for add_inf in available_additional_information %}
|
||||||
|
<option value="{{ add_inf.name }}">{{ add_inf.display_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% for add_inf in available_additional_information %}
|
||||||
|
<div class="aiform {{ add_inf.name }}" style="display: none;">
|
||||||
|
<form id="add_{{ add_inf.name }}_add-additional-information-modal-form" accept-charset="UTF-8"
|
||||||
|
action="" data-remote="true" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ add_inf.widget|safe }}
|
||||||
|
<input type="hidden" name="hidden" value="add-additional-information">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="add-additional-information-modal-submit">Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('#select-additional-information-type').change(function(e){
|
||||||
|
var selectedType = $("#select-additional-information-type :selected").val();
|
||||||
|
$('.aiform').hide();
|
||||||
|
$('.' + selectedType).show();
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#add-additional-information-modal-submit').click(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var selectedType = $("#select-additional-information-type :selected").val();
|
||||||
|
console.log(selectedType);
|
||||||
|
if (selectedType !== null && selectedType !== undefined && selectedType !== '') {
|
||||||
|
$('.' + selectedType + ' >form').submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!-- Update Scenario Additional Information-->
|
||||||
|
<div id="update_scenario_additional_information_modal" class="modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h3 class="modal-title">Update Additional Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="edit-scenario-additional-information-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for widget in update_widgets %}
|
||||||
|
{{ widget|safe }}
|
||||||
|
{% endfor %}
|
||||||
|
<input type="hidden" name="hidden" value="set-additional-information">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="edit-scenario-additional-information-modal-submit">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('#edit-scenario-additional-information-modal-submit').click(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$('#edit-scenario-additional-information-modal-form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -3,6 +3,8 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
|
{% include "modals/objects/add_additional_information_modal.html" %}
|
||||||
|
{% include "modals/objects/update_scenario_additional_information_modal.html" %}
|
||||||
{% include "modals/objects/generic_delete_modal.html" %}
|
{% include "modals/objects/generic_delete_modal.html" %}
|
||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
<div class="panel-group" id="scenario-detail">
|
<div class="panel-group" id="scenario-detail">
|
||||||
@ -24,6 +26,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Description</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ scenario.description }}
|
||||||
|
<br>
|
||||||
|
{{ scenario.scenario_type }}
|
||||||
|
<br>
|
||||||
|
Reported {{ scenario.scenario_date }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="scenario-table" class="table table-bordered table-striped table-hover">
|
<table id="scenario-table" class="table table-bordered table-striped table-hover">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -31,18 +45,42 @@
|
|||||||
<th>Property</th>
|
<th>Property</th>
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
<th>Unit</th>
|
<th>Unit</th>
|
||||||
|
{% if meta.can_edit %}
|
||||||
<th>Remove</th>
|
<th>Remove</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% for ai in scenario.get_additional_information %}
|
{% for ai in scenario.get_additional_information %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ai.property_name|safe }} </td>
|
<td> {{ ai.property_name|safe }} </td>
|
||||||
<td> {{ ai.property_data|safe }} </td>
|
<td> {{ ai.property_data|safe }} </td>
|
||||||
<td> {{ ai.property_unit|safe }} </td>
|
<td> {{ ai.property_unit|safe }} </td>
|
||||||
<td></td>
|
{% if meta.can_edit %}
|
||||||
|
<td>
|
||||||
|
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="uuid" value="{{ ai.uuid }}">
|
||||||
|
<input type="hidden" name="hidden" value="delete-additional-information">
|
||||||
|
<button type="submit" class="btn"><span class="glyphicon glyphicon-minus"></span></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if meta.can_edit %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>Delete all</td>
|
||||||
|
<td>
|
||||||
|
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="hidden" value="delete-all-additional-information">
|
||||||
|
<button type="submit" class="btn"><span class="glyphicon glyphicon-trash"></span></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
{% for obj in available_additional_information.types %}
|
|
||||||
<div id="table-{{obj.type}}-Div">
|
|
||||||
<table class="table table-bordered table-hover">
|
|
||||||
<tbody>
|
|
||||||
<tr id="{{ obj.type }}GroupRow" style="background-color: rgba(0, 0, 0, 0.08);">
|
|
||||||
<td><p style="font-size:18px">{{obj.title}}</p></td>
|
|
||||||
</tr>
|
|
||||||
<!-- Loop through all AIs and attach the ones without subtype -->
|
|
||||||
{% for ai in available_additional_information.ais %}
|
|
||||||
<tr>
|
|
||||||
{% if obj.type in ai.types and ai.sub_type is not defined %}
|
|
||||||
<td><span title="">{{ ai.name }}</span></td>
|
|
||||||
<!-- #TODO -->
|
|
||||||
{% for c in "1 2 3"|make_list %}
|
|
||||||
<td class="col-{{ c }}">
|
|
||||||
{% for form in ai.forms %}
|
|
||||||
<!-- Build input -->
|
|
||||||
{% if form.type == 'select' %}
|
|
||||||
<select class="form-control" name="{{ form.param_name}}">
|
|
||||||
<option value="">{{ form.placeholder }}</option>
|
|
||||||
{% for choice in form.choices %}
|
|
||||||
<option value="{{ choice.value }}">
|
|
||||||
{{ choice.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input type="{{ form.type }}" name="{{ form.param_name }}" class="form-control" placeholder="{{ form.placeholder|safe }}"/>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% for subtype in available_additional_information.subtypes %}
|
|
||||||
<tr id="{{ subtype.type }}GroupRow" style="background-color: rgba(0, 0, 0, 0.08);">
|
|
||||||
<td><p style="font-size:18px">{{subtype.title}}</p></td>
|
|
||||||
</tr>
|
|
||||||
<!-- Loop through all AIs and attach the ones with the same subtype -->
|
|
||||||
{% for ai in available_additional_information.ais %}
|
|
||||||
<tr>
|
|
||||||
{% if obj.type in ai.types and subtype.type == ai.sub_type %}
|
|
||||||
<td><span title="">{{ ai.name }}</span></td>
|
|
||||||
<!-- #TODO -->
|
|
||||||
{% for c in "1 2 3"|make_list %}
|
|
||||||
<td class="col-{{ c }}">
|
|
||||||
{% for form in ai.forms %}
|
|
||||||
<!-- Build input -->
|
|
||||||
{% if form.type == 'select' %}
|
|
||||||
<select class="form-control" name="{{ form.param_name }}">
|
|
||||||
<option value="">{{ form.placeholder }}</option>
|
|
||||||
{% for choice in form.choices %}
|
|
||||||
<option value="{{ choice.value }}">
|
|
||||||
{{ choice.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input type="{{ form.type }}" name="{{ form.param_name }}" class="form-control" placeholder="{{ form.placeholder|safe }}">
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
168
utilities/misc.py
Normal file
168
utilities/misc.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import html
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from enum import Enum
|
||||||
|
from types import NoneType
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
|
from envipy_additional_information import Interval, EnviPyModel
|
||||||
|
from envipy_additional_information import NAME_MAPPING
|
||||||
|
from pydantic import BaseModel, HttpUrl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLGenerator:
|
||||||
|
registry = {x.__name__: x for x in NAME_MAPPING.values()}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_html(additional_information: 'EnviPyModel', prefix='') -> str:
|
||||||
|
from typing import get_origin, get_args, Union
|
||||||
|
|
||||||
|
if isinstance(additional_information, type):
|
||||||
|
clz_name = additional_information.__name__
|
||||||
|
else:
|
||||||
|
clz_name = additional_information.__class__.__name__
|
||||||
|
|
||||||
|
widget = f'<h4>{clz_name}</h4>'
|
||||||
|
|
||||||
|
if hasattr(additional_information, 'uuid'):
|
||||||
|
uuid = additional_information.uuid
|
||||||
|
widget += f'<input type="hidden" name="{clz_name}__{prefix}__uuid" value="{uuid}">'
|
||||||
|
|
||||||
|
for name, field in additional_information.model_fields.items():
|
||||||
|
value = getattr(additional_information, name, None)
|
||||||
|
full_name = f"{clz_name}__{prefix}__{name}"
|
||||||
|
annotation = field.annotation
|
||||||
|
base_type = get_origin(annotation) or annotation
|
||||||
|
|
||||||
|
# Optional[Interval[float]] alias for Union[X, None]
|
||||||
|
if base_type is Union:
|
||||||
|
for arg in get_args(annotation):
|
||||||
|
if arg is not NoneType:
|
||||||
|
field_type = arg
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
field_type = base_type
|
||||||
|
|
||||||
|
is_interval_float = (
|
||||||
|
field_type == Interval[float] or
|
||||||
|
str(field_type) == str(Interval[float]) or
|
||||||
|
'Interval[float]' in str(field_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_interval_float:
|
||||||
|
widget += f"""
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{full_name}__start">{' '.join([x.capitalize() for x in name.split('_')])} Start</label>
|
||||||
|
<input type="number" class="form-control" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ''}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{full_name}__end">{' '.join([x.capitalize() for x in name.split('_')])} End</label>
|
||||||
|
<input type="number" class="form-control" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
elif issubclass(field_type, Enum):
|
||||||
|
options: str = ''
|
||||||
|
for e in field_type:
|
||||||
|
options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>'
|
||||||
|
|
||||||
|
widget += f"""
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{full_name}">{' '.join([x.capitalize() for x in name.split('_')])}</label>
|
||||||
|
<select class="form-control" id="{full_name}" name="{full_name}">
|
||||||
|
<option value="" disabled selected>Select {' '.join([x.capitalize() for x in name.split('_')])}</option>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
if field_type == str or field_type == HttpUrl:
|
||||||
|
input_type = 'text'
|
||||||
|
elif field_type == float or field_type == int:
|
||||||
|
input_type = 'number'
|
||||||
|
elif field_type == bool:
|
||||||
|
input_type = 'checkbox'
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Could not parse field type {field_type} for {name}")
|
||||||
|
|
||||||
|
value_to_use = value if value and field_type != bool else ''
|
||||||
|
|
||||||
|
widget += f"""
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{full_name}">{' '.join([x.capitalize() for x in name.split('_')])}</label>
|
||||||
|
<input type="{input_type}" class="form-control" id="{full_name}" name="{full_name}" value="{value_to_use}" {"checked" if value and field_type == bool else ""}>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return widget + "<hr>"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_models(params) -> Dict[str, List['EnviPyModel']]:
|
||||||
|
|
||||||
|
def has_non_none(d):
|
||||||
|
"""
|
||||||
|
Recursively checks if any value in a (possibly nested) dict is not None.
|
||||||
|
"""
|
||||||
|
for value in d.values():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if has_non_none(value): # recursive check
|
||||||
|
return True
|
||||||
|
elif value is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Build Pydantic model instances from flattened HTML parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: dict of {param_name: value}, e.g. form data
|
||||||
|
model_registry: mapping of class names (strings) to Pydantic model classes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {ClassName: [list of model instances]}
|
||||||
|
"""
|
||||||
|
grouped: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
||||||
|
|
||||||
|
# Step 1: group fields by ClassName and Number
|
||||||
|
for key, value in params.items():
|
||||||
|
if value == '':
|
||||||
|
value = None
|
||||||
|
|
||||||
|
parts = key.split("__")
|
||||||
|
if len(parts) < 3:
|
||||||
|
continue # skip invalid keys
|
||||||
|
|
||||||
|
class_name, number, *field_parts = parts
|
||||||
|
grouped.setdefault(class_name, {}).setdefault(number, {})
|
||||||
|
|
||||||
|
# handle nested fields like interval__start
|
||||||
|
target = grouped[class_name][number]
|
||||||
|
current = target
|
||||||
|
for p in field_parts[:-1]:
|
||||||
|
current = current.setdefault(p, {})
|
||||||
|
current[field_parts[-1]] = value
|
||||||
|
|
||||||
|
# Step 2: instantiate Pydantic models
|
||||||
|
instances: Dict[str, List[BaseModel]] = defaultdict(list)
|
||||||
|
for class_name, number_dict in grouped.items():
|
||||||
|
model_cls = HTMLGenerator.registry.get(class_name)
|
||||||
|
|
||||||
|
if not model_cls:
|
||||||
|
logger.info(f"Could not find model class for {class_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for number, fields in number_dict.items():
|
||||||
|
if not has_non_none(fields):
|
||||||
|
print(f"Skipping empty {class_name} {number} {fields}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
uuid = fields.pop('uuid', None)
|
||||||
|
instance = model_cls(**fields)
|
||||||
|
if uuid:
|
||||||
|
instance.__dict__['uuid'] = uuid
|
||||||
|
instances[class_name].append(instance)
|
||||||
|
|
||||||
|
return instances
|
||||||
4
uv.lock
generated
4
uv.lock
generated
@ -426,7 +426,7 @@ requires-dist = [
|
|||||||
{ name = "django-ninja", specifier = ">=1.4.1" },
|
{ name = "django-ninja", specifier = ">=1.4.1" },
|
||||||
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
||||||
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.0" },
|
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.0" },
|
||||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" },
|
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4" },
|
||||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
@ -443,7 +443,7 @@ requires-dist = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git#4804b24b3479bed6108a49e4401bff8947c03cbd" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4#4da604090bf7cf1f3f552d69485472dbc623030a" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user