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 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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
if description is not None and description.strip() != '':
|
||||
s.description = description
|
||||
s.date = date
|
||||
s.type = type
|
||||
s.additional_information = additional_information
|
||||
|
||||
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):
|
||||
|
||||
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.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 HttpResponseNotAllowed(['GET', ])
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', 'POST'])
|
||||
|
||||
|
||||
##############
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% if meta.can_edit %}
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -1,2 +1,14 @@
|
||||
{% 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 %}
|
||||
@ -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">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span> <span
|
||||
class="sr-only">Close</span>
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="js-title-step"></h4>
|
||||
<h4 class="modal-title">New Scenario</h4>
|
||||
</div>
|
||||
<form id="base-scenario-form" accept-charset="UTF-8" action="" data-remote="true" method="POST">
|
||||
<div class="modal-body hide" data-step="1" data-title="New Scenario - Step 1">
|
||||
<div class="jumbotron">Please enter name, description,
|
||||
and date of scenario. Date should be associated to the
|
||||
data, not the current date. For example, this could
|
||||
reflect the publishing date of a study. You can leave
|
||||
all fields but the name empty and fill them in
|
||||
later.
|
||||
<div class="modal-body">
|
||||
<form id="new_scenario_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/scenario"
|
||||
data-remote="true" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="jumbotron">Please enter name, description, and date of scenario. Date should be
|
||||
associated to the data, not the current date. For example, this could reflect the publishing
|
||||
date of a study. You can leave all fields but the name empty and fill them in later.
|
||||
<a target="_blank" href="https://wiki.envipath.org/index.php/scenario" role="button">wiki
|
||||
>></a>
|
||||
</div>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="studyname" placeholder="Name" class="form-control"/>
|
||||
<label for="name">Description</label>
|
||||
<input id="description" name="studydescription" placeholder="Description" class="form-control"/>
|
||||
<label for="scenario-name">Name</label>
|
||||
<input id="scenario-name" name="scenario-name" class="form-control" placeholder="Name"/>
|
||||
<label for="scenario-description">Description</label>
|
||||
<input id="scenario-description" name="scenario-description" class="form-control"
|
||||
placeholder="Description"/>
|
||||
<label id="dateField" for="dateYear">Date</label>
|
||||
<table>
|
||||
<tr>
|
||||
<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>
|
||||
<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="">
|
||||
</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">
|
||||
</th>
|
||||
</tr>
|
||||
</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 class="modal-body hide" data-step="2" data-title="New Scenario - Step 2">
|
||||
<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>
|
||||
{% endfor %}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step"
|
||||
data-orientation="next" id="nextbutton"></button>
|
||||
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p></p>
|
||||
<div id="scenariocontent"></div>
|
||||
|
||||
<!--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,
|
||||
<script>
|
||||
$(function () {
|
||||
// Initially hide all "specific" forms
|
||||
$("div[id$='-specific-inputs']").each(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
|
||||
@ -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 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" %}
|
||||
{% endblock action_modals %}
|
||||
<div class="panel-group" id="scenario-detail">
|
||||
@ -24,6 +26,18 @@
|
||||
</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">
|
||||
<table id="scenario-table" class="table table-bordered table-striped table-hover">
|
||||
<tbody>
|
||||
@ -31,18 +45,42 @@
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
{% if meta.can_edit %}
|
||||
<th>Remove</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
||||
{% for ai in scenario.get_additional_information %}
|
||||
<tr>
|
||||
<td>{{ ai.property_name|safe }} </td>
|
||||
<td> {{ ai.property_name|safe }} </td>
|
||||
<td> {{ ai.property_data|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>
|
||||
{% 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>
|
||||
</table>
|
||||
</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-polymorphic", specifier = ">=4.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 = "epam-indigo", specifier = ">=1.30.1" },
|
||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||
@ -443,7 +443,7 @@ requires-dist = [
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
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 = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user