forked from enviPath/enviPy
[Feature] Generate Compounds by Molfile (#423)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#423
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
from django.conf import settings as s
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, CompoundStructure
|
||||
|
||||
@ -19,6 +19,10 @@ class CompoundTest(TestCase):
|
||||
cls.user = User.objects.get(username="anonymous")
|
||||
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
||||
|
||||
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||
cls.INVALID_MOLFILE = "this is not a valid molfile"
|
||||
|
||||
def test_smoke(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
@ -190,3 +194,153 @@ class CompoundTest(TestCase):
|
||||
|
||||
c1.set_default_structure(c2)
|
||||
self.assertNotEqual(default_structure, c2)
|
||||
|
||||
def test_create_structure_from_molfile(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="PLACEHOLDER",
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Molfile Compound",
|
||||
description="Created from molfile",
|
||||
)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="PLACEHOLDER",
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Molfile Structure",
|
||||
description="Structure from molfile",
|
||||
)
|
||||
|
||||
# The SMILES must have been derived from the molfile, not the placeholder
|
||||
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
|
||||
self.assertIsNotNone(cs.smiles)
|
||||
self.assertTrue(len(cs.smiles) > 0)
|
||||
|
||||
def test_create_structure_from_molfile_stores_molfile(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
|
||||
name="Molfile Store Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs = c.default_structure
|
||||
|
||||
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
|
||||
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
|
||||
# on the existing structure
|
||||
_ = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Structure with Molfile",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
# Fetch fresh from DB to confirm persistence
|
||||
cs_db = CompoundStructure.objects.get(pk=cs.pk)
|
||||
self.assertIsNotNone(cs_db.molfile)
|
||||
self.assertNotEqual(cs_db.molfile.strip(), "")
|
||||
|
||||
def test_create_structure_from_invalid_molfile_raises(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
name="Invalid Molfile Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidMolfileException):
|
||||
CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
molfile=self.INVALID_MOLFILE,
|
||||
name="Bad Structure",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
def test_molfile_takes_precedence_over_smiles(self):
|
||||
"""When both molfile and smiles are supplied, molfile must win."""
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
name="Precedence Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="C", # intentionally wrong / different SMILES
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Molfile Wins",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
# SMILES should be derived from molfile, not the supplied "C"
|
||||
self.assertNotEqual(cs.smiles, "C")
|
||||
|
||||
def test_empty_molfile_falls_back_to_smiles(self):
|
||||
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
name="Empty Molfile Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
molfile=" ", # whitespace only – should be ignored
|
||||
name="Fallback to SMILES",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||
|
||||
def test_none_molfile_falls_back_to_smiles(self):
|
||||
"""None as molfile should be ignored and SMILES used instead."""
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
name="None Molfile Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
molfile=None,
|
||||
name="Fallback None Molfile",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||
|
||||
def test_molfile_deduplication(self):
|
||||
"""Creating a structure twice from the same molfile should return the existing object."""
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||
name="Molfile Dedup Test",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs1 = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="PLACEHOLDER",
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Molfile Structure",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
cs2 = CompoundStructure.create(
|
||||
compound=c,
|
||||
smiles="PLACEHOLDER",
|
||||
molfile=self.VALID_MOLFILE,
|
||||
name="Molfile Structure",
|
||||
description="No Desc",
|
||||
)
|
||||
|
||||
self.assertEqual(cs1.pk, cs2.pk)
|
||||
|
||||
@ -11,6 +11,9 @@ from epdb.models import Compound, Scenario, ExternalDatabase
|
||||
class CompoundViewTest(TestCase):
|
||||
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
||||
|
||||
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||
VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CompoundViewTest, cls).setUpClass()
|
||||
@ -396,3 +399,108 @@ class CompoundViewTest(TestCase):
|
||||
|
||||
c = Compound.objects.get(url=compound_url)
|
||||
self.assertEqual(len(c.aliases), 0)
|
||||
|
||||
def test_create_compound_via_molfile(self):
|
||||
"""POSTing a valid molfile without a SMILES should create a compound successfully."""
|
||||
response = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "4-Nitrobenzoic acid",
|
||||
"compound-description": "Created from molfile",
|
||||
"compound-molfile": self.VALID_MOLFILE,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
compound_url = response.url
|
||||
|
||||
c = Compound.objects.get(url=compound_url)
|
||||
|
||||
self.assertEqual(c.name, "4-Nitrobenzoic acid")
|
||||
self.assertEqual(c.description, "Created from molfile")
|
||||
# SMILES should have been extracted from molfile, so it must be non-empty
|
||||
self.assertIsNotNone(c.default_structure.smiles)
|
||||
self.assertNotEqual(c.default_structure.smiles.strip(), "")
|
||||
|
||||
def test_create_compound_molfile_takes_precedence_over_smiles(self):
|
||||
"""When both molfile and SMILES are submitted, the molfile should win."""
|
||||
response = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "4-Nitrobenzoic acid",
|
||||
"compound-description": "Molfile precedence test",
|
||||
"compound-smiles": "C", # intentionally wrong/different
|
||||
"compound-molfile": self.VALID_MOLFILE,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
compound_url = response.url
|
||||
|
||||
c = Compound.objects.get(url=compound_url)
|
||||
|
||||
# The resulting SMILES must NOT be the dummy "C" supplied via compound-smiles
|
||||
self.assertNotEqual(c.default_structure.smiles, "C")
|
||||
|
||||
def test_create_compound_via_invalid_molfile_returns_error(self):
|
||||
"""POSTing an invalid molfile should not create a compound and should return an error response."""
|
||||
initial_count = self.user1_default_package.compounds.count()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "Bad Molfile Compound",
|
||||
"compound-description": "Should fail",
|
||||
"compound-molfile": "this is not a molfile at all",
|
||||
},
|
||||
)
|
||||
|
||||
# The view should signal an error (non-2xx or a redirect to an error page, not 302 to a new compound)
|
||||
self.assertNotEqual(response.status_code, 302)
|
||||
# No new compound should have been created
|
||||
self.assertEqual(self.user1_default_package.compounds.count(), initial_count)
|
||||
|
||||
def test_create_compound_via_empty_molfile_falls_back_to_smiles(self):
|
||||
"""Submitting an empty molfile with a valid SMILES should fall back to SMILES."""
|
||||
response = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "Fallback SMILES Compound",
|
||||
"compound-description": "Empty molfile fallback",
|
||||
"compound-smiles": "C(CCl)Cl",
|
||||
"compound-molfile": " ", # whitespace only
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
compound_url = response.url
|
||||
|
||||
c = Compound.objects.get(url=compound_url)
|
||||
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
|
||||
|
||||
def test_create_compound_via_molfile_deduplication(self):
|
||||
"""Submitting the same molfile twice should return the existing compound."""
|
||||
response1 = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "4-Nitrobenzoic acid",
|
||||
"compound-description": "Molfile dedup test",
|
||||
"compound-molfile": self.VALID_MOLFILE,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response1.status_code, 302)
|
||||
compound_url_1 = response1.url
|
||||
|
||||
response2 = self.client.post(
|
||||
reverse("compounds"),
|
||||
{
|
||||
"compound-name": "4-Nitrobenzoic acid",
|
||||
"compound-description": "Molfile dedup test",
|
||||
"compound-molfile": self.VALID_MOLFILE,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response2.status_code, 302)
|
||||
self.assertEqual(response2.url, compound_url_1)
|
||||
self.assertEqual(self.user1_default_package.compounds.count(), 1)
|
||||
|
||||
Reference in New Issue
Block a user