forked from enviPath/enviPy
wip
This commit is contained in:
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bayer', '0003_package_data_pool'),
|
('bayer', '0002_initial'),
|
||||||
('epdb', '0023_alter_compoundstructure_options_and_more'),
|
('epdb', '0023_alter_compoundstructure_options_and_more'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -26,10 +26,16 @@ class Migration(migrations.Migration):
|
|||||||
name='PESStructure',
|
name='PESStructure',
|
||||||
fields=[
|
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')),
|
('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={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
},
|
},
|
||||||
bases=('epdb.compoundstructure',),
|
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'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -15,6 +15,7 @@ from epdb.models import (
|
|||||||
SimpleAmbitRule,
|
SimpleAmbitRule,
|
||||||
SimpleRDKitRule,
|
SimpleRDKitRule,
|
||||||
)
|
)
|
||||||
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
|
|
||||||
class Package(EnviPathModel):
|
class Package(EnviPathModel):
|
||||||
@ -114,7 +115,9 @@ class PESCompound(Compound):
|
|||||||
|
|
||||||
# Check if we find a direct match for a given pes_link
|
# Check if we find a direct match for a given pes_link
|
||||||
if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists():
|
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
|
# Generate Compound
|
||||||
c = PESCompound()
|
c = PESCompound()
|
||||||
@ -135,19 +138,37 @@ class PESCompound(Compound):
|
|||||||
|
|
||||||
c.save()
|
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
|
is_standardized = standardized_smiles == smiles
|
||||||
|
|
||||||
if not is_standardized:
|
if not is_standardized:
|
||||||
_ = CompoundStructure.create(
|
_ = PESStructure.create(
|
||||||
c,
|
c,
|
||||||
|
pes_url,
|
||||||
|
molfile,
|
||||||
standardized_smiles,
|
standardized_smiles,
|
||||||
name="Normalized structure of {}".format(name),
|
name="Normalized structure of {}".format(name),
|
||||||
description="{} (in its normalized form)".format(description),
|
description="{} (in its normalized form)".format(description),
|
||||||
normalized_structure=True,
|
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
|
c.default_structure = cs
|
||||||
@ -159,6 +180,53 @@ class PESCompound(Compound):
|
|||||||
class PESStructure(CompoundStructure):
|
class PESStructure(CompoundStructure):
|
||||||
pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link")
|
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):
|
def d3_json(self):
|
||||||
return {
|
return {
|
||||||
"is_pes": True,
|
"is_pes": True,
|
||||||
|
|||||||
@ -154,7 +154,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@click="submit('new-compound-modal-form')"
|
@click="submit('new-pes-modal-form')"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
<span x-show="!isSubmitting">Submit</span>
|
<span x-show="!isSubmitting">Submit</span>
|
||||||
|
|||||||
174
bayer/templates/modals/objects/add_pathway_pes_node_modal.html
Normal file
174
bayer/templates/modals/objects/add_pathway_pes_node_modal.html
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="add_pathway_pes_node_modal"
|
||||||
|
class="modal"
|
||||||
|
x-data="{
|
||||||
|
isSubmitting: false,
|
||||||
|
pesLink: null,
|
||||||
|
pesVizHtml: '',
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
get isPESSet() {
|
||||||
|
console.log(this.pesLink);
|
||||||
|
return this.pesLink !== null;
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePesViz() {
|
||||||
|
if (!this.isPESSet) {
|
||||||
|
this.pesVizHtml = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.objectFit = 'cover';
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
this.pesVizHtml = img.outerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
this.pesVizHtml = `
|
||||||
|
<div class='alert alert-error' role='alert'>
|
||||||
|
<h4 class='alert-heading'>Could not render PES!</h4>
|
||||||
|
<p>Could not render PES - Do you have access?</p>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
submit(formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
|
||||||
|
// Remove previously injected inputs
|
||||||
|
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Add values from dynamic form into the html form
|
||||||
|
if (this.formData) {
|
||||||
|
Object.entries(this.formData).forEach(([key, value]) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = key;
|
||||||
|
input.value = value;
|
||||||
|
input.classList.add('dynamic-param');
|
||||||
|
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form && form.checkValidity()) {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
form.submit();
|
||||||
|
} else if (form) {
|
||||||
|
form.reportValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@close="reset()"
|
||||||
|
>
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<h3 class="text-lg font-bold">New PES</h3>
|
||||||
|
|
||||||
|
<!-- Close button (X) -->
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="py-4">
|
||||||
|
<form
|
||||||
|
id="new-pes-node-modal-form"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
action="{% url 'create pes node' current_object.package.uuid current_object.uuid %}"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-name">
|
||||||
|
<span class="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-name"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-name"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-description">
|
||||||
|
<span class="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-description"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-description"
|
||||||
|
placeholder="Description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="pes-link">
|
||||||
|
<span class="label-text">Link to PES</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="pes-link"
|
||||||
|
name="pes-link"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
||||||
|
x-model="pesLink"
|
||||||
|
@input="updatePesViz()"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-action">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick="this.closest('dialog').close()"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="submit('new-pes-node-modal-form')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
<span x-show="!isSubmitting">Submit</span>
|
||||||
|
<span
|
||||||
|
x-show="isSubmitting"
|
||||||
|
class="loading loading-spinner loading-sm"
|
||||||
|
></span>
|
||||||
|
<span x-show="isSubmitting">Creating...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button :disabled="isSubmitting">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
@ -1,4 +1,11 @@
|
|||||||
{% if compound_structure.pes_link %}
|
{% if compound_structure.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
{% if compound.default_structure.pes_link %}
|
{% if compound.default_structure.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
{% if node.default_node_label.pes_link %}
|
{% if node.default_node_label.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework_modern.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="card bg-base-100">
|
<div class="card bg-base-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="card-title text-2xl">{{ package.name }} - ({{ package.get_classification_level_display }})</h2>
|
<h2 class="card-title text-2xl">{{ package.name }} {% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}<img src="{% static 'images/restricted_mid.png' %}" width="100">{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}<img src="{% static 'images/secret_mid.png' %}" width="60">{% endif %}</h2>
|
||||||
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
|
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "static/login_base.html" %}
|
{% extends "static/login_base.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block title %}enviPath - Sign In{% endblock %}
|
{% block title %}enviPath - Sign In{% endblock %}
|
||||||
|
|
||||||
{% block extra_styles %}
|
{% block extra_styles %}
|
||||||
@ -31,13 +31,18 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card bg-base-200 mb-6 ">
|
<div>
|
||||||
<div class="card-body">
|
<img src="{% static 'images/bayer-logo.svg' %}">
|
||||||
<h3 class="card-title">Welcome to the new enviPath!</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col space-y-4 ...">
|
||||||
|
<div><p></p></div>
|
||||||
|
<div><p></p></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<div class="border-base-300 mb-6 border-b">
|
<div class="border-base-300 mb-6 border-b" hidden>
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|||||||
0
bayer/tests/__init__.py
Normal file
0
bayer/tests/__init__.py
Normal file
0
bayer/tests/pes/__init__.py
Normal file
0
bayer/tests/pes/__init__.py
Normal file
174
bayer/tests/pes/test_pes.py
Normal file
174
bayer/tests/pes/test_pes.py
Normal file
File diff suppressed because one or more lines are too long
@ -7,8 +7,13 @@ UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"),
|
re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"),
|
||||||
re_path(
|
re_path(
|
||||||
rf"^package/(?P<package_uuid>{UUID})/compound$",
|
rf"^package/(?P<package_uuid>{UUID})/pes$",
|
||||||
v.create_pes,
|
v.create_pes,
|
||||||
name="create pes",
|
name="create pes",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/pes$",
|
||||||
|
v.create_pes_node,
|
||||||
|
name="create pes node",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -8,9 +8,12 @@ from django.shortcuts import redirect
|
|||||||
|
|
||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import Pathway, Node
|
||||||
from epdb.views import _anonymous_or_real
|
from epdb.views import _anonymous_or_real
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
@package_permission_required()
|
@package_permission_required()
|
||||||
def create_pes(request, package_uuid):
|
def create_pes(request, package_uuid):
|
||||||
@ -18,6 +21,10 @@ def create_pes(request, package_uuid):
|
|||||||
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
compound_name = request.POST.get('compound-name')
|
compound_name = request.POST.get('compound-name')
|
||||||
compound_description = request.POST.get('compound-description')
|
compound_description = request.POST.get('compound-description')
|
||||||
pes_link = request.POST.get('pes-link')
|
pes_link = request.POST.get('pes-link')
|
||||||
@ -28,6 +35,14 @@ def create_pes(request, package_uuid):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
return redirect(pes.url)
|
return redirect(pes.url)
|
||||||
@ -37,6 +52,55 @@ def create_pes(request, package_uuid):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@package_permission_required()
|
||||||
|
def create_pes_node(request, package_uuid, pathway_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
|
n = Node()
|
||||||
|
n.stereo_removed = False
|
||||||
|
n.pathway = current_pathway
|
||||||
|
n.depth = 0
|
||||||
|
|
||||||
|
n.default_node_label = pes.default_structure
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
n.node_labels.add(pes.default_structure)
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return BadRequest("Please provide a PES link.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def fetch_pes(request, pes_url) -> dict:
|
def fetch_pes(request, pes_url) -> dict:
|
||||||
proxies = {
|
proxies = {
|
||||||
|
|||||||
@ -1958,13 +1958,16 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
educts = []
|
educts = []
|
||||||
products = []
|
products = []
|
||||||
|
|
||||||
|
subclasses = CompoundStructure.__subclasses__()
|
||||||
|
|
||||||
if e.edgeAsSmirks:
|
if e.edgeAsSmirks:
|
||||||
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
||||||
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
||||||
educts.append(
|
educts.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.get(
|
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||||
|
get(
|
||||||
compound__package=p, smiles=stand_ed
|
compound__package=p, smiles=stand_ed
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
@ -1975,7 +1978,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
products.append(
|
products.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.get(
|
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||||
|
get(
|
||||||
compound__package=p, smiles=stand_pr
|
compound__package=p, smiles=stand_pr
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import nh3
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import QuerySet
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
@ -364,6 +365,14 @@ class PackageManager(object):
|
|||||||
|
|
||||||
groups = GroupManager.get_groups(user)
|
groups = GroupManager.get_groups(user)
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
|
||||||
|
if package.classification_level == Package.Classification.SECRET:
|
||||||
|
if package.data_pool not in groups:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
||||||
|
|
||||||
valid_perms = perms.get(permission)
|
valid_perms = perms.get(permission)
|
||||||
@ -406,6 +415,7 @@ class PackageManager(object):
|
|||||||
try:
|
try:
|
||||||
p = Package.objects.get(uuid=package_id)
|
p = Package.objects.get(uuid=package_id)
|
||||||
if PackageManager.readable(user, p):
|
if PackageManager.readable(user, p):
|
||||||
|
p = PackageManager.check_package_classification(user, p)
|
||||||
return p
|
return p
|
||||||
else:
|
else:
|
||||||
# FIXME: use custom exception to be translatable to 403 in API
|
# FIXME: use custom exception to be translatable to 403 in API
|
||||||
@ -415,6 +425,37 @@ class PackageManager(object):
|
|||||||
except Package.DoesNotExist:
|
except Package.DoesNotExist:
|
||||||
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_package_classification(user, pack: Package):
|
||||||
|
if pack.classification_level == Package.Classification.SECRET:
|
||||||
|
if pack.data_pool.user_member.filter(id=user.id).exists():
|
||||||
|
return pack
|
||||||
|
|
||||||
|
raise ValueError("Package is secret and not accessible to user!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return pack
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_package_classifications(user, package_qs: QuerySet[Package]):
|
||||||
|
non_secret = package_qs.exclude(classification_level=Package.Classification.SECRET)
|
||||||
|
secret = package_qs.filter(classification_level=Package.Classification.SECRET)
|
||||||
|
|
||||||
|
# TODO we should be able to do via the db
|
||||||
|
accessible_secret = []
|
||||||
|
|
||||||
|
for s_package in secret:
|
||||||
|
if s_package.data_pool.user_member.filter(id=user.id).exists():
|
||||||
|
accessible_secret.append(s_package.pk)
|
||||||
|
|
||||||
|
# Cannot combine a unique query with a non-unique query -> we have to call distinct
|
||||||
|
return Package.objects.filter(pk__in=accessible_secret).distinct() | non_secret.distinct()
|
||||||
|
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_readable_packages(user, include_reviewed=False):
|
def get_all_readable_packages(user, include_reviewed=False):
|
||||||
# UserPermission only exists if at least read is granted...
|
# UserPermission only exists if at least read is granted...
|
||||||
@ -441,6 +482,10 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
qs = PackageManager.check_package_classifications(user, qs)
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -487,11 +532,11 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
return qs
|
# EDIT START
|
||||||
|
qs = PackageManager.check_package_classifications(user, qs)
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
@staticmethod
|
return qs
|
||||||
def get_packages():
|
|
||||||
return Package.objects.all()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|||||||
@ -46,4 +46,9 @@ class Migration(migrations.Migration):
|
|||||||
name="molfile",
|
name="molfile",
|
||||||
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="group",
|
||||||
|
name="secret",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="Secret Group"),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -867,18 +867,25 @@ class Compound(
|
|||||||
|
|
||||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
|
|
||||||
|
subclasses = CompoundStructure.__subclasses__()
|
||||||
|
|
||||||
|
qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package)
|
||||||
|
if subclasses:
|
||||||
|
qs = qs.not_instance_of(*subclasses)
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES
|
# Check if we find a direct match for a given SMILES
|
||||||
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
|
if qs.exists():
|
||||||
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
|
return qs.first().compound
|
||||||
|
|
||||||
|
|
||||||
|
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||||
|
if subclasses:
|
||||||
|
qs = qs.not_instance_of(*subclasses)
|
||||||
|
|
||||||
# Check if we can find the standardized one
|
# Check if we can find the standardized one
|
||||||
if CompoundStructure.objects.filter(
|
if qs.exists():
|
||||||
smiles=standardized_smiles, compound__package=package
|
|
||||||
).exists():
|
|
||||||
# TODO should we add a structure?
|
# TODO should we add a structure?
|
||||||
return CompoundStructure.objects.get(
|
return qs.first().compound
|
||||||
smiles=standardized_smiles, compound__package=package
|
|
||||||
).compound
|
|
||||||
|
|
||||||
# Generate Compound
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
|
|||||||
BIN
static/images/Restricted.gif
Normal file
BIN
static/images/Restricted.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
30
static/images/bayer-logo.svg
Normal file
30
static/images/bayer-logo.svg
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 76 76" style="enable-background:new 0 0 76 76;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#10384F;}
|
||||||
|
.st1{fill:#89D329;}
|
||||||
|
.st2{fill:#00BCFF;}
|
||||||
|
</style>
|
||||||
|
<g id="Bayer_Cross_1_">
|
||||||
|
<path class="st0" d="M35.9,11.3h4.4c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9h-4.4V11.3z M35.9,15.5h4.5c0.6,0,1-0.4,1-1
|
||||||
|
c0-0.6-0.4-1-1-1h-4.5V15.5z M43,12.3c0.6,0.6,1,1.4,1,2.3c0,1.8-1.4,3.2-3.2,3.2h-7.3V7.3l7.2,0c1.7,0,3.1,1.4,3.1,3.1
|
||||||
|
C43.7,11.1,43.4,11.8,43,12.3z M44.7,30.3H42l-0.8-1.8h-5.9l-0.8,1.8h-2.7L37,19.8h2.4L44.7,30.3z M38.2,22.5l-1.8,3.8H40
|
||||||
|
L38.2,22.5z M41.8,32.6h3l-5.3,6.8v3.7h-2.5v-3.7l-5.3-6.8h3l3.6,4.8L41.8,32.6z M55.7,32.6v2.3h-7v1.8l6.8,0v2.3h-6.8v2h7v2.3
|
||||||
|
h-9.5V32.6H55.7z M63.4,39.1h-1.9v4h-2.5V32.6h6.4c1.8,0,3.2,1.5,3.2,3.3c0,1.5-1,2.7-2.3,3.1l3.1,4.1h-3L63.4,39.1z M65.2,34.8
|
||||||
|
h-3.6v2h3.6c0.6,0,1-0.5,1-1C66.2,35.3,65.7,34.8,65.2,34.8z M32.8,43.1h-2.7l-0.8-1.8h-5.9l-0.8,1.8h-2.7l5.3-10.5h2.4L32.8,43.1z
|
||||||
|
M26.3,35.3l-1.8,3.8h3.7L26.3,35.3z M10.4,36.6h4.4c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9l-4.4,0V36.6z M10.4,40.8h4.5
|
||||||
|
c0.6,0,1-0.4,1-1c0-0.6-0.4-1-1-1h-4.5V40.8z M17.5,37.6c0.6,0.6,1,1.4,1,2.3c0,1.8-1.4,3.2-3.2,3.2H7.9V32.6h7.2
|
||||||
|
c1.7,0,3.1,1.4,3.1,3.1C18.2,36.4,17.9,37.1,17.5,37.6z M43,45.3v2.3h-7v1.8l6.8,0v2.3h-6.8v2h7v2.3h-9.5V45.3H43z M41.2,61.6
|
||||||
|
c0-0.6-0.4-1-1-1h-4.3v2h4.3C40.8,62.6,41.2,62.2,41.2,61.6z M33.4,68.9V58.4h7c1.8,0,3.2,1.5,3.2,3.3c0,1.4-0.8,2.5-2,3l3.2,4.2
|
||||||
|
h-3l-3-4h-2.9v4H33.4z"/>
|
||||||
|
<path class="st1" d="M76.1,35.6C74.9,15.8,58.4,0,38.2,0C18,0,1.5,15.8,0.3,35.6c0,0.8,0.1,1.6,0.2,2.4c0.8,6.6,3.3,12.7,7.1,17.8
|
||||||
|
c6.9,9.4,18,15.5,30.6,15.5c-17.6,0-32-13.7-33.2-30.9c-0.1-0.8-0.1-1.6-0.1-2.4c0-0.8,0-1.6,0.1-2.4C6.2,18.4,20.6,4.7,38.2,4.7
|
||||||
|
c12.6,0,23.7,6.1,30.6,15.5c3.8,5.1,6.3,11.2,7.1,17.8c0.1,0.8,0.2,1.6,0.2,2.3c0-0.8,0.1-1.6,0.1-2.4
|
||||||
|
C76.2,37.2,76.2,36.4,76.1,35.6"/>
|
||||||
|
<path class="st2" d="M0.3,40.4C1.5,60.2,18,76,38.2,76c20.2,0,36.7-15.8,37.9-35.6c0-0.8-0.1-1.6-0.2-2.4
|
||||||
|
c-0.8-6.6-3.3-12.7-7.1-17.8c-6.9-9.4-18-15.5-30.6-15.5c17.6,0,32,13.7,33.2,30.9c0.1,0.8,0.1,1.6,0.1,2.4c0,0.8,0,1.6-0.1,2.4
|
||||||
|
c-1.2,17.3-15.6,30.9-33.2,30.9c-12.6,0-23.7-6.1-30.6-15.5C3.8,50.7,1.3,44.6,0.5,38c-0.1-0.8-0.2-1.6-0.2-2.3
|
||||||
|
c0,0.8-0.1,1.6-0.1,2.4C0.2,38.8,0.2,39.6,0.3,40.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/restricted_mid.png
Normal file
BIN
static/images/restricted_mid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/images/secret_mid.png
Normal file
BIN
static/images/secret_mid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/secret_small.png
Normal file
BIN
static/images/secret_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@ -9,6 +9,14 @@
|
|||||||
<i class="glyphicon glyphicon-plus"></i> Add Compound</a
|
<i class="glyphicon glyphicon-plus"></i> Add Compound</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
|
||||||
|
>
|
||||||
|
<i class="glyphicon glyphicon-plus"></i> Add PES</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
|
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<img src="{% static 'images/bayer-logo.svg' %}" width="40">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not public_mode %}
|
{% if not public_mode %}
|
||||||
@ -100,6 +101,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
{% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}
|
||||||
|
<img src="{% static 'images/restricted_mid.png' %}" width="200">
|
||||||
|
{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}
|
||||||
|
<img src="{% static 'images/secret_mid.png' %}" width="120">
|
||||||
|
{% endif %}
|
||||||
{% if not public_mode %}
|
{% if not public_mode %}
|
||||||
<a id="search-trigger" role="button" class="cursor-pointer">
|
<a id="search-trigger" role="button" class="cursor-pointer">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -77,6 +77,7 @@
|
|||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
{% include "modals/objects/add_pathway_node_modal.html" %}
|
{% include "modals/objects/add_pathway_node_modal.html" %}
|
||||||
|
{% include "modals/objects/add_pathway_pes_node_modal.html" %}
|
||||||
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||||
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
|
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
|
||||||
{% for tpl in add_templates %}
|
{% for tpl in add_templates %}
|
||||||
|
|||||||
Reference in New Issue
Block a user