diff --git a/bayer/admin.py b/bayer/admin.py index 8c38f3f3..f0b257bb 100644 --- a/bayer/admin.py +++ b/bayer/admin.py @@ -1,3 +1,19 @@ from django.contrib import admin # Register your models here. +from .models import ( + PESCompound, + PESStructure +) + + +class PESCompoundAdmin(admin.ModelAdmin): + pass + + +class PESStructureAdmin(admin.ModelAdmin): + pass + + +admin.site.register(PESCompound, PESCompoundAdmin) +admin.site.register(PESStructure, PESStructureAdmin) diff --git a/bayer/epdb_hooks.py b/bayer/epdb_hooks.py index b843782b..a1a237f5 100644 --- a/bayer/epdb_hooks.py +++ b/bayer/epdb_hooks.py @@ -4,6 +4,7 @@ from epdb.template_registry import register_template logger = logging.getLogger(__name__) +# PES Create register_template( "epdb.actions.collections.compound", "actions/collections/new_pes.html", @@ -13,3 +14,18 @@ register_template( "modals/collections/new_pes_modal.html", ) +# PES Viz +register_template( + "epdb.objects.compound.viz", + "objects/compound_viz.html", +) + +register_template( + "epdb.objects.compound_structure.viz", + "objects/compound_structure_viz.html", +) + +register_template( + "epdb.objects.node.viz", + "objects/node_viz.html", +) \ No newline at end of file diff --git a/bayer/migrations/0004_pescompound_pesstructure.py b/bayer/migrations/0004_pescompound_pesstructure.py new file mode 100644 index 00000000..b2a8f343 --- /dev/null +++ b/bayer/migrations/0004_pescompound_pesstructure.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.3 on 2026-04-15 20:03 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bayer', '0003_package_data_pool'), + ('epdb', '0023_alter_compoundstructure_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PESCompound', + fields=[ + ('compound_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compound')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.compound',), + ), + migrations.CreateModel( + 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')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.compoundstructure',), + ), + ] diff --git a/bayer/migrations/0005_pesstructure_pes_link.py b/bayer/migrations/0005_pesstructure_pes_link.py new file mode 100644 index 00000000..072b349b --- /dev/null +++ b/bayer/migrations/0005_pesstructure_pes_link.py @@ -0,0 +1,19 @@ +# 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 aa075692..53bffc16 100644 --- a/bayer/models.py +++ b/bayer/models.py @@ -1,11 +1,15 @@ from typing import List - +import urllib.parse +import nh3 from django.conf import settings as s -from django.db import models +from django.db import models, transaction from django.db.models import QuerySet +from django.urls import reverse from epdb.models import ( EnviPathModel, + Compound, + CompoundStructure, ParallelRule, SequentialRule, SimpleAmbitRule, @@ -95,4 +99,70 @@ class Package(EnviPathModel): return rules class Meta: - db_table = "epdb_package" \ No newline at end of file + db_table = "epdb_package" + + +class PESCompound(Compound): + + @staticmethod + @transaction.atomic + def create( + package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs + ) -> "Compound": + + pes_url = pes_data["pes_url"] + + # 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 + + # Generate Compound + c = PESCompound() + c.package = package + + if name is not None: + # Clean for potential XSS + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + + if name is None or name == "": + name = f"Compound {Compound.objects.filter(package=package).count() + 1}" + + c.name = name + + # We have a default here only set the value if it carries some payload + if description is not None and description.strip() != "": + c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() + + c.save() + + is_standardized = standardized_smiles == smiles + + 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, normalized_structure=is_standardized + ) + + c.default_structure = cs + c.save() + + return c + + +class PESStructure(CompoundStructure): + pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link") + + def d3_json(self): + return { + "is_pes": True, + "pes_link": self.pes_link, + # Will overwrite image from Node + "image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}" + } diff --git a/bayer/templates/actions/objects/compound.html b/bayer/templates/actions/objects/compound.html deleted file mode 100644 index e69de29b..00000000 diff --git a/bayer/templates/modals/collections/new_pes_modal.html b/bayer/templates/modals/collections/new_pes_modal.html index 2cf11949..7727697c 100644 --- a/bayer/templates/modals/collections/new_pes_modal.html +++ b/bayer/templates/modals/collections/new_pes_modal.html @@ -90,7 +90,7 @@