diff --git a/Dockerfile b/Dockerfile index 8bb98ee2..60ed33dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,12 +37,13 @@ RUN --mount=type=ssh \ # Now copy source and do a final sync to install the project itself # Ensure .dockerignore is reasonable COPY bayer bayer -COPY bridge bridge COPY biotransformer biotransformer +COPY bridge bridge COPY envipath envipath COPY epapi epapi COPY epauth epauth COPY epdb epdb +COPY epiuclid epiuclid COPY fixtures fixtures COPY migration migration COPY pepper pepper diff --git a/bayer/migrations/0003_package_data_pool.py b/bayer/migrations/0003_package_data_pool.py deleted file mode 100644 index 35d2b433..00000000 --- a/bayer/migrations/0003_package_data_pool.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 6.0.3 on 2026-04-14 19:07 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bayer', '0002_initial'), - ('epdb', '0023_alter_compoundstructure_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='package', - name='data_pool', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'), - ), - ] diff --git a/bayer/migrations/0004_pescompound_pesstructure.py b/bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py similarity index 72% rename from bayer/migrations/0004_pescompound_pesstructure.py rename to bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py index b2a8f343..066eec65 100644 --- a/bayer/migrations/0004_pescompound_pesstructure.py +++ b/bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-04-15 20:03 +# Generated by Django 6.0.3 on 2026-04-17 21:22 import django.db.models.deletion from django.db import migrations, models @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bayer', '0003_package_data_pool'), + ('bayer', '0002_initial'), ('epdb', '0023_alter_compoundstructure_options_and_more'), ] @@ -26,10 +26,16 @@ class Migration(migrations.Migration): name='PESStructure', fields=[ ('compoundstructure_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compoundstructure')), + ('pes_link', models.URLField(verbose_name='PES Link')), ], options={ 'abstract': False, }, bases=('epdb.compoundstructure',), ), + migrations.AddField( + model_name='package', + name='data_pool', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'), + ), ] diff --git a/bayer/migrations/0005_pesstructure_pes_link.py b/bayer/migrations/0005_pesstructure_pes_link.py deleted file mode 100644 index 072b349b..00000000 --- a/bayer/migrations/0005_pesstructure_pes_link.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 6.0.3 on 2026-04-16 08:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bayer', '0004_pescompound_pesstructure'), - ] - - operations = [ - migrations.AddField( - model_name='pesstructure', - name='pes_link', - field=models.URLField(default=None, verbose_name='PES Link'), - preserve_default=False, - ), - ] diff --git a/bayer/models.py b/bayer/models.py index 53bffc16..b4ac7632 100644 --- a/bayer/models.py +++ b/bayer/models.py @@ -15,6 +15,7 @@ from epdb.models import ( SimpleAmbitRule, SimpleRDKitRule, ) +from utilities.chem import FormatConverter class Package(EnviPathModel): @@ -114,7 +115,9 @@ class PESCompound(Compound): # Check if we find a direct match for a given pes_link if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists(): - return PESStructure.objects.get(pes_link=pes_url, compound__package=package).compound + # Due to normalization we might end up in having multiple structures + # All of them point to the same compound -> pick any + return PESStructure.objects.filter(pes_link=pes_url, compound__package=package).first().compound # Generate Compound c = PESCompound() @@ -135,19 +138,37 @@ class PESCompound(Compound): c.save() + molfile = pes_data.get("representativeStructures", [{}])[0].get("ctab") + + if molfile is None: + raise ValueError("PES data does not contain a valid mol file!") + + smiles = FormatConverter.to_smiles(FormatConverter.from_molfile(molfile)) + + standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True) + is_standardized = standardized_smiles == smiles if not is_standardized: - _ = CompoundStructure.create( + _ = PESStructure.create( c, + pes_url, + molfile, 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, normalized_structure=is_standardized + + cs = PESStructure.create( + c, + pes_url, + molfile, + smiles, + name=name, + description=description, + normalized_structure=is_standardized ) c.default_structure = cs @@ -159,6 +180,53 @@ class PESCompound(Compound): class PESStructure(CompoundStructure): pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link") + @staticmethod + @transaction.atomic + def create( + compound: Compound, + pes_link: str, + mol_file: str, + smiles: str, + name: str = None, + description: str = None, + *args, + **kwargs + ): + if compound.pk is None: + raise ValueError("Unpersisted Compound! Persist compound first!") + + cs = PESStructure() + # Clean for potential XSS + if name is not None: + cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + + if description is not None: + cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() + + cs.smiles = smiles + cs.mol_file = mol_file + cs.pes_link = pes_link + cs.compound = compound + + if "normalized_structure" in kwargs: + cs.normalized_structure = kwargs["normalized_structure"] + + cs.save() + + return cs + + @transaction.atomic + def add_structure( + self, + smiles: str, + name: str = None, + description: str = None, + default_structure: bool = False, + *args, + **kwargs, + ) -> "CompoundStructure": + raise ValueError("Not supported!") + def d3_json(self): return { "is_pes": True, diff --git a/bayer/templates/modals/collections/new_pes_modal.html b/bayer/templates/modals/collections/new_pes_modal.html index 7727697c..1ebbdc34 100644 --- a/bayer/templates/modals/collections/new_pes_modal.html +++ b/bayer/templates/modals/collections/new_pes_modal.html @@ -154,7 +154,7 @@ + + + +
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/bayer/templates/objects/compound_structure_viz.html b/bayer/templates/objects/compound_structure_viz.html index aeab1fd4..8924c63e 100644 --- a/bayer/templates/objects/compound_structure_viz.html +++ b/bayer/templates/objects/compound_structure_viz.html @@ -1,4 +1,11 @@ {% if compound_structure.pes_link %} + +
+ +
Link to PES
+
{{ compound_structure.pes_link }}
+
+
diff --git a/bayer/templates/objects/compound_viz.html b/bayer/templates/objects/compound_viz.html index 7727fa37..9ff10e75 100644 --- a/bayer/templates/objects/compound_viz.html +++ b/bayer/templates/objects/compound_viz.html @@ -1,4 +1,11 @@ {% if compound.default_structure.pes_link %} + +
+ +
Link to PES
+
{{ compound.default_structure.pes_link }}
+
+
diff --git a/bayer/templates/objects/node_viz.html b/bayer/templates/objects/node_viz.html index 16b47afc..4426726b 100644 --- a/bayer/templates/objects/node_viz.html +++ b/bayer/templates/objects/node_viz.html @@ -1,4 +1,11 @@ {% if node.default_node_label.pes_link %} + +
+ +
Link to PES
+
{{ node.default_node_label.pes_link }}
+
+
diff --git a/bayer/templates/objects/package.html b/bayer/templates/objects/package.html index 803ec23b..c03be1d2 100644 --- a/bayer/templates/objects/package.html +++ b/bayer/templates/objects/package.html @@ -1,5 +1,5 @@ {% extends "framework_modern.html" %} - +{% load static %} {% block content %} {% block action_modals %} @@ -16,7 +16,7 @@
-

{{ package.name }} - ({{ package.get_classification_level_display }})

+

{{ package.name }} {% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}{% endif %}