forked from enviPath/enviPy
Implement Compound CRUD (#22)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#22
This commit is contained in:
115
epdb/models.py
115
epdb/models.py
@ -281,58 +281,121 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
def structures(self):
|
||||
return CompoundStructure.objects.filter(compound=self)
|
||||
|
||||
@property
|
||||
def normalized_structure(self):
|
||||
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return '{}/compound/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
# @property
|
||||
# def related_pathways(self):
|
||||
# pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
|
||||
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
|
||||
@transaction.atomic
|
||||
def set_default_structure(self, cs: 'CompoundStructure'):
|
||||
if cs.compound != self:
|
||||
raise ValueError("Attempt to set a CompoundStructure stored in a different compound as default")
|
||||
|
||||
# @property
|
||||
# def related_reactions(self):
|
||||
# return (
|
||||
# Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
|
||||
# |
|
||||
# Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
||||
# ).order_by('name')
|
||||
self.default_structure = cs
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def related_pathways(self):
|
||||
pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
|
||||
return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
|
||||
|
||||
@property
|
||||
def related_reactions(self):
|
||||
return (
|
||||
Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
|
||||
|
|
||||
Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
||||
).order_by('name')
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs):
|
||||
def create(package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs) -> 'Compound':
|
||||
|
||||
# Pre check
|
||||
# Validity of SMILES etc
|
||||
if smiles is None or smiles == '':
|
||||
raise ValueError('SMILES is required')
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise ValueError('Given SMILES is invalid')
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles)
|
||||
|
||||
# Check if we find a direct match for a given SMILES
|
||||
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
|
||||
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
|
||||
|
||||
# Check if we can find the standardized one
|
||||
if CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package).exists():
|
||||
# TODO should we add a structure?
|
||||
return CompoundStructure.objects.get(smiles=standardized_smiles, compound__package=package).compound
|
||||
|
||||
# Generate Compound
|
||||
c = Compound()
|
||||
c.package = package
|
||||
if name is not None:
|
||||
|
||||
# For name and description we have defaults so only set them if they carry a real value
|
||||
if name is not None and name != '':
|
||||
c.name = name
|
||||
|
||||
if description is not None:
|
||||
if description is not None and description != '':
|
||||
c.description = description
|
||||
|
||||
c.save()
|
||||
|
||||
normalized_smiles = smiles # chem.normalize(smiles)
|
||||
is_standardized = standardized_smiles == smiles
|
||||
|
||||
if normalized_smiles != smiles:
|
||||
_ = CompoundStructure.create(c, normalized_smiles, name='Normalized structure of {}'.format(name),
|
||||
if not is_standardized:
|
||||
_ = CompoundStructure.create(c, standardized_smiles, name='Normalized structure of {}'.format(name),
|
||||
description='{} (in its normalized form)'.format(description),
|
||||
normalized_structure=True)
|
||||
|
||||
cs = CompoundStructure.create(c, smiles, name=name, description=description)
|
||||
cs = CompoundStructure.create(c, smiles, name=name, description=description, normalized_structure=is_standardized)
|
||||
|
||||
c.default_structure = cs
|
||||
c.save()
|
||||
|
||||
return c
|
||||
|
||||
@transaction.atomic
|
||||
def add_structure(self, smiles: str, name: str = None, description: str = None, default_structure: bool = False,
|
||||
*args, **kwargs) -> 'CompoundStructure':
|
||||
|
||||
if smiles is None or smiles == '':
|
||||
raise ValueError('SMILES is required')
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise ValueError('Given SMILES is invalid')
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles)
|
||||
|
||||
is_standardized = standardized_smiles == smiles
|
||||
|
||||
if self.normalized_structure.smiles != standardized_smiles:
|
||||
raise ValueError('The standardized SMILES does not match the compounds standardized one!')
|
||||
|
||||
if is_standardized:
|
||||
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
||||
|
||||
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
||||
if CompoundStructure.objects.filter(smiles__in=smiles, compound__package=self.package).exists():
|
||||
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
||||
|
||||
cs = CompoundStructure.create(self, smiles, name=name, description=description, normalized_structure=is_standardized)
|
||||
|
||||
if default_structure:
|
||||
self.default_structure = cs
|
||||
self.save()
|
||||
|
||||
return cs
|
||||
|
||||
class Meta:
|
||||
unique_together = [('uuid', 'package')]
|
||||
|
||||
@ -391,6 +454,10 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
def InChIKey(self):
|
||||
return FormatConverter.InChIKey(self.smiles)
|
||||
|
||||
@property
|
||||
def canonical_smiles(self):
|
||||
return FormatConverter.canonicalize(self.smiles)
|
||||
|
||||
@property
|
||||
def as_svg(self):
|
||||
return IndigoUtils.mol_to_svg(self.smiles)
|
||||
@ -401,10 +468,10 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
|
||||
# I think this only affects Django Admin which we are barely using
|
||||
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||
# _non_polymorphic = models.Manager()
|
||||
#
|
||||
# class Meta:
|
||||
# base_manager_name = '_non_polymorphic'
|
||||
_non_polymorphic = models.Manager()
|
||||
|
||||
class Meta:
|
||||
base_manager_name = '_non_polymorphic'
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, *args, **kwargs):
|
||||
|
||||
@ -684,7 +684,7 @@ def package_compound(request, package_uuid, compound_uuid):
|
||||
|
||||
return render(request, 'objects/compound.html', context)
|
||||
|
||||
if request.method == 'POST':
|
||||
elif request.method == 'POST':
|
||||
if hidden := request.POST.get('hidden', None):
|
||||
if hidden == 'delete-compound':
|
||||
current_compound.delete()
|
||||
@ -706,6 +706,8 @@ def package_compound(request, package_uuid, compound_uuid):
|
||||
return redirect(current_compound.url)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
# https://envipath.org/package/<uuid>/compound/<uuid>/structure
|
||||
@ -724,7 +726,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
||||
reviewed_compound_structure_qs = CompoundStructure.objects.none()
|
||||
unreviewed_compound_structure_qs = CompoundStructure.objects.none()
|
||||
|
||||
if package.reviewed:
|
||||
if current_package.reviewed:
|
||||
reviewed_compound_structure_qs = current_compound.structures.order_by('name')
|
||||
else:
|
||||
unreviewed_compound_structure_qs = current_compound.structures.order_by('name')
|
||||
@ -734,13 +736,24 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
||||
|
||||
return render(request, 'collections/objects_list.html', context)
|
||||
|
||||
elif request.method == 'POST':
|
||||
structure_name = request.POST.get('structure-name')
|
||||
structure_smiles = request.POST.get('structure-smiles')
|
||||
structure_description = request.POST.get('structure-description')
|
||||
|
||||
cs = current_compound.add_structure(structure_smiles, structure_name, structure_description)
|
||||
|
||||
return redirect(cs.url)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# https://envipath.org/package/<id>/compound/<id>/structure/<id>
|
||||
def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
|
||||
current_user = _anonymous_or_real(request)
|
||||
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||
current_compound = Compound.objects.get(package=current_package, uuid=compound_uuid)
|
||||
current_structure = CompoundStructure.get(compound=current_compound, uuid=structure_uuid)
|
||||
current_structure = CompoundStructure.objects.get(compound=current_compound, uuid=structure_uuid)
|
||||
|
||||
if request.method == 'GET':
|
||||
context = get_base_context(request)
|
||||
@ -818,6 +831,8 @@ def package_rules(request, package_uuid):
|
||||
r = Rule.create(current_package, rule_type, name=rule_name, description=rule_description, **params)
|
||||
return redirect(r.url)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# https://envipath.org/package/<id>/rule/<id>
|
||||
def package_rule(request, package_uuid, rule_uuid):
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
<a role="button" data-toggle="modal" data-target="#edit_compound_modal">
|
||||
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#add_structure_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#delete_compound_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
||||
|
||||
@ -16,12 +16,13 @@
|
||||
<input id="compound-name" class="form-control" name="compound-name" placeholder="Name"/>
|
||||
<label for="compound-description">Description</label>
|
||||
<input id="compound-description" class="form-control" name="compound-description" placeholder="Description"/>
|
||||
<label for="compound-smiles">SMILES</label>
|
||||
<input type="text" class="form-control" name="compound-smiles" placeholder="SMILES" id="compound-smiles">
|
||||
<p></p>
|
||||
<div>
|
||||
<iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
|
||||
height="510"></iframe>
|
||||
</div>
|
||||
<input type="hidden" name="compound-smiles" id="compound-smiles">
|
||||
<p></p>
|
||||
</form>
|
||||
</div>
|
||||
@ -35,16 +36,43 @@
|
||||
|
||||
</div>
|
||||
<script>
|
||||
|
||||
function newCompoundModalketcherToNewCompoundModalTextInput() {
|
||||
$('#compound-smiles').val(this.ketcher.getSmiles());
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#new_compound_ketcher').on('load', function() {
|
||||
const checkKetcherReady = () => {
|
||||
win = this.contentWindow
|
||||
if (win.ketcher && 'editor' in win.ketcher) {
|
||||
win.ketcher.editor.event.change.handlers.push({
|
||||
once: false,
|
||||
priority: 0,
|
||||
f: newCompoundModalketcherToNewCompoundModalTextInput,
|
||||
ketcher: win.ketcher
|
||||
});
|
||||
} else {
|
||||
setTimeout(checkKetcherReady, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkKetcherReady();
|
||||
})
|
||||
|
||||
$(function() {
|
||||
$('#new_compound_modal_form_submit').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).prop("disabled",true);
|
||||
|
||||
k = getKetcher('new_compound_ketcher');
|
||||
$('#compound-smiles').val(k.getSmiles());
|
||||
|
||||
|
||||
// submit form
|
||||
$('#new_compound_modal_form').submit();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
78
templates/modals/objects/add_structure_modal.html
Normal file
78
templates/modals/objects/add_structure_modal.html
Normal file
@ -0,0 +1,78 @@
|
||||
{% load static %}
|
||||
<div class="modal fade bs-modal-lg" id="add_structure_modal" tabindex="-1" aria-labelledby="add_structure_modal" aria-modal="true"
|
||||
role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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>
|
||||
<h4 class="modal-title">Create a new Structure</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="add_structure_modal_form" accept-charset="UTF-8" action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}" data-remote="true" method="post">
|
||||
{% csrf_token %}
|
||||
<label for="structure-name">Name</label>
|
||||
<input id="structure-name" class="form-control" name="structure-name" placeholder="Name"/>
|
||||
<label for="structure-description">Description</label>
|
||||
<input id="structure-description" class="form-control" name="structure-description" placeholder="Description"/>
|
||||
<label for="structure-smiles">SMILES</label>
|
||||
<input type="text" class="form-control" name="structure-smiles" placeholder="SMILES" id="structure-smiles">
|
||||
<p></p>
|
||||
<div>
|
||||
<iframe id="add_structure_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
|
||||
height="510"></iframe>
|
||||
</div>
|
||||
<p></p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="add_structure_modal_form_submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
|
||||
function newStructureModalketcherToNewStructureModalTextInput() {
|
||||
$('#structure-smiles').val(this.ketcher.getSmiles());
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#add_structure_ketcher').on('load', function() {
|
||||
const checkKetcherReady = () => {
|
||||
win = this.contentWindow
|
||||
if (win.ketcher && 'editor' in win.ketcher) {
|
||||
win.ketcher.editor.event.change.handlers.push({
|
||||
once: false,
|
||||
priority: 0,
|
||||
f: newStructureModalketcherToNewStructureModalTextInput,
|
||||
ketcher: win.ketcher
|
||||
});
|
||||
} else {
|
||||
setTimeout(checkKetcherReady, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkKetcherReady();
|
||||
})
|
||||
|
||||
$(function() {
|
||||
$('#add_structure_modal_form_submit').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).prop("disabled",true);
|
||||
|
||||
|
||||
|
||||
// submit form
|
||||
$('#add_structure_modal_form').submit();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_compound_modal.html" %}
|
||||
{% include "modals/objects/add_structure_modal.html" %}
|
||||
{% include "modals/objects/delete_compound_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
@ -71,7 +72,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMILES -->
|
||||
<!-- Canonical SMILES -->
|
||||
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||
<h4 class="panel-title">
|
||||
<a id="compound-canonical-smiles-link" data-toggle="collapse" data-parent="#compound-detail"
|
||||
href="#compound-canonical-smiles">Canonical SMILES Representation</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="compound-canonical-smiles" class="panel-collapse collapse in">
|
||||
<div class="panel-body list-group-item">
|
||||
{{ compound.default_structure.canonical_smiles }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- InChiKey -->
|
||||
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||
<h4 class="panel-title">
|
||||
<a id="compound-inchi-link" data-toggle="collapse" data-parent="#compound-detail"
|
||||
@ -84,10 +98,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Reactions -->
|
||||
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||
<h4 class="panel-title">
|
||||
<a id="compound-reaction-link" data-toggle="collapse" data-parent="#compound-detail"
|
||||
href="#compound-reaction">Reactions</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="compound-reaction" class="panel-collapse collapse in">
|
||||
<div class="panel-body list-group-item">
|
||||
{% for r in compound.related_reactions %}
|
||||
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pathways -->
|
||||
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||
<h4 class="panel-title">
|
||||
<a id="compound-pathway-link" data-toggle="collapse" data-parent="#compound-detail"
|
||||
href="#compound-pathway">Pathways</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="compound-pathway" class="panel-collapse collapse in">
|
||||
<div class="panel-body list-group-item">
|
||||
{% for r in compound.related_pathways %}
|
||||
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External Identifiers -->
|
||||
|
||||
|
||||
196
tests/test_compound_model.py
Normal file
196
tests/test_compound_model.py
Normal file
@ -0,0 +1,196 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, CompoundStructure
|
||||
|
||||
|
||||
class CompoundTest(TestCase):
|
||||
fixtures = ["test_fixture.json.gz"]
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CompoundTest, cls).setUpClass()
|
||||
cls.user = User.objects.get(username='anonymous')
|
||||
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_smoke(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
self.assertEqual(c.default_structure.smiles,
|
||||
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
|
||||
self.assertEqual(c.name, 'Afoxolaner')
|
||||
self.assertEqual(c.description, 'No Desc')
|
||||
|
||||
def test_missing_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(
|
||||
self.package,
|
||||
smiles=None,
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(
|
||||
self.package,
|
||||
smiles='',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
def test_smiles_are_trimmed(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles=' C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
self.assertEqual(c.default_structure.smiles,
|
||||
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
|
||||
|
||||
def test_name_and_description_optional(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
)
|
||||
|
||||
self.assertEqual(c.name, 'no name')
|
||||
self.assertEqual(c.description, 'no description')
|
||||
|
||||
def test_empty_name_and_description_are_ignored(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='',
|
||||
description='',
|
||||
)
|
||||
|
||||
self.assertEqual(c.name, 'no name')
|
||||
self.assertEqual(c.description, 'no description')
|
||||
|
||||
def test_deduplication(self):
|
||||
c1 = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
c2 = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
# Check if create detects that this Compound already exist
|
||||
# In this case the existing object should be returned
|
||||
self.assertEqual(c1.pk, c2.pk)
|
||||
self.assertEqual(len(self.package.compounds), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(
|
||||
self.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='Afoxolaner',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
def test_create_with_standardized_smiles(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Standardized SMILES',
|
||||
description='No Desc'
|
||||
)
|
||||
self.assertEqual(len(c.structures.all()), 1)
|
||||
|
||||
cs = c.structures.all()[0]
|
||||
self.assertEqual(cs.normalized_structure, True)
|
||||
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
|
||||
|
||||
def test_create_with_non_standardized_smiles(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1',
|
||||
name='Non Standardized SMILES',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
self.assertEqual(len(c.structures.all()), 2)
|
||||
for cs in c.structures.all():
|
||||
if cs.normalized_structure:
|
||||
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
|
||||
break
|
||||
else:
|
||||
# Loop finished without break, lets fail...
|
||||
self.assertEqual(1, 2)
|
||||
|
||||
def test_add_structure_smoke(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Standardized SMILES',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
c.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
|
||||
|
||||
self.assertEqual(len(c.structures.all()), 2)
|
||||
|
||||
def test_add_structure_with_different_normalized_smiles(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Standardized SMILES',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
c.add_structure(
|
||||
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
'Different Standardized SMILES')
|
||||
|
||||
def test_delete(self):
|
||||
c = Compound.create(
|
||||
self.package,
|
||||
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Standardization Test',
|
||||
description='No Desc'
|
||||
)
|
||||
|
||||
c.delete()
|
||||
|
||||
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0)
|
||||
self.assertEqual(CompoundStructure.objects.filter(compound__package=self.package).count(), 0)
|
||||
|
||||
def test_set_as_default_structure(self):
|
||||
c1 = Compound.create(
|
||||
self.package,
|
||||
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Standardized SMILES',
|
||||
description='No Desc'
|
||||
)
|
||||
default_structure = c1.default_structure
|
||||
|
||||
c2 = c1.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
|
||||
|
||||
c1.set_default_structure(c2)
|
||||
self.assertNotEqual(default_structure, c2)
|
||||
@ -70,13 +70,17 @@ class FormatConverter(object):
|
||||
return Chem.MolFromSmiles(smiles)
|
||||
|
||||
@staticmethod
|
||||
def to_smiles(mol):
|
||||
return Chem.MolToSmiles(mol)
|
||||
def to_smiles(mol, canonical=False):
|
||||
return Chem.MolToSmiles(mol, canonical=canonical)
|
||||
|
||||
@staticmethod
|
||||
def InChIKey(smiles):
|
||||
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def canonicalize(smiles: str):
|
||||
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
|
||||
|
||||
@staticmethod
|
||||
def maccs(smiles):
|
||||
mol = Chem.MolFromSmiles(smiles)
|
||||
|
||||
@ -36,7 +36,8 @@ def ensure_plugins_installed():
|
||||
if not is_installed(package_name):
|
||||
install_wheel(wheel_path)
|
||||
else:
|
||||
print(f"Plugin already installed: {package_name}")
|
||||
# print(f"Plugin already installed: {package_name}")
|
||||
pass
|
||||
|
||||
|
||||
def discover_plugins(_cls: Type = None) -> Dict[str, Type]:
|
||||
|
||||
Reference in New Issue
Block a user