[Fix] Remove all Scenarios, catch empty SMILES, prevent default Package delete (#134)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#134
This commit is contained in:
2025-09-30 19:10:57 +13:00
parent b757a07f91
commit 3f5bb76633
9 changed files with 215 additions and 53 deletions

View File

@ -233,6 +233,11 @@ def breadcrumbs(first_level_object=None, second_level_namespace=None, second_lev
def set_scenarios(current_user, attach_object, scenario_urls: List[str]): def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
scens = [] scens = []
for scenario_url in scenario_urls: for scenario_url in scenario_urls:
# As empty lists will be removed in POST request well send ['']
if scenario_url == '':
continue
package = PackageManager.get_package_by_url(current_user, scenario_url) package = PackageManager.get_package_by_url(current_user, scenario_url)
scen = Scenario.objects.get(package=package, uuid=scenario_url.split('/')[-1]) scen = Scenario.objects.get(package=package, uuid=scenario_url.split('/')[-1])
scens.append(scen) scens.append(scen)
@ -743,33 +748,43 @@ def package_model(request, package_uuid, model_uuid):
current_model = EPModel.objects.get(package=current_package, uuid=model_uuid) current_model = EPModel.objects.get(package=current_package, uuid=model_uuid)
if request.method == 'GET': if request.method == 'GET':
classify = request.GET.get('classify', False)
ad_assessment = request.GET.get('app-domain-assessment', False)
if request.GET.get('classify', False): if classify or ad_assessment:
smiles = request.GET['smiles'] smiles = request.GET.get('smiles', '').strip()
stand_smiles = FormatConverter.standardize(smiles)
pred_res = current_model.predict(stand_smiles)
res = []
for pr in pred_res: # Check if smiles is non empty and valid
if len(pr) > 0: if smiles == '':
products = [] return JsonResponse({'error': 'Received empty SMILES'}, status=400)
for prod_set in pr.product_sets:
logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set]))
res.append({ try:
'products': list(set(products)), stand_smiles = FormatConverter.standardize(smiles)
'probability': pr.probability, except ValueError as e:
'btrule': {k: getattr(pr.rule, k) for k in ['url', 'name']} if pr.rule is not None else None return JsonResponse({'error': f'"{smiles}" is not a valid SMILES'}, status=400)
})
return JsonResponse(res, safe=False) if classify:
pred_res = current_model.predict(stand_smiles)
res = []
elif request.GET.get('app-domain-assessment', False): for pr in pred_res:
smiles = request.GET['smiles'] if len(pr) > 0:
stand_smiles = FormatConverter.standardize(smiles) products = []
app_domain_assessment = current_model.app_domain.assess(stand_smiles)[0] for prod_set in pr.product_sets:
return JsonResponse(app_domain_assessment, safe=False) logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set]))
res.append({
'products': list(set(products)),
'probability': pr.probability,
'btrule': {k: getattr(pr.rule, k) for k in ['url', 'name']} if pr.rule is not None else None
})
return JsonResponse(res, safe=False)
else:
app_domain_assessment = current_model.app_domain.assess(stand_smiles)[0]
return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request) context = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - {current_model.name}' context['title'] = f'enviPath - {current_package.name} - {current_model.name}'
@ -860,6 +875,11 @@ def package(request, package_uuid):
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete': if hidden == 'delete':
if current_user.default_package == current_package:
return error(request, f'Package "{current_package.name}" is the default and cannot be deleted!',
'You cannot delete the default package. If you want to delete this package you have to set another default package first.')
logger.debug(current_package.delete()) logger.debug(current_package.delete())
return redirect(s.SERVER_URL + '/package') return redirect(s.SERVER_URL + '/package')
elif hidden == 'publish-package': elif hidden == 'publish-package':
@ -1017,9 +1037,9 @@ def package_compound(request, package_uuid, compound_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_compound, selected_scenarios) set_scenarios(current_user, current_compound, selected_scenarios)
return redirect(current_compound.url) return redirect(current_compound.url)
@ -1126,9 +1146,9 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_structure, selected_scenarios) set_scenarios(current_user, current_structure, selected_scenarios)
return redirect(current_structure.url) return redirect(current_structure.url)
@ -1253,9 +1273,9 @@ def package_rule(request, package_uuid, rule_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_rule, selected_scenarios) set_scenarios(current_user, current_rule, selected_scenarios)
return redirect(current_rule.url) return redirect(current_rule.url)
@ -1356,9 +1376,9 @@ def package_reaction(request, package_uuid, reaction_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_reaction, selected_scenarios) set_scenarios(current_user, current_reaction, selected_scenarios)
return redirect(current_reaction.url) return redirect(current_reaction.url)
@ -1421,10 +1441,10 @@ def package_pathways(request, package_uuid):
name = request.POST.get('name') name = request.POST.get('name')
description = request.POST.get('description') description = request.POST.get('description')
pw_mode = request.POST.get('predict', 'predict') pw_mode = request.POST.get('predict', 'predict').strip()
smiles = request.POST.get('smiles') smiles = request.POST.get('smiles', '').strip()
if smiles is None or smiles.strip() == '': if 'smiles' in request.POST and smiles == '':
return error(request, "Pathway prediction failed!", return error(request, "Pathway prediction failed!",
"Pathway prediction failed due to missing or empty SMILES") "Pathway prediction failed due to missing or empty SMILES")
@ -1543,9 +1563,9 @@ def package_pathway(request, package_uuid, pathway_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_pathway, selected_scenarios) set_scenarios(current_user, current_pathway, selected_scenarios)
return redirect(current_pathway.url) return redirect(current_pathway.url)
@ -1689,9 +1709,9 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_node, selected_scenarios) set_scenarios(current_user, current_node, selected_scenarios)
return redirect(current_node.url) return redirect(current_node.url)
@ -1798,9 +1818,9 @@ def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
current_edge.delete() current_edge.delete()
return redirect(current_pathway.url) return redirect(current_pathway.url)
selected_scenarios = request.POST.getlist('selected-scenarios') if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios is not None:
set_scenarios(current_user, current_edge, selected_scenarios) set_scenarios(current_user, current_edge, selected_scenarios)
return redirect(current_edge.url) return redirect(current_edge.url)

View File

@ -6,8 +6,7 @@
<h4 class="alert-heading">{{ error_message }}</h4> <h4 class="alert-heading">{{ error_message }}</h4>
<hr> <hr>
<p class="mb-0"> <p class="mb-0">
{{ error_detail }}<br> {{ error_detail }}
The error was logged and will be investigated.
</p> </p>
</div> </div>

View File

@ -89,7 +89,7 @@
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse"> <div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse">
<ul class="nav navbar-nav navbar-nav-framework"> <ul class="nav navbar-nav navbar-nav-framework">
<li> <li>
<a class="button" data-toggle="modal" data-target="#predict_modal"> <a href="#" data-toggle="modal" data-target="#predict_modal">
Predict Pathway Predict Pathway
</a> </a>
</li> </li>

View File

@ -103,6 +103,7 @@
var textSmiles = $('#index-form-text-input').val().trim(); var textSmiles = $('#index-form-text-input').val().trim();
if (textSmiles === '') { if (textSmiles === '') {
$(this).prop("disabled", false);
return; return;
} }

View File

@ -64,6 +64,9 @@
$('#set_scenario_modal_form_submit').on('click', function (e) { $('#set_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault(); e.preventDefault();
if ($('##scenario-select').val().length == 0) {
$('##scenario-select').val([''])
}
$('#set_scenario_modal_form').submit(); $('#set_scenario_modal_form').submit();
}); });
}); });

View File

@ -315,12 +315,18 @@
$("#predict-button").on("click", function (e) { $("#predict-button").on("click", function (e) {
e.preventDefault(); e.preventDefault();
clear("predictResultTable");
data = { data = {
"smiles": $("#smiles-to-predict").val(), "smiles": $("#smiles-to-predict").val(),
"classify": "ILikeCats!" "classify": "ILikeCats!"
} }
clear("predictResultTable"); if (data["smiles"].trim() === "") {
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}"); makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({ $.ajax({
@ -332,17 +338,17 @@
$("#predictLoading").empty(); $("#predictLoading").empty();
handlePredictionResponse(data); handlePredictionResponse(data);
} catch (error) { } catch (error) {
console.log("Error");
$("#predictLoading").empty(); $("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger"); $("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/"); $("#predictResultTable").append("Error while processing response :/");
} }
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty(); $("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger"); $("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/"); $("#predictResultTable").append(jqXHR.responseJSON.error);
} },
}); });
}); });
} }
@ -351,12 +357,20 @@
$("#assess-button").on("click", function (e) { $("#assess-button").on("click", function (e) {
e.preventDefault(); e.preventDefault();
clear("appDomainAssessmentResultTable");
data = { data = {
"smiles": $("#smiles-to-assess").val(), "smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!" "app-domain-assessment": "ILikeCats!"
} }
clear("appDomainAssessmentResultTable"); if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}"); makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({ $.ajax({
@ -369,16 +383,15 @@
handleAssessmentResponse("{% url 'depict' %}", data); handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data); console.log(data);
} catch (error) { } catch (error) {
console.log("Error");
$("#appDomainLoading").empty(); $("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger"); $("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/"); $("#appDomainAssessmentResultTable").append("Error while processing response :/");
} }
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty(); $("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger"); $("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/"); $("#appDomainAssessmentResultTable").append(jqXHR.responseJSON.error);
} }
}); });
}); });

View File

@ -0,0 +1,113 @@
from django.test import TestCase, override_settings
from django.urls import reverse
from django.conf import settings as s
from epdb.logic import UserManager, PackageManager
from epdb.models import Pathway, Edge, Package, User
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models")
class PathwayViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(PathwayViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user("user1", "user1@envipath.com", "SuperSafe",
set_setting=True, add_to_group=True, is_active=True)
cls.user1_default_package = cls.user1.default_package
cls.model_package = Package.objects.get(name='Fixtures')
def setUp(self):
self.client.force_login(self.user1)
def test_predict(self):
self.client.force_login(User.objects.get(username="admin"))
response = self.client.get(
reverse("package model detail", kwargs={
'package_uuid': str(self.model_package.uuid),
'model_uuid': str(self.model_package.models.first().uuid)
}), {
'classify': 'ILikeCats!',
'smiles': 'CCN(CC)C(=O)C1=CC(=CC=C1)CO',
}
)
expected = [
{
'products': [
[
'O=C(O)C1=CC(CO)=CC=C1',
'CCNCC'
]
],
'probability': 0.25,
'btrule': {
'url': 'http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/0e6e9290-b658-4450-b291-3ec19fa19206',
'name': 'bt0430-4011'
}
}, {
'products': [
[
'CCNC(=O)C1=CC(CO)=CC=C1',
'CC=O'
]
], 'probability': 0.0,
'btrule': {
'url': 'http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/27a3a353-0b66-4228-bd16-e407949e90df',
'name': 'bt0243-4301'
}
}, {
'products': [
[
'CCN(CC)C(=O)C1=CC(C=O)=CC=C1'
]
], 'probability': 0.75,
'btrule': {
'url': 'http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022',
'name': 'bt0001-3568'
}
}
]
actual = response.json()
self.assertEqual(actual, expected)
response = self.client.get(
reverse("package model detail", kwargs={
'package_uuid': str(self.model_package.uuid),
'model_uuid': str(self.model_package.models.first().uuid)
}), {
'classify': 'ILikeCats!',
'smiles': '',
}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['error'], 'Received empty SMILES')
response = self.client.get(
reverse("package model detail", kwargs={
'package_uuid': str(self.model_package.uuid),
'model_uuid': str(self.model_package.models.first().uuid)
}), {
'classify': 'ILikeCats!',
'smiles': ' ', # Input should be stripped
}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['error'], 'Received empty SMILES')
response = self.client.get(
reverse("package model detail", kwargs={
'package_uuid': str(self.model_package.uuid),
'model_uuid': str(self.model_package.models.first().uuid)
}), {
'classify': 'ILikeCats!',
'smiles': 'RandomInput',
}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['error'], '"RandomInput" is not a valid SMILES')

View File

@ -178,3 +178,15 @@ class PackageViewTest(TestCase):
response = self.client.get(package_url) response = self.client.get(package_url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_delete_default_package(self):
self.client.force_login(self.user1)
# Try to delete the default package
response = self.client.post(self.user1.default_package.url, {
"hidden": "delete"
})
self.assertEqual(response.status_code, 400)
self.assertTrue(f'You cannot delete the default package. '
f'If you want to delete this package you have to '
f'set another default package first' in response.content.decode())

View File

@ -161,7 +161,8 @@ class RuleViewTest(TestCase):
'package_uuid': str(r.package.uuid), 'package_uuid': str(r.package.uuid),
'rule_uuid': str(r.uuid) 'rule_uuid': str(r.uuid)
}), { }), {
"selected-scenarios": [] # We have to set an empty string to avoid that the parameter is removed
"selected-scenarios": ""
} }
) )