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):
|
def structures(self):
|
||||||
return CompoundStructure.objects.filter(compound=self)
|
return CompoundStructure.objects.filter(compound=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def normalized_structure(self):
|
||||||
|
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return '{}/compound/{}'.format(self.package.url, self.uuid)
|
return '{}/compound/{}'.format(self.package.url, self.uuid)
|
||||||
|
|
||||||
# @property
|
@transaction.atomic
|
||||||
# def related_pathways(self):
|
def set_default_structure(self, cs: 'CompoundStructure'):
|
||||||
# pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
|
if cs.compound != self:
|
||||||
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
|
raise ValueError("Attempt to set a CompoundStructure stored in a different compound as default")
|
||||||
|
|
||||||
# @property
|
self.default_structure = cs
|
||||||
# def related_reactions(self):
|
self.save()
|
||||||
# return (
|
|
||||||
# Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
|
@property
|
||||||
# |
|
def related_pathways(self):
|
||||||
# Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list('pathway', flat=True)
|
||||||
# ).order_by('name')
|
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
|
@staticmethod
|
||||||
@transaction.atomic
|
@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
|
if smiles is None or smiles == '':
|
||||||
# Validity of SMILES etc
|
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():
|
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
|
||||||
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
|
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
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
c.package = package
|
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
|
c.name = name
|
||||||
|
|
||||||
if description is not None:
|
if description is not None and description != '':
|
||||||
c.description = description
|
c.description = description
|
||||||
|
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
normalized_smiles = smiles # chem.normalize(smiles)
|
is_standardized = standardized_smiles == smiles
|
||||||
|
|
||||||
if normalized_smiles != smiles:
|
if not is_standardized:
|
||||||
_ = CompoundStructure.create(c, normalized_smiles, name='Normalized structure of {}'.format(name),
|
_ = CompoundStructure.create(c, standardized_smiles, name='Normalized structure of {}'.format(name),
|
||||||
description='{} (in its normalized form)'.format(description),
|
description='{} (in its normalized form)'.format(description),
|
||||||
normalized_structure=True)
|
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.default_structure = cs
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
return c
|
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:
|
class Meta:
|
||||||
unique_together = [('uuid', 'package')]
|
unique_together = [('uuid', 'package')]
|
||||||
|
|
||||||
@ -391,6 +454,10 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
def InChIKey(self):
|
def InChIKey(self):
|
||||||
return FormatConverter.InChIKey(self.smiles)
|
return FormatConverter.InChIKey(self.smiles)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def canonical_smiles(self):
|
||||||
|
return FormatConverter.canonicalize(self.smiles)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
return IndigoUtils.mol_to_svg(self.smiles)
|
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
|
# I think this only affects Django Admin which we are barely using
|
||||||
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||||
# _non_polymorphic = models.Manager()
|
_non_polymorphic = models.Manager()
|
||||||
#
|
|
||||||
# class Meta:
|
class Meta:
|
||||||
# base_manager_name = '_non_polymorphic'
|
base_manager_name = '_non_polymorphic'
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def apply(self, *args, **kwargs):
|
def apply(self, *args, **kwargs):
|
||||||
|
|||||||
@ -684,7 +684,7 @@ def package_compound(request, package_uuid, compound_uuid):
|
|||||||
|
|
||||||
return render(request, 'objects/compound.html', context)
|
return render(request, 'objects/compound.html', context)
|
||||||
|
|
||||||
if request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
if hidden := request.POST.get('hidden', None):
|
if hidden := request.POST.get('hidden', None):
|
||||||
if hidden == 'delete-compound':
|
if hidden == 'delete-compound':
|
||||||
current_compound.delete()
|
current_compound.delete()
|
||||||
@ -706,6 +706,8 @@ def package_compound(request, package_uuid, compound_uuid):
|
|||||||
return redirect(current_compound.url)
|
return redirect(current_compound.url)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
# https://envipath.org/package/<uuid>/compound/<uuid>/structure
|
# 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()
|
reviewed_compound_structure_qs = CompoundStructure.objects.none()
|
||||||
unreviewed_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')
|
reviewed_compound_structure_qs = current_compound.structures.order_by('name')
|
||||||
else:
|
else:
|
||||||
unreviewed_compound_structure_qs = current_compound.structures.order_by('name')
|
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)
|
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>
|
# https://envipath.org/package/<id>/compound/<id>/structure/<id>
|
||||||
def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
|
def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
|
||||||
current_user = _anonymous_or_real(request)
|
current_user = _anonymous_or_real(request)
|
||||||
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
current_compound = Compound.objects.get(package=current_package, uuid=compound_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':
|
if request.method == 'GET':
|
||||||
context = get_base_context(request)
|
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)
|
r = Rule.create(current_package, rule_type, name=rule_name, description=rule_description, **params)
|
||||||
return redirect(r.url)
|
return redirect(r.url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
# https://envipath.org/package/<id>/rule/<id>
|
# https://envipath.org/package/<id>/rule/<id>
|
||||||
def package_rule(request, package_uuid, rule_uuid):
|
def package_rule(request, package_uuid, rule_uuid):
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
<a role="button" data-toggle="modal" data-target="#edit_compound_modal">
|
<a role="button" data-toggle="modal" data-target="#edit_compound_modal">
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
|
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#add_structure_modal">
|
||||||
|
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="button" data-toggle="modal" data-target="#delete_compound_modal">
|
<a class="button" data-toggle="modal" data-target="#delete_compound_modal">
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
<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"/>
|
<input id="compound-name" class="form-control" name="compound-name" placeholder="Name"/>
|
||||||
<label for="compound-description">Description</label>
|
<label for="compound-description">Description</label>
|
||||||
<input id="compound-description" class="form-control" name="compound-description" placeholder="Description"/>
|
<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>
|
<p></p>
|
||||||
<div>
|
<div>
|
||||||
<iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
|
<iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
|
||||||
height="510"></iframe>
|
height="510"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="compound-smiles" id="compound-smiles">
|
|
||||||
<p></p>
|
<p></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -35,16 +36,43 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
function newCompoundModalketcherToNewCompoundModalTextInput() {
|
||||||
|
$('#compound-smiles').val(this.ketcher.getSmiles());
|
||||||
|
}
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#new_compound_modal_form_submit').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).prop("disabled",true);
|
|
||||||
|
|
||||||
k = getKetcher('new_compound_ketcher');
|
$('#new_compound_ketcher').on('load', function() {
|
||||||
$('#compound-smiles').val(k.getSmiles());
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// submit form
|
checkKetcherReady();
|
||||||
$('#new_compound_modal_form').submit();
|
})
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('#new_compound_modal_form_submit').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).prop("disabled",true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// submit form
|
||||||
|
$('#new_compound_modal_form').submit();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</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 %}
|
{% block action_modals %}
|
||||||
{% include "modals/objects/edit_compound_modal.html" %}
|
{% include "modals/objects/edit_compound_modal.html" %}
|
||||||
|
{% include "modals/objects/add_structure_modal.html" %}
|
||||||
{% include "modals/objects/delete_compound_modal.html" %}
|
{% include "modals/objects/delete_compound_modal.html" %}
|
||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
@ -71,7 +72,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a id="compound-inchi-link" data-toggle="collapse" data-parent="#compound-detail"
|
<a id="compound-inchi-link" data-toggle="collapse" data-parent="#compound-detail"
|
||||||
@ -84,10 +98,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Reactions -->
|
<!-- 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 -->
|
<!-- 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 -->
|
<!-- 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)
|
return Chem.MolFromSmiles(smiles)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_smiles(mol):
|
def to_smiles(mol, canonical=False):
|
||||||
return Chem.MolToSmiles(mol)
|
return Chem.MolToSmiles(mol, canonical=canonical)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def InChIKey(smiles):
|
def InChIKey(smiles):
|
||||||
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
|
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def canonicalize(smiles: str):
|
||||||
|
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def maccs(smiles):
|
def maccs(smiles):
|
||||||
mol = Chem.MolFromSmiles(smiles)
|
mol = Chem.MolFromSmiles(smiles)
|
||||||
|
|||||||
@ -36,7 +36,8 @@ def ensure_plugins_installed():
|
|||||||
if not is_installed(package_name):
|
if not is_installed(package_name):
|
||||||
install_wheel(wheel_path)
|
install_wheel(wheel_path)
|
||||||
else:
|
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]:
|
def discover_plugins(_cls: Type = None) -> Dict[str, Type]:
|
||||||
|
|||||||
Reference in New Issue
Block a user