[Fix] Stereochemistry prediction handling (#228 and #238) (#250)

**This pull request will need a separate migration pull-request**

I have added an alert box in two places when the user tries to predict with stereo chemistry.

When a user predicts a pathway with stereo chemistry an alert box is shown in that node's hover.
To do this I added two new fields. Pathway now has a "predicted" BooleanField indicating whether it was predicted or not. It is set to True if the pathway mode for prediction is "predict" or "incremental" and False if it is "build". I think it is a flag that could be useful in the future, perhaps for analysing how many predicted pathways are in enviPath?
Node now has a `stereo_removed` BooleanField which is set to True if the Node's parent Pathways has "predicted" as true and the node SMILES has stereochemistry.
<img width="500" alt="{927AC9FF-DBC9-4A19-9E6E-0EDD3B08C7AC}.png" src="attachments/69ea29bc-c2d2-4cd2-8e98-aae5c5737f69">

When a user does a prediction on a model's page it shows at the top of the list. This did not require any new fields as the entered SMILES does not get saved anywhere.
<img width="500" alt="{BED66F12-5F07-419E-AAA6-FE1FE5B4F266}.png" src="attachments/5fcc3a9b-4d1a-4e48-acac-76b7571f6507">

I think the alert box is an alright solution but if you have a great idea for something that looks/fits better please change it or let me know.

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#250
Co-authored-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
This commit is contained in:
2025-12-03 10:19:34 +13:00
committed by jebus
parent 69df139256
commit 901de4640c
23 changed files with 126 additions and 49 deletions

View File

@ -41,7 +41,7 @@ jobs:
EP_DATA_DIR: /opt/enviPy/ EP_DATA_DIR: /opt/enviPy/
ALLOWED_HOSTS: 127.0.0.1,localhost ALLOWED_HOSTS: 127.0.0.1,localhost
DEBUG: True DEBUG: True
LOG_LEVEL: DEBUG LOG_LEVEL: INFO
MODEL_BUILDING_ENABLED: True MODEL_BUILDING_ENABLED: True
APPLICABILITY_DOMAIN_ENABLED: True APPLICABILITY_DOMAIN_ENABLED: True
ENVIFORMER_PRESENT: True ENVIFORMER_PRESENT: True

View File

@ -0,0 +1,22 @@
# Generated by Django 5.2.7 on 2025-12-02 13:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0011_auto_20251111_1413"),
]
operations = [
migrations.AddField(
model_name="node",
name="stereo_removed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="pathway",
name="predicted",
field=models.BooleanField(default=False),
),
]

View File

@ -1593,6 +1593,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
setting = models.ForeignKey( setting = models.ForeignKey(
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True "epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
) )
predicted = models.BooleanField(default=False, null=False)
@property @property
def root_nodes(self): def root_nodes(self):
@ -1804,6 +1805,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
smiles: str, smiles: str,
name: Optional[str] = None, name: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
predicted: bool = False,
): ):
pw = Pathway() pw = Pathway()
pw.package = package pw.package = package
@ -1816,6 +1818,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
pw.name = name pw.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
pw.predicted = predicted
pw.save() pw.save()
try: try:
@ -1946,6 +1949,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
) )
out_edges = models.ManyToManyField("epdb.Edge", verbose_name="Outgoing Edges") out_edges = models.ManyToManyField("epdb.Edge", verbose_name="Outgoing Edges")
depth = models.IntegerField(verbose_name="Node depth", null=False, blank=False) depth = models.IntegerField(verbose_name="Node depth", null=False, blank=False)
stereo_removed = models.BooleanField(default=False, null=False)
def _url(self): def _url(self):
return "{}/node/{}".format(self.pathway.url, self.uuid) return "{}/node/{}".format(self.pathway.url, self.uuid)
@ -1955,6 +1959,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
return { return {
"depth": self.depth, "depth": self.depth,
"stereo_removed": self.stereo_removed,
"url": self.url, "url": self.url,
"node_label_id": self.default_node_label.url, "node_label_id": self.default_node_label.url,
"image": f"{self.url}?image=svg", "image": f"{self.url}?image=svg",
@ -1980,12 +1985,17 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
name: Optional[str] = None, name: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
): ):
stereo_removed = False
if pathway.predicted and FormatConverter.has_stereo(smiles):
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
stereo_removed = True
c = Compound.create(pathway.package, smiles, name=name, description=description) c = Compound.create(pathway.package, smiles, name=name, description=description)
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists(): if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure) return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
n = Node() n = Node()
n.stereo_removed = stereo_removed
n.pathway = pathway n.pathway = pathway
n.depth = depth n.depth = depth

View File

@ -961,9 +961,9 @@ def package_model(request, package_uuid, model_uuid):
# Check if smiles is non empty and valid # Check if smiles is non empty and valid
if smiles == "": if smiles == "":
return JsonResponse({"error": "Received empty SMILES"}, status=400) return JsonResponse({"error": "Received empty SMILES"}, status=400)
stereo = FormatConverter.has_stereo(smiles)
try: try:
stand_smiles = FormatConverter.standardize(smiles) stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
except ValueError: except ValueError:
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400) return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
@ -974,7 +974,7 @@ def package_model(request, package_uuid, model_uuid):
current_user, predict_simple, current_model.pk, stand_smiles current_user, predict_simple, current_model.pk, stand_smiles
) )
res = [] res = {"pred": [], "stereo": stereo}
for pr in pred_res: for pr in pred_res:
if len(pr) > 0: if len(pr) > 0:
@ -983,7 +983,7 @@ def package_model(request, package_uuid, model_uuid):
logger.debug(f"Checking {prod_set}") logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set])) products.append(tuple([x for x in prod_set]))
res.append( res["pred"].append(
{ {
"products": list(set(products)), "products": list(set(products)),
"probability": pr.probability, "probability": pr.probability,
@ -1931,7 +1931,6 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed!", "Pathway prediction failed!",
"Pathway prediction failed due to missing or empty SMILES", "Pathway prediction failed due to missing or empty SMILES",
) )
try: try:
stand_smiles = FormatConverter.standardize(smiles) stand_smiles = FormatConverter.standardize(smiles)
except ValueError: except ValueError:
@ -1954,8 +1953,13 @@ def package_pathways(request, package_uuid):
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting) prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
else: else:
prediction_setting = current_user.prediction_settings() prediction_setting = current_user.prediction_settings()
pw = Pathway.create(
pw = Pathway.create(current_package, stand_smiles, name=name, description=description) current_package,
stand_smiles,
name=name,
description=description,
predicted=pw_mode in {"predict", "incremental"},
)
# set mode # set mode
pw.kv.update({"mode": pw_mode}) pw.kv.update({"mode": pw_mode})

View File

@ -360,7 +360,11 @@ function draw(pathway, elem) {
} }
function node_popup(n) { function node_popup(n) {
popupContent = "<a href='" + n.url + "'>" + n.name + "</a><br>"; popupContent = "";
if (n.stereo_removed) {
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
}
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
popupContent += "Depth " + n.depth + "<br>" popupContent += "Depth " + n.depth + "<br>"
if (appDomainViewEnabled) { if (appDomainViewEnabled) {

View File

@ -117,7 +117,9 @@
<!-- Predict Panel --> <!-- Predict Panel -->
<div class="collapse-arrow bg-base-200 collapse"> <div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Predict</div> <div class="collapse-title text-xl font-medium" id="predictTitle">
Predict
</div>
<div class="collapse-content"> <div class="collapse-content">
<div class="form-control"> <div class="form-control">
<div class="join w-full"> <div class="join w-full">
@ -193,7 +195,13 @@
{# FIXME: This is a hack to get the precision recall curve data into the JavaScript code. #} {# FIXME: This is a hack to get the precision recall curve data into the JavaScript code. #}
<script> <script>
function handlePredictionResponse(data) { function handlePredictionResponse(data) {
let res = "<table class='table table-zebra'>" let stereo = data["stereo"]
data = data["pred"]
let res = ""
if (stereo) {
res += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span><br>"
}
res += "<table class='table table-zebra'>"
res += "<thead>" res += "<thead>"
res += "<th scope='col'>#</th>" res += "<th scope='col'>#</th>"

View File

@ -1,11 +1,12 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure from epdb.models import Compound, User, CompoundStructure
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class CompoundTest(TestCase): class CompoundTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
def setUp(self): def setUp(self):
pass pass

View File

@ -1,12 +1,14 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction from epdb.models import Compound, User, Reaction
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class CopyTest(TestCase): class CopyTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,14 +1,16 @@
import os.path import os.path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Reaction, Compound, User, Rule, Package from epdb.models import Reaction, Compound, User, Rule, Package
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
from utilities.ml import RuleBasedDataset, EnviFormerDataset from utilities.ml import RuleBasedDataset, EnviFormerDataset
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class DatasetTest(TestCase): class DatasetTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
def setUp(self): def setUp(self):
self.cs1 = Compound.create( self.cs1 = Compound.create(
@ -106,7 +108,9 @@ class DatasetTest(TestCase):
def test_extra_features(self): def test_extra_features(self):
reactions = [r for r in Reaction.objects.filter(package=self.BBD_SUBSET)] reactions = [r for r in Reaction.objects.filter(package=self.BBD_SUBSET)]
applicable_rules = [r for r in Rule.objects.filter(package=self.BBD_SUBSET)] applicable_rules = [r for r in Rule.objects.filter(package=self.BBD_SUBSET)]
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules, feat_funcs=[FormatConverter.maccs, FormatConverter.morgan]) ds = RuleBasedDataset.generate_dataset(
reactions, applicable_rules, feat_funcs=[FormatConverter.maccs, FormatConverter.morgan]
)
print(ds.shape) print(ds.shape)
def test_to_arff(self): def test_to_arff(self):

View File

@ -3,7 +3,8 @@ from datetime import datetime
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase, tag from django.test import TestCase, override_settings, tag
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import EnviFormer, Setting, User from epdb.models import EnviFormer, Setting, User
@ -30,8 +31,9 @@ def measure_predict(mod, pathway_pk=None):
@tag("slow") @tag("slow")
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class EnviFormerTest(TestCase): class EnviFormerTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -2,7 +2,7 @@ from tempfile import TemporaryDirectory
import numpy as np import numpy as np
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import MLRelativeReasoning, RuleBasedRelativeReasoning, User from epdb.models import MLRelativeReasoning, RuleBasedRelativeReasoning, User
@ -10,8 +10,9 @@ from epdb.models import MLRelativeReasoning, RuleBasedRelativeReasoning, User
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class ModelTest(TestCase): class ModelTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -52,7 +53,7 @@ class ModelTest(TestCase):
expected = { expected = {
("CC=O", "CCNC(=O)C1=CC(C)=CC=C1"): ( ("CC=O", "CCNC(=O)C1=CC(C)=CC=C1"): (
"bt0243-4301", "bt0243-4301",
np.float64(0.33333333333333337), np.float64(0.5),
), ),
("CC1=CC=CC(C(=O)O)=C1", "CCNCC"): ("bt0430-4011", np.float64(0.25)), ("CC1=CC=CC(C(=O)O)=C1", "CCNCC"): ("bt0430-4011", np.float64(0.25)),
} }

View File

@ -1,5 +1,5 @@
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase from django.test import TestCase, override_settings
from networkx.utils.misc import graphs_equal from networkx.utils.misc import graphs_equal
from epdb.logic import PackageManager, SPathway from epdb.logic import PackageManager, SPathway
@ -9,8 +9,9 @@ from utilities.ml import graph_from_pathway, multigen_eval, pathway_edit_eval
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class MultiGenTest(TestCase): class MultiGenTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,11 +1,13 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction, Rule from epdb.models import Compound, User, Reaction, Rule
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class ReactionTest(TestCase): class ReactionTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,11 +1,12 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Rule, User from epdb.models import Rule, User
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class RuleTest(TestCase): class RuleTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,15 +1,16 @@
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase from django.test import TestCase, override_settings
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import SimpleAmbitRule, User from epdb.models import SimpleAmbitRule, User
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class SimpleAmbitRuleTest(TestCase): class SimpleAmbitRuleTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,4 +1,5 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from envipy_additional_information import Temperature, Interval from envipy_additional_information import Temperature, Interval
@ -6,8 +7,9 @@ from epdb.logic import UserManager, PackageManager
from epdb.models import Compound, Scenario, ExternalDatabase from epdb.models import Compound, Scenario, ExternalDatabase
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class CompoundViewTest(TestCase): class CompoundViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -9,12 +9,12 @@ Package = s.GET_PACKAGE_MODEL()
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True) @override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class PathwayViewTest(TestCase): class ModelViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(PathwayViewTest, cls).setUpClass() super(ModelViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user( cls.user1 = UserManager.create_user(
"user1", "user1",
"user1@envipath.com", "user1@envipath.com",
@ -72,7 +72,7 @@ class PathwayViewTest(TestCase):
}, },
] ]
actual = response.json() actual = response.json()["pred"]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
response = self.client.get( response = self.client.get(

View File

@ -1,6 +1,6 @@
from django.conf import settings as s from django.conf import settings as s
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, tag from django.test import TestCase, override_settings, tag
from django.urls import reverse from django.urls import reverse
from epdb.logic import UserManager from epdb.logic import UserManager
@ -15,8 +15,9 @@ from epdb.models import (
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class PackageViewTest(TestCase): class PackageViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,4 +1,5 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from envipy_additional_information import Temperature, Interval from envipy_additional_information import Temperature, Interval
@ -6,8 +7,9 @@ from epdb.logic import UserManager, PackageManager
from epdb.models import Reaction, Scenario, ExternalDatabase from epdb.models import Reaction, Scenario, ExternalDatabase
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class ReactionViewTest(TestCase): class ReactionViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,4 +1,5 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from envipy_additional_information import Temperature, Interval from envipy_additional_information import Temperature, Interval
@ -6,8 +7,9 @@ from epdb.logic import UserManager, PackageManager
from epdb.models import Rule, Scenario from epdb.models import Rule, Scenario
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class RuleViewTest(TestCase): class RuleViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -1,12 +1,14 @@
from django.test import TestCase from django.conf import settings as s
from django.test import TestCase, override_settings
from django.urls import reverse
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Package, User from epdb.models import Package, User
from django.urls import reverse
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class UserViewTest(TestCase): class UserViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -48,8 +50,9 @@ class UserViewTest(TestCase):
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# TODO currently fails as the fixture does not provide a global setting... self.assertContains(
self.assertContains(response, "Registration failed!") response, "Your account has been created! An admin will activate it soon!"
)
def test_register_password_mismatch(self): def test_register_password_mismatch(self):
response = self.client.post( response = self.client.post(

View File

@ -81,6 +81,10 @@ class FormatConverter(object):
def formula(smiles): def formula(smiles):
return Chem.rdMolDescriptors.CalcMolFormula(FormatConverter.from_smiles(smiles)) return Chem.rdMolDescriptors.CalcMolFormula(FormatConverter.from_smiles(smiles))
@staticmethod
def has_stereo(smiles):
return "@" in smiles or "/" in smiles or "\\" in smiles
@staticmethod @staticmethod
def from_smiles(smiles): def from_smiles(smiles):
return Chem.MolFromSmiles(smiles) return Chem.MolFromSmiles(smiles)