From 2e246667440e3f3b50e8bc4d2dd56211f034516f Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Tue, 21 Apr 2026 10:26:35 +0200 Subject: [PATCH] wip --- bayer/migrations/0003_package_data_pool.py | 20 -- ...ompound_pesstructure_package_data_pool.py} | 10 +- .../migrations/0005_pesstructure_pes_link.py | 19 -- bayer/models.py | 76 +++++++- .../modals/collections/new_pes_modal.html | 2 +- .../objects/add_pathway_pes_node_modal.html | 174 ++++++++++++++++++ .../objects/compound_structure_viz.html | 7 + bayer/templates/objects/compound_viz.html | 7 + bayer/templates/objects/node_viz.html | 7 + bayer/templates/objects/package.html | 4 +- bayer/templates/static/login.html | 17 +- bayer/tests/__init__.py | 0 bayer/tests/pes/__init__.py | 0 bayer/tests/pes/test_pes.py | 174 ++++++++++++++++++ bayer/urls.py | 7 +- bayer/views.py | 64 +++++++ epdb/legacy_api.py | 8 +- epdb/logic.py | 53 +++++- ...lter_compoundstructure_options_and_more.py | 5 + epdb/models.py | 23 ++- static/images/Restricted.gif | Bin 0 -> 1577 bytes static/images/bayer-logo.svg | 30 +++ static/images/restricted_mid.png | Bin 0 -> 2243 bytes static/images/secret_mid.png | Bin 0 -> 1490 bytes static/images/secret_small.png | Bin 0 -> 3226 bytes templates/actions/objects/pathway.html | 8 + templates/components/navbar.html | 6 + templates/objects/pathway.html | 1 + 28 files changed, 653 insertions(+), 69 deletions(-) delete mode 100644 bayer/migrations/0003_package_data_pool.py rename bayer/migrations/{0004_pescompound_pesstructure.py => 0003_pescompound_pesstructure_package_data_pool.py} (72%) delete mode 100644 bayer/migrations/0005_pesstructure_pes_link.py create mode 100644 bayer/templates/modals/objects/add_pathway_pes_node_modal.html create mode 100644 bayer/tests/__init__.py create mode 100644 bayer/tests/pes/__init__.py create mode 100644 bayer/tests/pes/test_pes.py create mode 100644 static/images/Restricted.gif create mode 100644 static/images/bayer-logo.svg create mode 100644 static/images/restricted_mid.png create mode 100644 static/images/secret_mid.png create mode 100644 static/images/secret_small.png 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 %}