This commit is contained in:
Tim Lorsbach
2026-04-17 19:39:54 +02:00
parent ca0508d96a
commit d1a00f71b4
19 changed files with 412 additions and 115 deletions

View File

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

View File

@ -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",
)

View File

@ -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',),
),
]

View File

@ -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,
),
]

View File

@ -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"
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)}"
}

View File

@ -90,7 +90,7 @@
<form
id="new-pes-modal-form"
accept-charset="UTF-8"
action="{% url 'package compound list' meta.current_package.uuid %}"
action="{% url 'create pes' meta.current_package.uuid %}"
method="post"
>
{% csrf_token %}
@ -129,9 +129,10 @@
name="pes-link"
type="text"
class="input input-bordered w-full"
placeholder="Link to PES"
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
x-model="pesLink"
@input="updatePesViz()"
required
/>
</div>

View File

@ -0,0 +1,12 @@
{% if compound_structure.pes_link %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
<div class="collapse-content">
<div class="flex justify-center">
<img src='{% url 'depict_pes' %}?pesLink={{ compound_structure.pes_link|urlencode }}'/>
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,12 @@
{% if compound.default_structure.pes_link %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
<div class="collapse-content">
<div class="flex justify-center">
<img src='{% url 'depict_pes' %}?pesLink={{ compound.default_structure.pes_link|urlencode }}'/>
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,12 @@
{% if node.default_node_label.pes_link %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
<div class="collapse-content">
<div class="flex justify-center">
<img src='{% url 'depict_pes' %}?pesLink={{ node.default_node_label.pes_link|urlencode }}'/>
</div>
</div>
</div>
{% endif %}

View File

@ -2,7 +2,13 @@ from django.urls import re_path
from . import views as v
UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
urlpatterns = [
re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound$",
v.create_pes,
name="create pes",
),
]

View File

@ -1,13 +1,41 @@
import base64
import requests
from django.conf import settings as s
from django.core.exceptions import BadRequest
from django.http import HttpResponse
from pydantic import BaseModel
from django.shortcuts import redirect
from utilities.chem import FormatConverter
from bayer.models import PESCompound
from epdb.logic import PackageManager
from epdb.views import _anonymous_or_real
from utilities.decorators import package_permission_required
class PES(BaseModel):
pass
@package_permission_required()
def create_pes(request, package_uuid):
current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
if request.method == "POST":
compound_name = request.POST.get('compound-name')
compound_description = request.POST.get('compound-description')
pes_link = request.POST.get('pes-link')
if pes_link:
try:
pes_data = fetch_pes(request, pes_link)
except ValueError as e:
return BadRequest(f"Could not fetch PES data for {pes_link}")
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
return redirect(pes.url)
else:
return BadRequest("Please provide a PES link.")
else:
pass
def fetch_pes(request, pes_url) -> dict:
@ -19,28 +47,35 @@ def fetch_pes(request, pes_url) -> dict:
from epauth.views import get_access_token_from_request
token = get_access_token_from_request(request)
if token:
if token or True:
for k, v in s.PES_API_MAPPING.items():
if pes_url.startsWith(k):
if pes_url.startswith(k):
pes_id = pes_url.split('/')[-1]
headers = {"Authorization": f"Bearer {token['access_token']}"}
params = {"pes_reg_entity_corporate_id": pes_id}
res = requests.get(v, headers=headers, params=params, proxies=proxies)
try:
res.raise_for_status()
pes_data = res.json()
if len(pes_data) == 0:
raise ValueError(f"PES with id {pes_id} not found")
res_data = pes_data[0]
if pes_id == 'dummy' or True:
import json
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
res_data["pes_url"] = pes_url
return res_data
else:
headers = {"Authorization": f"Bearer {token['access_token']}"}
params = {"pes_reg_entity_corporate_id": pes_id}
except requests.exceptions.HTTPError as e:
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
res = requests.get(v, headers=headers, params=params, proxies=proxies)
try:
res.raise_for_status()
pes_data = res.json()
if len(pes_data) == 0:
raise ValueError(f"PES with id {pes_id} not found")
res_data = pes_data[0]
res_data["pes_url"] = pes_url
return res_data
except requests.exceptions.HTTPError as e:
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
else:
raise ValueError(f"Unknown URL {pes_url}")
else:
@ -52,8 +87,10 @@ def visualize_pes(request):
if pes_link:
pes_data = fetch_pes(request, pes_link)
print(pes_data)
return HttpResponse(
FormatConverter.to_png("c1ccccc1"), content_type="image/png"
)
representations = pes_data.get('representations')
for rep in representations:
if rep.get('type') == 'color':
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
return HttpResponse(image_data, content_type="image/png")