Implement Compound CRUD (#22)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#22
This commit is contained in:
2025-07-03 07:17:04 +12:00
parent 4e58a1fad7
commit 9323a9f7d7
9 changed files with 472 additions and 40 deletions

View File

@ -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):

View File

@ -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):