forked from enviPath/enviPy
Compare commits
27 Commits
dd0f7eaf05
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 72a63b4876 | |||
| 2c2437e3f5 | |||
| 9bc9f86ff1 | |||
| dba6514013 | |||
| a092d4a558 | |||
| 2502c020f7 | |||
| 6ab9180291 | |||
| 3657d14659 | |||
| ef6091d416 | |||
| 14cfc1e4d7 | |||
| 868bbf5c05 | |||
| be5ee1d1d7 | |||
| 20fd949dfd | |||
| c9b643fe6e | |||
| 1a9f1cf9af | |||
| c7c7e17e43 | |||
| 674e10c7fa | |||
| 8079b80d57 | |||
| 76e63fda2c | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e | |||
| 9d70db2ca2 | |||
| fec26d0233 | |||
| 689f7998eb | |||
| 8498e59fa1 |
19
Dockerfile
19
Dockerfile
@ -6,18 +6,23 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
curl \
|
curl \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
git \
|
git \
|
||||||
nodejs \
|
ca-certificates \
|
||||||
npm \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install pnpm
|
# Install Node 22 + pnpm
|
||||||
RUN npm install -g pnpm
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& corepack enable \
|
||||||
|
&& corepack prepare pnpm@latest --activate \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
ENV PATH="/root/.local/bin:${PATH}"
|
ENV PATH="/root/.local/bin:${PATH}"
|
||||||
@ -32,17 +37,17 @@ RUN mkdir -p -m 0700 /root/.ssh \
|
|||||||
# We'll need access to private repos, let docker make use of host ssh agent and use it like:
|
# We'll need access to private repos, let docker make use of host ssh agent and use it like:
|
||||||
# docker build --ssh default -t envipath/envipy:1.0 .
|
# docker build --ssh default -t envipath/envipy:1.0 .
|
||||||
RUN --mount=type=ssh \
|
RUN --mount=type=ssh \
|
||||||
uv sync --locked --extra ms-login --extra pepper-plugin
|
uv sync --locked --extra pepper-plugin
|
||||||
|
|
||||||
# Now copy source and do a final sync to install the project itself
|
# Now copy source and do a final sync to install the project itself
|
||||||
# Ensure .dockerignore is reasonable
|
# Ensure .dockerignore is reasonable
|
||||||
COPY bayer bayer
|
|
||||||
COPY bridge bridge
|
|
||||||
COPY biotransformer biotransformer
|
COPY biotransformer biotransformer
|
||||||
|
COPY bridge bridge
|
||||||
COPY envipath envipath
|
COPY envipath envipath
|
||||||
COPY epapi epapi
|
COPY epapi epapi
|
||||||
COPY epauth epauth
|
COPY epauth epauth
|
||||||
COPY epdb epdb
|
COPY epdb epdb
|
||||||
|
COPY epiuclid epiuclid
|
||||||
COPY fixtures fixtures
|
COPY fixtures fixtures
|
||||||
COPY migration migration
|
COPY migration migration
|
||||||
COPY pepper pepper
|
COPY pepper pepper
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BayerConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'bayer'
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
|
||||||
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Package',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
|
||||||
('classification_level', models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted'), (20, 'Secret')], default=10)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'db_table': 'epdb_package',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('bayer', '0001_initial'),
|
|
||||||
('epdb', '0019_remove_scenario_additional_information_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='package',
|
|
||||||
name='license',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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,98 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from django.conf import settings as s
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models import QuerySet
|
|
||||||
|
|
||||||
from epdb.models import (
|
|
||||||
EnviPathModel,
|
|
||||||
ParallelRule,
|
|
||||||
SequentialRule,
|
|
||||||
SimpleAmbitRule,
|
|
||||||
SimpleRDKitRule,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Package(EnviPathModel):
|
|
||||||
reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False)
|
|
||||||
license = models.ForeignKey(
|
|
||||||
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Classification(models.IntegerChoices):
|
|
||||||
INTERNAL = 0, "Internal"
|
|
||||||
RESTRICTED = 10 , "Restricted"
|
|
||||||
SECRET = 20, "Secret"
|
|
||||||
|
|
||||||
classification_level = models.IntegerField(
|
|
||||||
choices=Classification,
|
|
||||||
default=Classification.RESTRICTED,
|
|
||||||
)
|
|
||||||
|
|
||||||
data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True,
|
|
||||||
verbose_name="Data pool", default=None)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
# explicitly handle related Rules
|
|
||||||
for r in self.rules.all():
|
|
||||||
r.delete()
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name} (pk={self.pk})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def compounds(self) -> QuerySet:
|
|
||||||
return self.compound_set.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rules(self) -> QuerySet:
|
|
||||||
return self.rule_set.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reactions(self) -> QuerySet:
|
|
||||||
return self.reaction_set.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pathways(self) -> QuerySet:
|
|
||||||
return self.pathway_set.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scenarios(self) -> QuerySet:
|
|
||||||
return self.scenario_set.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def models(self) -> QuerySet:
|
|
||||||
return self.epmodel_set.all()
|
|
||||||
|
|
||||||
def _url(self):
|
|
||||||
return "{}/package/{}".format(s.SERVER_URL, self.uuid)
|
|
||||||
|
|
||||||
def get_applicable_rules(self) -> List["Rule"]:
|
|
||||||
"""
|
|
||||||
Returns a ordered set of rules where the following applies:
|
|
||||||
1. All Composite will be added to result
|
|
||||||
2. All SimpleRules will be added if theres no CompositeRule present using the SimpleRule
|
|
||||||
Ordering is based on "url" field.
|
|
||||||
"""
|
|
||||||
rules = []
|
|
||||||
rule_qs = self.rules
|
|
||||||
|
|
||||||
reflected_simple_rules = set()
|
|
||||||
|
|
||||||
for r in rule_qs:
|
|
||||||
if isinstance(r, ParallelRule) or isinstance(r, SequentialRule):
|
|
||||||
rules.append(r)
|
|
||||||
for sr in r.simple_rules.all():
|
|
||||||
reflected_simple_rules.add(sr)
|
|
||||||
|
|
||||||
for r in rule_qs:
|
|
||||||
if isinstance(r, SimpleAmbitRule) or isinstance(r, SimpleRDKitRule):
|
|
||||||
if r not in reflected_simple_rules:
|
|
||||||
rules.append(r)
|
|
||||||
|
|
||||||
rules = sorted(rules, key=lambda x: x.url)
|
|
||||||
return rules
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
db_table = "epdb_package"
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
|
|
||||||
<dialog
|
|
||||||
id="new_package_modal"
|
|
||||||
class="modal"
|
|
||||||
x-data="{
|
|
||||||
isSubmitting: false,
|
|
||||||
packageClassification: null,
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.isSubmitting = false;
|
|
||||||
this.selectedType = '';
|
|
||||||
this.buildAppDomain = false;
|
|
||||||
this.requiresRulePackages = false;
|
|
||||||
this.requiresDataPackages = false;
|
|
||||||
this.additional_parameters = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
setFormData(data) {
|
|
||||||
this.formData = data;
|
|
||||||
},
|
|
||||||
|
|
||||||
get isSecret() {
|
|
||||||
return this.packageClassification === '20';
|
|
||||||
},
|
|
||||||
|
|
||||||
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 Package</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_package_form"
|
|
||||||
accept-charset="UTF-8"
|
|
||||||
action=""
|
|
||||||
method="post"
|
|
||||||
>
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<!-- Name -->
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="package-name">
|
|
||||||
<span class="label-text">Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="package-name"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
name="package-name"
|
|
||||||
placeholder="Name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="package-description">
|
|
||||||
<span class="label-text">Description</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="package-description"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="Description..."
|
|
||||||
name="package-description"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Classification Level -->
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="package-classification">
|
|
||||||
<span class="label-text">Package Classification</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="package-classification"
|
|
||||||
name="package-classification"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
x-model="packageClassification"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="null" disabled selected>Select Classification</option>
|
|
||||||
<option value="0">Internal</option>
|
|
||||||
<option value="10">Restricted</option>
|
|
||||||
<option value="20">Secret</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Secret Groups -->
|
|
||||||
<div class="form-control mb-3" x-show="isSecret" x-cloak>
|
|
||||||
<label class="label" for="package-data-pool">
|
|
||||||
<span class="label-text">Data Pool for SECRET Package</span>
|
|
||||||
</label>
|
|
||||||
<p>Only users with this role can be granted access to this package</p>
|
|
||||||
<select
|
|
||||||
id="package-data-pool"
|
|
||||||
name="package-data-pool"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected>Select Data Pool</option>
|
|
||||||
{% for obj in meta.available_groups %}
|
|
||||||
{% if obj.secret %}
|
|
||||||
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="modal-action">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
onclick="this.closest('dialog').close()"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click="submit('new_package_form')"
|
|
||||||
:disabled="isSubmitting || !selectedType || loadingSchemas"
|
|
||||||
>
|
|
||||||
<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,97 +0,0 @@
|
|||||||
{% extends "framework_modern.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/objects/edit_package_modal.html" %}
|
|
||||||
{% include "modals/objects/edit_package_permissions_modal.html" %}
|
|
||||||
{% include "modals/objects/publish_package_modal.html" %}
|
|
||||||
{% include "modals/objects/set_license_modal.html" %}
|
|
||||||
{% include "modals/objects/export_package_modal.html" %}
|
|
||||||
{% include "modals/objects/generic_delete_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
<div class="space-y-2 p-4">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div class="card bg-base-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h2 class="card-title text-2xl">{{ package.name }} - ({{ package.get_classification_level_display }})</h2>
|
|
||||||
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-wrench"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Actions
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="-1"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
|
||||||
>
|
|
||||||
{% block actions %}
|
|
||||||
{% include "actions/objects/package.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">{{ package.description|safe }}</p>
|
|
||||||
<ul class="menu bg-base-200 rounded-box mt-4 w-full">
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/pathway" class="hover:bg-base-300"
|
|
||||||
>Pathways ({{ package.pathways.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/rule" class="hover:bg-base-300"
|
|
||||||
>Rules ({{ package.rules.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/compound" class="hover:bg-base-300"
|
|
||||||
>Compounds ({{ package.compounds.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/reaction" class="hover:bg-base-300"
|
|
||||||
>Reactions ({{ package.reactions.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/model" class="hover:bg-base-300"
|
|
||||||
>Models ({{ package.models.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ package.url }}/scenario" class="hover:bg-base-300"
|
|
||||||
>Scenarios ({{ package.scenarios.count }})</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Show actions button if there are actions
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const actionsButton = document.getElementById("actionsButton");
|
|
||||||
const actionsList = actionsButton?.querySelector("ul");
|
|
||||||
if (actionsList && actionsList.children.length > 0) {
|
|
||||||
actionsButton?.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock content %}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
@ -254,7 +254,14 @@ class Classifier(Plugin):
|
|||||||
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
||||||
if cls.Config is None:
|
if cls.Config is None:
|
||||||
return None
|
return None
|
||||||
return cls.Config(**(data or {}))
|
|
||||||
|
# remove empty strings a.k.a unset params to not overwrite defaults
|
||||||
|
cpy = {}
|
||||||
|
if data is not None:
|
||||||
|
for k, v in data.items():
|
||||||
|
if v != "":
|
||||||
|
cpy[k] = v
|
||||||
|
return cls.Config(**cpy)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, data: dict | None = None):
|
def create(cls, data: dict | None = None):
|
||||||
|
|||||||
@ -1,54 +1,26 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:18
|
image: postgres:18
|
||||||
container_name: eppostgres
|
container_name: envipath-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: envipath
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
- postgres_data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: epredis
|
container_name: envipath-redis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
|
||||||
- ep_bayer_redis_data:/data
|
|
||||||
|
|
||||||
biotransformer3:
|
|
||||||
image: envipath/biotransformer3:1.0
|
|
||||||
container_name: epbiotransformer3
|
|
||||||
|
|
||||||
# web:
|
|
||||||
# image: envipath/envipy-bayer:1.0
|
|
||||||
# container_name: epdjango
|
|
||||||
# ports:
|
|
||||||
# - "127.0.0.1:8000:8000"
|
|
||||||
# env_file:
|
|
||||||
# - .env
|
|
||||||
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
|
||||||
# volumes:
|
|
||||||
# - ep_bayer_data:/opt/enviPy/
|
|
||||||
|
|
||||||
celery_worker:
|
|
||||||
image: envipath/envipy-bayer:1.0
|
|
||||||
container_name: epcelery
|
|
||||||
env_file:
|
|
||||||
- .env.dev
|
|
||||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
|
||||||
volumes:
|
|
||||||
- ep_bayer_data:/opt/enviPy/
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ep_bayer_postgres_data:
|
postgres_data:
|
||||||
ep_bayer_redis_data:
|
|
||||||
ep_bayer_data:
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
- ep_postgres_data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@ -18,14 +18,14 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: epredis
|
container_name: epredis
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_redis_data:/data
|
- ep_redis_data:/data
|
||||||
|
|
||||||
biotransformer3:
|
biotransformer3:
|
||||||
image: envipath/biotransformer3:1.0
|
image: envipath/biotransformer3:1.0
|
||||||
container_name: epbiotransformer3
|
container_name: epbiotransformer3
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: envipath/envipy-bayer:1.0
|
image: envipath/envipy:1.0
|
||||||
container_name: epdjango
|
container_name: epdjango
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8000:8000"
|
- "127.0.0.1:8000:8000"
|
||||||
@ -33,18 +33,18 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_data:/opt/enviPy/
|
- ep_data:/opt/enviPy/
|
||||||
|
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: envipath/envipy-bayer:1.0
|
image: envipath/envipy:1.0
|
||||||
container_name: epcelery
|
container_name: epcelery
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_data:/opt/enviPy/
|
- ep_data:/opt/enviPy/
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ep_bayer_postgres_data:
|
ep_postgres_data:
|
||||||
ep_bayer_redis_data:
|
ep_redis_data:
|
||||||
ep_bayer_data:
|
ep_data:
|
||||||
|
|||||||
@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier
|
|||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev")
|
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env")
|
||||||
print(f"Loading env from {ENV_PATH}")
|
print(f"Loading env from {ENV_PATH}")
|
||||||
load_dotenv(ENV_PATH, override=False)
|
load_dotenv(ENV_PATH, override=False)
|
||||||
|
|
||||||
@ -442,14 +442,3 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T
|
|||||||
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
||||||
if BIOTRANSFORMER_ENABLED:
|
if BIOTRANSFORMER_ENABLED:
|
||||||
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
||||||
|
|
||||||
# PES
|
|
||||||
PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None)
|
|
||||||
if PES_API_MAPPING:
|
|
||||||
import json
|
|
||||||
PES_API_MAPPING = json.loads(PES_API_MAPPING)
|
|
||||||
else:
|
|
||||||
PES_API_MAPPING = {}
|
|
||||||
|
|
||||||
# AD Group Mapping
|
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,9 @@ if "migration" in s.INSTALLED_APPS:
|
|||||||
if s.MS_ENTRA_ENABLED:
|
if s.MS_ENTRA_ENABLED:
|
||||||
urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
|
urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
|
||||||
|
|
||||||
|
if s.TENANT != "public":
|
||||||
|
urlpatterns.append(path(f"{PATH_PREFIX}", include(f"{s.TENANT}.urls")))
|
||||||
|
|
||||||
# Custom error handlers
|
# Custom error handlers
|
||||||
handler400 = "epdb.views.handler400"
|
handler400 = "epdb.views.handler400"
|
||||||
handler403 = "epdb.views.handler403"
|
handler403 = "epdb.views.handler403"
|
||||||
|
|||||||
@ -117,25 +117,28 @@ class APIPermissionTestBase(TestCase):
|
|||||||
|
|
||||||
# Create test compounds in each package
|
# Create test compounds in each package
|
||||||
cls.reviewed_compound = Compound.create(
|
cls.reviewed_compound = Compound.create(
|
||||||
cls.reviewed_package, "C", "Reviewed Compound", "Test compound"
|
cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.owned_compound = Compound.create(
|
cls.owned_compound = Compound.create(
|
||||||
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound"
|
cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.read_compound = Compound.create(
|
cls.read_compound = Compound.create(
|
||||||
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound"
|
cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.write_compound = Compound.create(
|
cls.write_compound = Compound.create(
|
||||||
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound"
|
cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.all_compound = Compound.create(
|
cls.all_compound = Compound.create(
|
||||||
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound"
|
cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.no_access_compound = Compound.create(
|
cls.no_access_compound = Compound.create(
|
||||||
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound"
|
cls.unreviewed_package_no_access,
|
||||||
|
"CCCCCC",
|
||||||
|
name="No Access Compound",
|
||||||
|
description="Test compound",
|
||||||
)
|
)
|
||||||
cls.group_compound = Compound.create(
|
cls.group_compound = Compound.create(
|
||||||
cls.group_package, "CCCCCCC", "Group Compound", "Test compound"
|
cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -294,8 +294,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Reviewed Compound {idx:03d}",
|
name=f"Reviewed Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -305,8 +305,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Draft Compound {idx:03d}",
|
name=f"Draft Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
epapi/v1/endpoints/joblogs.py
Normal file
26
epapi/v1/endpoints/joblogs.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django.conf import settings as s
|
||||||
|
from ninja import Router
|
||||||
|
from ninja_extra.pagination import paginate
|
||||||
|
|
||||||
|
from epdb.models import JobLog
|
||||||
|
from ..pagination import EnhancedPageNumberPagination
|
||||||
|
from ..schemas import JobLogOutSchema
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
|
||||||
|
@paginate(
|
||||||
|
EnhancedPageNumberPagination,
|
||||||
|
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||||
|
)
|
||||||
|
def list_all_joblogs(request):
|
||||||
|
"""
|
||||||
|
List all JobLogs from reviewed packages.
|
||||||
|
"""
|
||||||
|
current_user = request.user
|
||||||
|
|
||||||
|
if current_user.is_superuser:
|
||||||
|
return JobLog.objects.all().order_by("-created")
|
||||||
|
else:
|
||||||
|
return JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
@ -15,6 +15,7 @@ from .endpoints import (
|
|||||||
additional_information,
|
additional_information,
|
||||||
settings,
|
settings,
|
||||||
groups,
|
groups,
|
||||||
|
joblogs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main router with authentication
|
# Main router with authentication
|
||||||
@ -37,6 +38,7 @@ router.add_router("", structure.router)
|
|||||||
router.add_router("", additional_information.router)
|
router.add_router("", additional_information.router)
|
||||||
router.add_router("", settings.router)
|
router.add_router("", settings.router)
|
||||||
router.add_router("", groups.router)
|
router.add_router("", groups.router)
|
||||||
|
router.add_router("", joblogs.router)
|
||||||
|
|
||||||
if s.IUCLID_EXPORT_ENABLED:
|
if s.IUCLID_EXPORT_ENABLED:
|
||||||
from epiuclid.api import router as iuclid_router
|
from epiuclid.api import router as iuclid_router
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
from ninja import FilterSchema, FilterLookup, Schema
|
from datetime import datetime
|
||||||
from typing import Annotated, Optional, List, Dict, Any
|
from typing import Annotated, Optional, List, Dict, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from ninja import Field, FilterSchema, FilterLookup, Schema
|
||||||
|
|
||||||
|
|
||||||
# Filter schema for query parameters
|
# Filter schema for query parameters
|
||||||
class ReviewStatusFilter(FilterSchema):
|
class ReviewStatusFilter(FilterSchema):
|
||||||
@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
|
|||||||
url: str = ""
|
url: str = ""
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleUserOutSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
url: str
|
||||||
|
name: str = Field(alias="username")
|
||||||
|
|
||||||
|
|
||||||
|
class JobLogOutSchema(Schema):
|
||||||
|
user: SimpleUserOutSchema
|
||||||
|
id: UUID = Field(alias="task_id")
|
||||||
|
url: str
|
||||||
|
name: str = Field(alias="job_name")
|
||||||
|
created: datetime = Field(alias="created")
|
||||||
|
status: str = Field(alias="status")
|
||||||
|
done: Optional[datetime] = Field(None, alias="done_at")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_url(obj):
|
||||||
|
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
|
||||||
|
|||||||
@ -1,12 +1,32 @@
|
|||||||
import msal
|
import msal
|
||||||
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 login
|
from django.contrib.auth import login
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from epdb.logic import UserManager
|
from epdb.logic import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
def get_msal_app_with_cache(request):
|
||||||
|
"""
|
||||||
|
Create MSAL app with session-based token cache.
|
||||||
|
"""
|
||||||
|
cache = msal.SerializableTokenCache()
|
||||||
|
|
||||||
|
# Load cache from session if it exists
|
||||||
|
if request.session.get("msal_token_cache"):
|
||||||
|
cache.deserialize(request.session["msal_token_cache"])
|
||||||
|
|
||||||
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
|
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
||||||
|
authority=s.MS_ENTRA_AUTHORITY,
|
||||||
|
token_cache=cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
return msal_app, cache
|
||||||
|
|
||||||
|
|
||||||
def entra_login(request):
|
def entra_login(request):
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
client_id=s.MS_ENTRA_CLIENT_ID,
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
@ -23,11 +43,7 @@ def entra_login(request):
|
|||||||
|
|
||||||
|
|
||||||
def entra_callback(request):
|
def entra_callback(request):
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app, cache = get_msal_app_with_cache(request)
|
||||||
client_id=s.MS_ENTRA_CLIENT_ID,
|
|
||||||
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
|
||||||
authority=s.MS_ENTRA_AUTHORITY,
|
|
||||||
)
|
|
||||||
|
|
||||||
flow = request.session.pop("msal_auth_flow", None)
|
flow = request.session.pop("msal_auth_flow", None)
|
||||||
if not flow:
|
if not flow:
|
||||||
@ -36,11 +52,18 @@ def entra_callback(request):
|
|||||||
# Acquire token using the flow and callback request
|
# Acquire token using the flow and callback request
|
||||||
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
|
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
|
||||||
|
|
||||||
|
# Save the token cache to session
|
||||||
|
if cache.has_state_changed:
|
||||||
|
request.session["msal_token_cache"] = cache.serialize()
|
||||||
|
|
||||||
claims = result["id_token_claims"]
|
claims = result["id_token_claims"]
|
||||||
|
|
||||||
user_name = claims["name"]
|
user_name = claims.get("name")
|
||||||
user_email = claims["emailaddress"]
|
user_email = claims.get("emailaddress", claims.get("email"))
|
||||||
user_oid = claims["oid"]
|
user_oid = claims.get("oid")
|
||||||
|
|
||||||
|
if not all([user_name, user_email, user_oid]):
|
||||||
|
raise ValueError("Missing required claims in ID token")
|
||||||
|
|
||||||
# Get implementing class
|
# Get implementing class
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -57,4 +80,56 @@ def entra_callback(request):
|
|||||||
|
|
||||||
login(request, u)
|
login(request, u)
|
||||||
|
|
||||||
return redirect("/") # Handle errors
|
return redirect(s.SERVER_URL) # Handle errors
|
||||||
|
|
||||||
|
|
||||||
|
def get_access_token_from_request(request, scopes=None):
|
||||||
|
"""
|
||||||
|
Get an access token from the request using MSAL token cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if auth via Access Token
|
||||||
|
if request.headers.get("Authorization"):
|
||||||
|
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
|
||||||
|
|
||||||
|
if scopes is None:
|
||||||
|
scopes = s.MS_ENTRA_SCOPES
|
||||||
|
|
||||||
|
# Get user from request (must be authenticated)
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create MSAL app with persistent cache
|
||||||
|
msal_app, cache = get_msal_app_with_cache(request)
|
||||||
|
|
||||||
|
# Try to get accounts from cache
|
||||||
|
accounts = msal_app.get_accounts()
|
||||||
|
|
||||||
|
if not accounts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the account that matches the current user
|
||||||
|
user_account = None
|
||||||
|
for account in accounts:
|
||||||
|
if account.get("local_account_id") == str(request.user.uuid):
|
||||||
|
user_account = account
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no matching account found, use the first available account
|
||||||
|
if not user_account and accounts:
|
||||||
|
user_account = accounts[0]
|
||||||
|
|
||||||
|
if not user_account:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try to acquire token silently from cache
|
||||||
|
result = msal_app.acquire_token_silent(scopes=scopes, account=user_account)
|
||||||
|
|
||||||
|
# Save cache changes back to session
|
||||||
|
if cache.has_state_changed:
|
||||||
|
request.session["msal_token_cache"] = cache.serialize()
|
||||||
|
|
||||||
|
if result and "access_token" in result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
112
epdb/admin.py
112
epdb/admin.py
@ -1,5 +1,8 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
@ -29,6 +32,8 @@ from .models import (
|
|||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
@ -45,6 +50,113 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
"date_joined",
|
"date_joined",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
actions = ["send_welcome_mail", "send_affiliation_mail"]
|
||||||
|
|
||||||
|
@admin.action(description="Send welcome mail")
|
||||||
|
def send_welcome_mail(self, request, queryset):
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
tpl = """Hello {username},
|
||||||
|
|
||||||
|
Your account has been successfully activated.
|
||||||
|
|
||||||
|
To log in, please visit
|
||||||
|
https://envipath.org/password_reset/
|
||||||
|
and request a new password.
|
||||||
|
|
||||||
|
If you have any questions or feedback, feel free to visit our community forum at
|
||||||
|
https://community.envipath.org/.
|
||||||
|
You do not need to register again for the forum - you can log in using your enviPath account by clicking "Log In" and then "Log in with enviPath."
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The enviPath Team"""
|
||||||
|
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for user in queryset:
|
||||||
|
if user.is_active:
|
||||||
|
logger.info(f"{user.username} already active - not sending mail again")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
"Your enviPath Account Is Now Active",
|
||||||
|
tpl.format(username=user.username),
|
||||||
|
"admin@envipath.org",
|
||||||
|
[user.email],
|
||||||
|
bcc=["admin@envipath.org"],
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
user.password = "ASDF"
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
users.append(user)
|
||||||
|
logger.info(f"{user.username} -> {user.email} mail sent")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request, f"Sent welcome mail to {[u.email for u in users]}", messages.SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.action(description="Send affiliation mail")
|
||||||
|
def send_affiliation_mail(self, request, queryset):
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
tpl = """Dear {username},
|
||||||
|
|
||||||
|
Thank you for your interest in enviPath!
|
||||||
|
|
||||||
|
Please note that the public enviPath system is intended for non-commercial use only.
|
||||||
|
We see that you registered using the email address {email}.
|
||||||
|
If possible, we kindly ask you to register using an official email address that reflects your affiliation (e.g., a university, NGO, or research organization).
|
||||||
|
|
||||||
|
If you would like us to update your account, simply reply to this email and let us know which address we should use.
|
||||||
|
We will then change it in our system, and you will receive a password reset email at the new address.
|
||||||
|
|
||||||
|
If you are registering with a company email address and are interested in commercial use, you are very welcome to book a meeting with us so we can discuss how we can best support you.
|
||||||
|
To book a meeting, please visit https://envipath.com/book
|
||||||
|
|
||||||
|
If changing to an affiliation email address is not possible, please contact us at registration@envipath.org
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
enviPath team"""
|
||||||
|
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for user in queryset:
|
||||||
|
if user.is_active or user.contacted:
|
||||||
|
logger.info(
|
||||||
|
f"{user.username} already active or already contacted - not sending mail again"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
"Regarding your enviPath registration",
|
||||||
|
tpl.format(username=user.username, email=user.email),
|
||||||
|
"admin@envipath.org",
|
||||||
|
[user.email],
|
||||||
|
bcc=["admin@envipath.org"],
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
user.contacted = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
users.append(user)
|
||||||
|
logger.info(f"{user.username} -> {user.email} affiliation mail sent")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request, f"Sent affiliation mail to {[u.email for u in users]}", messages.SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|||||||
10
epdb/exceptions.py
Normal file
10
epdb/exceptions.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class InvalidSMILESException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMolfileException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PackageImportException(Exception):
|
||||||
|
pass
|
||||||
@ -386,23 +386,50 @@ class PackageSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_readers(obj: Package):
|
def resolve_readers(obj: Package):
|
||||||
users = User.objects.filter(
|
readers = []
|
||||||
id__in=UserPackagePermission.objects.filter(
|
|
||||||
package=obj, permission=UserPackagePermission.READ[0]
|
|
||||||
).values_list("user", flat=True)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
return [{u.id: u.get_name()} for u in users]
|
user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=user_ids).distinct()
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||||
|
|
||||||
|
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
|
||||||
|
"group", flat=True
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||||
|
|
||||||
|
return readers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_writers(obj: Package):
|
def resolve_writers(obj: Package):
|
||||||
users = User.objects.filter(
|
writers = []
|
||||||
id__in=UserPackagePermission.objects.filter(
|
|
||||||
package=obj, permission=UserPackagePermission.WRITE[0]
|
|
||||||
).values_list("user", flat=True)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
return [{u.id: u.get_name()} for u in users]
|
user_ids = UserPackagePermission.objects.filter(
|
||||||
|
package=obj,
|
||||||
|
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=user_ids).distinct()
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||||
|
|
||||||
|
group_ids = GroupPackagePermission.objects.filter(
|
||||||
|
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
|
||||||
|
).values_list("group", flat=True)
|
||||||
|
|
||||||
|
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||||
|
|
||||||
|
return writers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_comment(obj):
|
def resolve_review_comment(obj):
|
||||||
@ -764,6 +791,7 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
|
|||||||
|
|
||||||
class CreateCompound(Schema):
|
class CreateCompound(Schema):
|
||||||
compoundSmiles: str
|
compoundSmiles: str
|
||||||
|
compoundMolFile: str | None = None
|
||||||
compoundName: str | None = None
|
compoundName: str | None = None
|
||||||
compoundDescription: str | None = None
|
compoundDescription: str | None = None
|
||||||
inchi: str | None = None
|
inchi: str | None = None
|
||||||
@ -777,9 +805,13 @@ def create_package_compound(
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
# inchi is not used atm
|
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
p,
|
||||||
|
c.compoundSmiles,
|
||||||
|
molfile=c.compoundMolFile,
|
||||||
|
name=c.compoundName,
|
||||||
|
description=c.compoundDescription,
|
||||||
|
inchi=c.inchi,
|
||||||
)
|
)
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -799,6 +831,27 @@ def delete_compound(request, package_uuid, compound_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCompoundStructure(Schema):
|
||||||
|
smiles: str
|
||||||
|
name: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
inchi: str | None = None
|
||||||
|
molfile: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
|
||||||
|
def create_package_compound_structure(
|
||||||
|
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
||||||
|
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
|
||||||
|
return redirect(cs.url)
|
||||||
|
except ValueError as e:
|
||||||
|
return 400, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||||
)
|
)
|
||||||
@ -1325,6 +1378,7 @@ class ScenarioSchema(Schema):
|
|||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||||
collectionID: Optional[str] = None
|
collectionID: Optional[str] = None
|
||||||
|
date: str = Field(None, alias="scenario_date")
|
||||||
description: str = Field(None, alias="description")
|
description: str = Field(None, alias="description")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
identifier: str = "scenario"
|
identifier: str = "scenario"
|
||||||
@ -1468,6 +1522,7 @@ def create_package_additional_information(request, package_uuid):
|
|||||||
scen = request.POST.get("scenario")
|
scen = request.POST.get("scenario")
|
||||||
scenario = Scenario.objects.get(package=p, url=scen)
|
scenario = Scenario.objects.get(package=p, url=scen)
|
||||||
|
|
||||||
|
if request.POST.get("adInfoTypes[]"):
|
||||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||||
attach_obj = url_parser.get_object()
|
attach_obj = url_parser.get_object()
|
||||||
|
|
||||||
@ -1491,6 +1546,33 @@ def create_package_additional_information(request, package_uuid):
|
|||||||
content_object=attach_obj,
|
content_object=attach_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif request.POST.get("ais"):
|
||||||
|
import json
|
||||||
|
|
||||||
|
parsed_ais = json.loads(request.POST.get("ais"))
|
||||||
|
|
||||||
|
for ai_type, ais in parsed_ais.items():
|
||||||
|
for ai in ais:
|
||||||
|
attach_obj = None
|
||||||
|
if ai.get("related"):
|
||||||
|
url_parser = EPDBURLParser(ai.get("related").get("url"))
|
||||||
|
attach_obj = url_parser.get_object()
|
||||||
|
|
||||||
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
|
if not attach_obj.url.startswith(p.url):
|
||||||
|
raise ValueError(
|
||||||
|
"Additional Information can only be set to objects stored in the same package!"
|
||||||
|
)
|
||||||
|
|
||||||
|
AdditionalInformation.create(
|
||||||
|
p,
|
||||||
|
AdditionalInformation.from_dict(ai_type, ai),
|
||||||
|
scenario=scenario,
|
||||||
|
content_object=attach_obj,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO implement additional information endpoint ?
|
# TODO implement additional information endpoint ?
|
||||||
return redirect(f"{scenario.url}")
|
return redirect(f"{scenario.url}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -1533,13 +1615,14 @@ class PathwayNode(Schema):
|
|||||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
idcomp: str = Field(None, alias="default_node_label.url")
|
idcomp: str = Field(None, alias="node_label_id")
|
||||||
idreact: str = Field(None, alias="default_node_label.url")
|
idreact: str = Field(None, alias="node_label_id")
|
||||||
image: str = Field(None, alias="image")
|
image: str = Field(None, alias="image")
|
||||||
imageSize: int = Field(None, alias="image_size")
|
imageSize: int = Field(None, alias="image_size")
|
||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
smiles: str = Field(None, alias="smiles")
|
||||||
|
pseudo: bool = Field(False, alias="pseudo")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_atom_count(obj: Node):
|
def resolve_atom_count(obj: Node):
|
||||||
@ -1552,24 +1635,10 @@ class PathwayNode(Schema):
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_engineered_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_image(obj: Node):
|
|
||||||
return f"{obj.default_node_label.url}?image=svg"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_image_size(obj: Node):
|
def resolve_image_size(obj: Node):
|
||||||
return 400
|
return 400
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_proposed_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class PathwaySchema(Schema):
|
class PathwaySchema(Schema):
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
@ -1693,6 +1762,29 @@ def create_package_pathway(
|
|||||||
return 403, {"message": str(e)}
|
return 403, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
|
def update_pathway(request, package_uuid, pathway_uuid):
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
|
||||||
|
if request.POST.get("scenario"):
|
||||||
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
|
||||||
|
|
||||||
|
pw.scenarios.add(scen)
|
||||||
|
pw.save()
|
||||||
|
|
||||||
|
return redirect(f"{pw.url}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return 400, {"message": "No scenario specified!"}
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||||
try:
|
try:
|
||||||
@ -1798,6 +1890,7 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
class CreateNode(Schema):
|
class CreateNode(Schema):
|
||||||
nodeAsSmiles: str
|
nodeAsSmiles: str
|
||||||
|
nodeAsMolFile: str | None = None
|
||||||
nodeName: str | None = None
|
nodeName: str | None = None
|
||||||
nodeReason: str | None = None
|
nodeReason: str | None = None
|
||||||
nodeDepth: str | None = None
|
nodeDepth: str | None = None
|
||||||
@ -1813,11 +1906,18 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
|||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
|
||||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||||
node_depth = int(n.nodeDepth)
|
node_depth = int(float(n.nodeDepth))
|
||||||
else:
|
else:
|
||||||
node_depth = -1
|
node_depth = -1
|
||||||
|
|
||||||
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
n = Node.create(
|
||||||
|
pw,
|
||||||
|
n.nodeAsSmiles,
|
||||||
|
node_depth,
|
||||||
|
molfile=n.nodeAsMolFile,
|
||||||
|
name=n.nodeName,
|
||||||
|
description=n.nodeReason,
|
||||||
|
)
|
||||||
|
|
||||||
return redirect(n.url)
|
return redirect(n.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -1958,6 +2058,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
for pr in e.products.split(","):
|
for pr in e.products.split(","):
|
||||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
||||||
|
|
||||||
|
multi_step = False
|
||||||
|
if e.multistep and e.multistep.strip() == "true":
|
||||||
|
multi_step = True
|
||||||
|
|
||||||
new_e = Edge.create(
|
new_e = Edge.create(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
start_nodes=educts,
|
start_nodes=educts,
|
||||||
@ -1965,11 +2069,15 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
rule=None,
|
rule=None,
|
||||||
name=None,
|
name=None,
|
||||||
description=e.edgeReason,
|
description=e.edgeReason,
|
||||||
|
multi_step=multi_step,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update depths as sideeffect of above operation
|
||||||
|
pw.update_depths()
|
||||||
|
|
||||||
return redirect(new_e.url)
|
return redirect(new_e.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding Edge failed!"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
||||||
@ -2160,3 +2268,26 @@ def get_setting(request, setting_uuid):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Util #
|
||||||
|
########
|
||||||
|
class NonPersistent(Schema):
|
||||||
|
smiles: str
|
||||||
|
setting_url: str = Field(..., alias="settingUri")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/util", response={200: Any, 403: Error})
|
||||||
|
def predict(request, np: Form[NonPersistent]):
|
||||||
|
try:
|
||||||
|
from epdb.logic import SPathway
|
||||||
|
|
||||||
|
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||||
|
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||||
|
spw.predict()
|
||||||
|
return spw.to_json()
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|||||||
115
epdb/logic.py
115
epdb/logic.py
@ -264,8 +264,12 @@ class GroupManager(object):
|
|||||||
return bool(re.findall(GroupManager.group_pattern, url))
|
return bool(re.findall(GroupManager.group_pattern, url))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_group(current_user, name, description):
|
def create_group(current_user, name, description, *args, **kwargs):
|
||||||
g = Group()
|
g = Group()
|
||||||
|
|
||||||
|
if "uuid" in kwargs:
|
||||||
|
g.uuid = kwargs["uuid"]
|
||||||
|
|
||||||
# Clean for potential XSS
|
# Clean for potential XSS
|
||||||
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
@ -341,52 +345,17 @@ class PackageManager(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def readable(user, package):
|
def readable(user, package):
|
||||||
if (
|
return (
|
||||||
UserPackagePermission.objects.filter(package=package, user=user).exists()
|
PackageManager.has_package_permission(user, package, "read") | package.reviewed is True
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package, group__in=GroupManager.get_groups(user)
|
|
||||||
)
|
)
|
||||||
or package.reviewed is True
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def writable(user, package):
|
def writable(user, package):
|
||||||
if (
|
return PackageManager.has_package_permission(user, package, "write")
|
||||||
UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.WRITE[0]
|
|
||||||
).exists()
|
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package,
|
|
||||||
group__in=GroupManager.get_groups(user),
|
|
||||||
permission=Permission.WRITE[0],
|
|
||||||
).exists()
|
|
||||||
or UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.ALL[0]
|
|
||||||
).exists()
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def administrable(user, package):
|
def administrable(user, package):
|
||||||
if (
|
return PackageManager.has_package_permission(user, package, "all")
|
||||||
UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.ALL[0]
|
|
||||||
).exists()
|
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package,
|
|
||||||
group__in=GroupManager.get_groups(user),
|
|
||||||
permission=Permission.ALL[0],
|
|
||||||
).exists()
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
|
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
|
||||||
@ -470,7 +439,9 @@ class PackageManager(object):
|
|||||||
# remove package if user is owner and package is reviewed e.g. admin
|
# remove package if user is owner and package is reviewed e.g. admin
|
||||||
qs = qs.filter(reviewed=False)
|
qs = qs.filter(reviewed=False)
|
||||||
|
|
||||||
return qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_writeable_packages(user):
|
def get_all_writeable_packages(user):
|
||||||
@ -514,7 +485,9 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.filter(reviewed=False)
|
qs = qs.filter(reviewed=False)
|
||||||
|
|
||||||
return qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_packages():
|
def get_packages():
|
||||||
@ -716,6 +689,10 @@ class PackageManager(object):
|
|||||||
struc.description = structure["description"]
|
struc.description = structure["description"]
|
||||||
struc.aliases = structure.get("aliases", [])
|
struc.aliases = structure.get("aliases", [])
|
||||||
struc.smiles = structure["smiles"]
|
struc.smiles = structure["smiles"]
|
||||||
|
|
||||||
|
if structure.get("molfile"):
|
||||||
|
struc.molfile = structure["molfile"]
|
||||||
|
|
||||||
struc.save()
|
struc.save()
|
||||||
|
|
||||||
for scen in structure["scenarios"]:
|
for scen in structure["scenarios"]:
|
||||||
@ -1018,52 +995,9 @@ class PackageManager(object):
|
|||||||
|
|
||||||
print("Fixing Node depths...")
|
print("Fixing Node depths...")
|
||||||
total_pws = Pathway.objects.filter(package=pack).count()
|
total_pws = Pathway.objects.filter(package=pack).count()
|
||||||
|
|
||||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||||
in_count = defaultdict(lambda: 0)
|
pw.update_depths()
|
||||||
out_count = defaultdict(lambda: 0)
|
|
||||||
|
|
||||||
for e in pw.edges:
|
|
||||||
# TODO check if this will remain
|
|
||||||
for react in e.start_nodes.all():
|
|
||||||
out_count[str(react.uuid)] += 1
|
|
||||||
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
in_count[str(prod.uuid)] += 1
|
|
||||||
|
|
||||||
root_nodes = []
|
|
||||||
for n in pw.nodes:
|
|
||||||
num_parents = in_count[str(n.uuid)]
|
|
||||||
if num_parents == 0:
|
|
||||||
# must be a root node or unconnected node
|
|
||||||
if n.depth != 0:
|
|
||||||
n.depth = 0
|
|
||||||
n.save()
|
|
||||||
|
|
||||||
# Only root node may have children
|
|
||||||
if out_count[str(n.uuid)] > 0:
|
|
||||||
root_nodes.append(n)
|
|
||||||
|
|
||||||
levels = [root_nodes]
|
|
||||||
seen = set()
|
|
||||||
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
|
|
||||||
for i, level_nodes in enumerate(levels):
|
|
||||||
new_level = []
|
|
||||||
for n in level_nodes:
|
|
||||||
for e in n.out_edges.all():
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
if str(prod.uuid) not in seen:
|
|
||||||
old_depth = prod.depth
|
|
||||||
if old_depth != i + 1:
|
|
||||||
prod.depth = i + 1
|
|
||||||
prod.save()
|
|
||||||
|
|
||||||
new_level.append(prod)
|
|
||||||
|
|
||||||
seen.add(str(n.uuid))
|
|
||||||
|
|
||||||
if new_level:
|
|
||||||
levels.append(new_level)
|
|
||||||
|
|
||||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||||
|
|
||||||
return pack
|
return pack
|
||||||
@ -1077,7 +1011,7 @@ class PackageManager(object):
|
|||||||
add_import_timestamp=True,
|
add_import_timestamp=True,
|
||||||
trust_reviewed=False,
|
trust_reviewed=False,
|
||||||
) -> Package:
|
) -> Package:
|
||||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
importer = PackageImporter(data, preserve_uuids)
|
||||||
imported_package = importer.do_import()
|
imported_package = importer.do_import()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@ -1894,6 +1828,11 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if edge.rule:
|
||||||
|
e["rule"] = edge.rule.simple_json()
|
||||||
|
if edge.probability:
|
||||||
|
e["probability"] = edge.probability
|
||||||
|
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -99,11 +99,15 @@ class Command(BaseCommand):
|
|||||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||||
new_license.save()
|
new_license.save()
|
||||||
|
|
||||||
def import_package(self, data, owner):
|
def import_package(self, data, owner, all_envipath_user_group):
|
||||||
return PackageManager.import_legacy_package(
|
p = PackageManager.import_legacy_package(
|
||||||
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PackageManager.grant_read(owner, p, all_envipath_user_group)
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
def create_default_setting(self, owner, packages):
|
def create_default_setting(self, owner, packages):
|
||||||
s = SettingManager.create_setting(
|
s = SettingManager.create_setting(
|
||||||
owner,
|
owner,
|
||||||
@ -198,7 +202,7 @@ class Command(BaseCommand):
|
|||||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||||
).read()
|
).read()
|
||||||
)
|
)
|
||||||
imported_package = self.import_package(package_data, admin)
|
imported_package = self.import_package(package_data, admin, g)
|
||||||
mapping[p.replace(".json", "")] = imported_package
|
mapping[p.replace(".json", "")] = imported_package
|
||||||
|
|
||||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||||
|
|||||||
594
epdb/migrations/0001_initial.py
Normal file
594
epdb/migrations/0001_initial.py
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-07-22 20:58
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Compound',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EPModel',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Permission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='License',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('link', models.URLField(verbose_name='link')),
|
||||||
|
('image_link', models.URLField(verbose_name='Image link')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Rule',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('hashed_key', models.CharField(max_length=128, unique=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
|
||||||
|
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompoundStructure',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('smiles', models.TextField(verbose_name='SMILES')),
|
||||||
|
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
||||||
|
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
||||||
|
('normalized_structure', models.BooleanField(default=False)),
|
||||||
|
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='default_structure',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Edge',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EnviFormer',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PluginModel',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleBaseRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(verbose_name='Group name')),
|
||||||
|
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
||||||
|
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_group',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Node',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('depth', models.IntegerField(verbose_name='Node depth')),
|
||||||
|
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
||||||
|
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
||||||
|
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='end_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='start_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Package',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
||||||
|
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_package',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Pathway',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Reaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
||||||
|
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
||||||
|
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
||||||
|
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='edge_label',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Scenario',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('scenario_date', models.CharField(default='No date', max_length=256)),
|
||||||
|
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
||||||
|
('additional_information', models.JSONField(verbose_name='Additional Information')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Setting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('public', models.BooleanField(default=False)),
|
||||||
|
('global_default', models.BooleanField(default=False)),
|
||||||
|
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
||||||
|
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
||||||
|
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
||||||
|
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='setting',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_setting',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MLRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApplicabilityDomain',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('num_neighbours', models.FloatField(default=5)),
|
||||||
|
('reliability_threshold', models.FloatField(default=0.5)),
|
||||||
|
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
||||||
|
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleAmbitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('smirks', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
||||||
|
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRDKitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRuleOrdering',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order_index', models.IntegerField()),
|
||||||
|
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
||||||
|
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sequentialrule',
|
||||||
|
name='simple_rules',
|
||||||
|
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ParallelRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='compound',
|
||||||
|
unique_together={('uuid', 'package')},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GroupPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'group')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSettingPermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('setting', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,128 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-08-25 18:07
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('epdb', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalDatabase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
||||||
|
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
||||||
|
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Database',
|
||||||
|
'verbose_name_plural': 'External Databases',
|
||||||
|
'db_table': 'epdb_external_database',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='apitoken',
|
||||||
|
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='edge',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='edge',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, help_text='Whether this token is active'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='modified',
|
||||||
|
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='functional_groups',
|
||||||
|
field=models.JSONField(blank=True, default=dict, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='app_domain',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='created',
|
||||||
|
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='expires_at',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='hashed_key',
|
||||||
|
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='num_neighbours',
|
||||||
|
field=models.IntegerField(default=5),
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='apitoken',
|
||||||
|
table='epdb_api_token',
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalIdentifier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('object_id', models.IntegerField()),
|
||||||
|
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
||||||
|
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
||||||
|
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Identifier',
|
||||||
|
'verbose_name_plural': 'External Identifiers',
|
||||||
|
'db_table': 'epdb_external_identifier',
|
||||||
|
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
||||||
|
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,228 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-08-26 17:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_url(apps, schema_editor):
|
||||||
|
MODELS = [
|
||||||
|
'User',
|
||||||
|
'Group',
|
||||||
|
'Package',
|
||||||
|
'Compound',
|
||||||
|
'CompoundStructure',
|
||||||
|
'Pathway',
|
||||||
|
'Edge',
|
||||||
|
'Node',
|
||||||
|
'Reaction',
|
||||||
|
'SimpleAmbitRule',
|
||||||
|
'SimpleRDKitRule',
|
||||||
|
'ParallelRule',
|
||||||
|
'SequentialRule',
|
||||||
|
'Scenario',
|
||||||
|
'Setting',
|
||||||
|
'MLRelativeReasoning',
|
||||||
|
'EnviFormer',
|
||||||
|
'ApplicabilityDomain',
|
||||||
|
]
|
||||||
|
for model in MODELS:
|
||||||
|
obj_cls = apps.get_model("epdb", model)
|
||||||
|
for obj in obj_cls.objects.all():
|
||||||
|
obj.url = assemble_url(obj)
|
||||||
|
if obj.url is None:
|
||||||
|
raise ValueError(f"Could not assemble url for {obj}")
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def assemble_url(obj):
|
||||||
|
from django.conf import settings as s
|
||||||
|
match obj.__class__.__name__:
|
||||||
|
case 'User':
|
||||||
|
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Group':
|
||||||
|
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Package':
|
||||||
|
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Compound':
|
||||||
|
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'CompoundStructure':
|
||||||
|
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
|
||||||
|
case 'SimpleAmbitRule':
|
||||||
|
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'SimpleRDKitRule':
|
||||||
|
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'ParallelRule':
|
||||||
|
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'SequentialRule':
|
||||||
|
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
|
||||||
|
case 'Reaction':
|
||||||
|
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Pathway':
|
||||||
|
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Node':
|
||||||
|
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
|
||||||
|
case 'Edge':
|
||||||
|
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
|
||||||
|
case 'MLRelativeReasoning':
|
||||||
|
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'EnviFormer':
|
||||||
|
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'ApplicabilityDomain':
|
||||||
|
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
|
||||||
|
case 'Scenario':
|
||||||
|
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Setting':
|
||||||
|
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown model {obj.__class__.__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='scenario',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='setting',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
|
||||||
|
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compound',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='edge',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='node',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='package',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rule',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='scenario',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='setting',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-09 09:21
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='mlrelativereasoning',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='data_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='eval_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='rule_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleBasedRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
||||||
|
('min_count', models.IntegerField(default=10)),
|
||||||
|
('max_count', models.IntegerField(default=0)),
|
||||||
|
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='RuleBaseRelativeReasoning',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
epdb/migrations/0005_alter_group_group_member.py
Normal file
18
epdb/migrations/0005_alter_group_group_member.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-11 06:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0004_alter_mlrelativereasoning_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='group_member',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-18 06:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0005_alter_group_group_member'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rulebasedrelativereasoning',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-10-07 08:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='enviformer',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='app_domain',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='data_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='eval_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='eval_results',
|
||||||
|
field=models.JSONField(blank=True, default=dict, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='model_status',
|
||||||
|
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='rule_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
||||||
|
),
|
||||||
|
]
|
||||||
64
epdb/migrations/0008_enzymelink.py
Normal file
64
epdb/migrations/0008_enzymelink.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-10 06:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EnzymeLink",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
model_utils.fields.AutoLastModifiedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField(default="no name", verbose_name="Name")),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(default="no description", verbose_name="Descriptions"),
|
||||||
|
),
|
||||||
|
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
||||||
|
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("ec_number", models.TextField(verbose_name="EC Number")),
|
||||||
|
("classification_level", models.IntegerField(verbose_name="Classification Level")),
|
||||||
|
("linking_method", models.TextField(verbose_name="Linking Method")),
|
||||||
|
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
|
||||||
|
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
|
||||||
|
(
|
||||||
|
"rule",
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
66
epdb/migrations/0009_joblog.py
Normal file
66
epdb/migrations/0009_joblog.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-27 09:39
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0008_enzymelink"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="JobLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
model_utils.fields.AutoLastModifiedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("task_id", models.UUIDField(unique=True)),
|
||||||
|
("job_name", models.TextField()),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("INITIAL", "Initial"),
|
||||||
|
("SUCCESS", "Success"),
|
||||||
|
("FAILURE", "Failure"),
|
||||||
|
("REVOKED", "Revoked"),
|
||||||
|
("IGNORED", "Ignored"),
|
||||||
|
],
|
||||||
|
default="INITIAL",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
|
||||||
|
("task_result", models.TextField(blank=True, default=None, null=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
epdb/migrations/0010_license_cc_string.py
Normal file
18
epdb/migrations/0010_license_cc_string.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-11 14:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0009_joblog"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="license",
|
||||||
|
name="cc_string",
|
||||||
|
field=models.TextField(default="by-nc-sa", verbose_name="CC string"),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
59
epdb/migrations/0011_auto_20251111_1413.py
Normal file
59
epdb/migrations/0011_auto_20251111_1413.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-11 14:13
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Min
|
||||||
|
|
||||||
|
|
||||||
|
def set_cc(apps, schema_editor):
|
||||||
|
License = apps.get_model("epdb", "License")
|
||||||
|
|
||||||
|
# For all existing licenses extract cc_string from link
|
||||||
|
for license in License.objects.all():
|
||||||
|
pattern = r"/licenses/([^/]+)/4\.0"
|
||||||
|
match = re.search(pattern, license.link)
|
||||||
|
if match:
|
||||||
|
license.cc_string = match.group(1)
|
||||||
|
license.save()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Could not find license for {license.link}")
|
||||||
|
|
||||||
|
# Ensure we have all licenses
|
||||||
|
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
|
||||||
|
for cc_string in cc_strings:
|
||||||
|
if not License.objects.filter(cc_string=cc_string).exists():
|
||||||
|
new_license = License()
|
||||||
|
new_license.cc_string = cc_string
|
||||||
|
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
|
||||||
|
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||||
|
new_license.save()
|
||||||
|
|
||||||
|
# As we might have existing Licenses representing the same License,
|
||||||
|
# get min pk and all pks as a list
|
||||||
|
license_lookup_qs = License.objects.values("cc_string").annotate(
|
||||||
|
lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",))
|
||||||
|
)
|
||||||
|
|
||||||
|
license_lookup = {
|
||||||
|
row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs
|
||||||
|
}
|
||||||
|
|
||||||
|
Packages = apps.get_model("epdb", "Package")
|
||||||
|
|
||||||
|
for k, v in license_lookup.items():
|
||||||
|
# Set min pk to all packages pointing to any of the duplicates
|
||||||
|
Packages.objects.filter(pk__in=v[1]).update(license_id=v[0])
|
||||||
|
# remove the min pk from "other" pks as we use them for deletion
|
||||||
|
v[1].remove(v[0])
|
||||||
|
# Delete redundant License objects
|
||||||
|
License.objects.filter(pk__in=v[1]).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0010_license_cc_string"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(set_cc)]
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-02 13:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0011_auto_20251111_1413"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="node",
|
||||||
|
name="stereo_removed",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="predicted",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
epdb/migrations/0013_setting_expansion_schema.py
Normal file
25
epdb/migrations/0013_setting_expansion_schema.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-14 11:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0012_node_stereo_removed_pathway_predicted"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="setting",
|
||||||
|
name="expansion_schema",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("BFS", "Breadth First Search"),
|
||||||
|
("DFS", "Depth First Search"),
|
||||||
|
("GREEDY", "Greedy"),
|
||||||
|
],
|
||||||
|
default="BFS",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-14 16:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0013_setting_expansion_schema"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="setting",
|
||||||
|
old_name="expansion_schema",
|
||||||
|
new_name="expansion_scheme",
|
||||||
|
),
|
||||||
|
]
|
||||||
17
epdb/migrations/0015_user_is_reviewer.py
Normal file
17
epdb/migrations/0015_user_is_reviewer.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-19 19:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0014_rename_expansion_schema_setting_expansion_scheme"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_reviewer",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-12 09:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0015_user_is_reviewer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="epmodel",
|
||||||
|
name="model_status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("INITIAL", "Initial"),
|
||||||
|
("INITIALIZING", "Model is initializing."),
|
||||||
|
("BUILDING", "Model is building."),
|
||||||
|
(
|
||||||
|
"BUILT_NOT_EVALUATED",
|
||||||
|
"Model is built and can be used for predictions, Model is not evaluated yet.",
|
||||||
|
),
|
||||||
|
("EVALUATING", "Model is evaluating"),
|
||||||
|
("FINISHED", "Model has finished building and evaluation."),
|
||||||
|
("ERROR", "Model has failed."),
|
||||||
|
],
|
||||||
|
default="INITIAL",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PropertyPluginModel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"epmodel_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="epdb.epmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("threshold", models.FloatField(default=0.5)),
|
||||||
|
("eval_results", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("multigen_eval", models.BooleanField(default=False)),
|
||||||
|
("plugin_identifier", models.CharField(max_length=255)),
|
||||||
|
(
|
||||||
|
"app_domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="epdb.applicabilitydomain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"data_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_data_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Data Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"eval_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"rule_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("epdb.epmodel",),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="setting",
|
||||||
|
name="property_models",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="settings",
|
||||||
|
to="epdb.propertypluginmodel",
|
||||||
|
verbose_name="Setting Property Models",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="PluginModel",
|
||||||
|
),
|
||||||
|
]
|
||||||
93
epdb/migrations/0017_additionalinformation.py
Normal file
93
epdb/migrations/0017_additionalinformation.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-20 12:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("epdb", "0016_remove_enviformer_model_status_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AdditionalInformation",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
||||||
|
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("type", models.TextField(verbose_name="Additional Information Type")),
|
||||||
|
("data", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("object_id", models.PositiveBigIntegerField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"content_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"package",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Package",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"scenario",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="scenario_additional_information",
|
||||||
|
to="epdb.scenario",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"indexes": [
|
||||||
|
models.Index(fields=["type"], name="epdb_additi_type_394349_idx"),
|
||||||
|
models.Index(
|
||||||
|
fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["scenario", "content_type", "object_id"],
|
||||||
|
name="epdb_additi_scenari_ef2bf5_idx",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(
|
||||||
|
models.Q(("content_type__isnull", True), ("object_id__isnull", True)),
|
||||||
|
models.Q(("content_type__isnull", False), ("object_id__isnull", False)),
|
||||||
|
_connector="OR",
|
||||||
|
),
|
||||||
|
name="ck_addinfo_gfk_pair",
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(
|
||||||
|
("scenario__isnull", False),
|
||||||
|
("content_type__isnull", False),
|
||||||
|
_connector="OR",
|
||||||
|
),
|
||||||
|
name="ck_addinfo_not_both_null",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-20 12:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def get_additional_information(scenario):
|
||||||
|
from envipy_additional_information import registry
|
||||||
|
from envipy_additional_information.parsers import TypeOfAerationParser
|
||||||
|
|
||||||
|
for k, vals in scenario.additional_information.items():
|
||||||
|
if k == "enzyme":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k == "SpikeConentration":
|
||||||
|
k = "SpikeConcentration"
|
||||||
|
|
||||||
|
if k == "AerationType":
|
||||||
|
k = "TypeOfAeration"
|
||||||
|
|
||||||
|
for v in vals:
|
||||||
|
# Per default additional fields are ignored
|
||||||
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
|
try:
|
||||||
|
inst = MAPPING[k](**v)
|
||||||
|
except Exception:
|
||||||
|
if k == "TypeOfAeration":
|
||||||
|
toa = TypeOfAerationParser()
|
||||||
|
inst = toa.from_string(v["type"])
|
||||||
|
|
||||||
|
# Add uuid to uniquely identify objects for manipulation
|
||||||
|
if "uuid" in v:
|
||||||
|
inst.__dict__["uuid"] = v["uuid"]
|
||||||
|
|
||||||
|
yield inst
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
Scenario = apps.get_model("epdb", "Scenario")
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
bulk = []
|
||||||
|
related = []
|
||||||
|
ctype = {o.model: o for o in ContentType.objects.all()}
|
||||||
|
parents = Scenario.objects.prefetch_related(
|
||||||
|
"compound_set",
|
||||||
|
"compoundstructure_set",
|
||||||
|
"reaction_set",
|
||||||
|
"rule_set",
|
||||||
|
"pathway_set",
|
||||||
|
"node_set",
|
||||||
|
"edge_set",
|
||||||
|
).filter(parent__isnull=True)
|
||||||
|
|
||||||
|
for i, scenario in enumerate(parents):
|
||||||
|
print(f"{i + 1}/{len(parents)}", end="\r")
|
||||||
|
if scenario.parent is not None:
|
||||||
|
related.append(scenario.parent)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ai in get_additional_information(scenario):
|
||||||
|
bulk.append(
|
||||||
|
AdditionalInformation(
|
||||||
|
package=scenario.package,
|
||||||
|
scenario=scenario,
|
||||||
|
type=ai.__class__.__name__,
|
||||||
|
data=ai.model_dump(mode="json"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n", len(bulk))
|
||||||
|
|
||||||
|
related = Scenario.objects.prefetch_related(
|
||||||
|
"compound_set",
|
||||||
|
"compoundstructure_set",
|
||||||
|
"reaction_set",
|
||||||
|
"rule_set",
|
||||||
|
"pathway_set",
|
||||||
|
"node_set",
|
||||||
|
"edge_set",
|
||||||
|
).filter(parent__isnull=False)
|
||||||
|
|
||||||
|
for i, scenario in enumerate(related):
|
||||||
|
print(f"{i + 1}/{len(related)}", end="\r")
|
||||||
|
parent = scenario.parent
|
||||||
|
# Check to which objects this scenario is attached to
|
||||||
|
for ai in get_additional_information(scenario):
|
||||||
|
rel_objs = [
|
||||||
|
"compound",
|
||||||
|
"compoundstructure",
|
||||||
|
"reaction",
|
||||||
|
"rule",
|
||||||
|
"pathway",
|
||||||
|
"node",
|
||||||
|
"edge",
|
||||||
|
]
|
||||||
|
for rel_obj in rel_objs:
|
||||||
|
for o in getattr(scenario, f"{rel_obj}_set").all():
|
||||||
|
bulk.append(
|
||||||
|
AdditionalInformation(
|
||||||
|
package=scenario.package,
|
||||||
|
scenario=parent,
|
||||||
|
type=ai.__class__.__name__,
|
||||||
|
data=ai.model_dump(mode="json"),
|
||||||
|
content_type=ctype[rel_obj],
|
||||||
|
object_id=o.pk,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Start creating additional information objects...")
|
||||||
|
AdditionalInformation.objects.bulk_create(bulk)
|
||||||
|
print("Done!")
|
||||||
|
print(len(bulk))
|
||||||
|
|
||||||
|
Scenario.objects.filter(parent__isnull=False).delete()
|
||||||
|
# Call ai save to fix urls
|
||||||
|
ais = AdditionalInformation.objects.all()
|
||||||
|
total = ais.count()
|
||||||
|
|
||||||
|
for i, ai in enumerate(ais):
|
||||||
|
print(f"{i + 1}/{total}", end="\r")
|
||||||
|
ai.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0017_additionalinformation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -1,741 +1,20 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
||||||
|
|
||||||
import django.contrib.auth.models
|
from django.db import migrations
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.contrib.postgres.fields
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
("epdb", "0018_auto_20260220_1203"),
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.RemoveField(
|
||||||
name='ApplicabilityDomain',
|
model_name="scenario",
|
||||||
fields=[
|
name="additional_information",
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('num_neighbours', models.IntegerField(default=5)),
|
|
||||||
('reliability_threshold', models.FloatField(default=0.5)),
|
|
||||||
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
|
||||||
('functional_groups', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.RemoveField(
|
||||||
name='Edge',
|
model_name="scenario",
|
||||||
fields=[
|
name="parent",
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EPModel',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalDatabase',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
|
||||||
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
|
||||||
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
|
||||||
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Database',
|
|
||||||
'verbose_name_plural': 'External Databases',
|
|
||||||
'db_table': 'epdb_external_database',
|
|
||||||
'ordering': ['name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Permission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='License',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('cc_string', models.TextField(verbose_name='CC string')),
|
|
||||||
('link', models.URLField(verbose_name='link')),
|
|
||||||
('image_link', models.URLField(verbose_name='Image link')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Rule',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('email', models.EmailField(max_length=254, unique=True)),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('is_reviewer', models.BooleanField(default=False)),
|
|
||||||
('default_package', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Default Package')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'user',
|
|
||||||
'verbose_name_plural': 'users',
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='APIToken',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('hashed_key', models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)),
|
|
||||||
('expires_at', models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True)),
|
|
||||||
('name', models.CharField(help_text='Descriptive name for this token', max_length=100)),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Whether this token is active')),
|
|
||||||
('user', models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'API Token',
|
|
||||||
'verbose_name_plural': 'API Tokens',
|
|
||||||
'db_table': 'epdb_api_token',
|
|
||||||
'ordering': ['-created'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Compound',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CompoundStructure',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('smiles', models.TextField(verbose_name='SMILES')),
|
|
||||||
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
|
||||||
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
|
||||||
('normalized_structure', models.BooleanField(default=False)),
|
|
||||||
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='default_structure',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PropertyPluginModel',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('plugin_identifier', models.CharField(max_length=255)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Group',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('name', models.TextField(verbose_name='Group name')),
|
|
||||||
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('group_member', models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
|
||||||
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_group',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='JobLog',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('task_id', models.UUIDField(unique=True)),
|
|
||||||
('job_name', models.TextField()),
|
|
||||||
('status', models.CharField(choices=[('INITIAL', 'Initial'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKED', 'Revoked'), ('IGNORED', 'Ignored')], default='INITIAL', max_length=20)),
|
|
||||||
('done_at', models.DateTimeField(blank=True, default=None, null=True)),
|
|
||||||
('task_result', models.TextField(blank=True, default=None, null=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Package',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
|
||||||
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'swappable': 'EPDB_PACKAGE_MODEL',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Node',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('depth', models.IntegerField(verbose_name='Node depth')),
|
|
||||||
('stereo_removed', models.BooleanField(default=False)),
|
|
||||||
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
|
||||||
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
|
||||||
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='end_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='start_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Pathway',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('predicted', models.BooleanField(default=False)),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Reaction',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
|
||||||
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
|
||||||
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
|
||||||
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EnzymeLink',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('ec_number', models.TextField(verbose_name='EC Number')),
|
|
||||||
('classification_level', models.IntegerField(verbose_name='Classification Level')),
|
|
||||||
('linking_method', models.TextField(verbose_name='Linking Method')),
|
|
||||||
('edge_evidence', models.ManyToManyField(to='epdb.edge')),
|
|
||||||
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.rule')),
|
|
||||||
('reaction_evidence', models.ManyToManyField(to='epdb.reaction')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='edge_label',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Scenario',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('scenario_date', models.CharField(default='No date', max_length=256)),
|
|
||||||
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reaction',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compoundstructure',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Setting',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('public', models.BooleanField(default=False)),
|
|
||||||
('global_default', models.BooleanField(default=False)),
|
|
||||||
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
|
||||||
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
|
||||||
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
|
||||||
('expansion_scheme', models.CharField(choices=[('BFS', 'Breadth First Search'), ('DFS', 'Depth First Search'), ('GREEDY', 'Greedy')], default='BFS', max_length=20)),
|
|
||||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Setting Rule Packages')),
|
|
||||||
('property_models', models.ManyToManyField(blank=True, related_name='settings', to='epdb.propertypluginmodel', verbose_name='Setting Property Models')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='setting',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_setting',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EnviFormer',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MLRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='model',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='app_domain',
|
|
||||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='data_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='eval_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='rule_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RuleBasedRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('min_count', models.IntegerField(default=10)),
|
|
||||||
('max_count', models.IntegerField(default=0)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalIdentifier',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('object_id', models.IntegerField()),
|
|
||||||
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
|
||||||
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
|
||||||
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
|
||||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
||||||
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Identifier',
|
|
||||||
'verbose_name_plural': 'External Identifiers',
|
|
||||||
'db_table': 'epdb_external_identifier',
|
|
||||||
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
|
||||||
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleAmbitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('smirks', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
|
||||||
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRDKitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRuleOrdering',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('order_index', models.IntegerField()),
|
|
||||||
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
|
||||||
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sequentialrule',
|
|
||||||
name='simple_rules',
|
|
||||||
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ParallelRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='compound',
|
|
||||||
unique_together={('uuid', 'package')},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AdditionalInformation',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('type', models.TextField(verbose_name='Additional Information Type')),
|
|
||||||
('data', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
|
||||||
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('scenario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scenario_additional_information', to='epdb.scenario')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [models.Index(fields=['type'], name='epdb_additi_type_394349_idx'), models.Index(fields=['scenario', 'type'], name='epdb_additi_scenari_a59edf_idx'), models.Index(fields=['content_type', 'object_id'], name='epdb_additi_content_44d4b4_idx'), models.Index(fields=['scenario', 'content_type', 'object_id'], name='epdb_additi_scenari_ef2bf5_idx')],
|
|
||||||
'constraints': [models.CheckConstraint(condition=models.Q(models.Q(('content_type__isnull', True), ('object_id__isnull', True)), models.Q(('content_type__isnull', False), ('object_id__isnull', False)), _connector='OR'), name='ck_addinfo_gfk_pair'), models.CheckConstraint(condition=models.Q(('scenario__isnull', False), ('content_type__isnull', False), _connector='OR'), name='ck_addinfo_not_both_null')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GroupPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'group')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserSettingPermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('setting', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,50 +1,49 @@
|
|||||||
# Generated by Django 6.0.3 on 2026-04-14 19:07
|
# Generated by Django 6.0.3 on 2026-04-21 11:43
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('epdb', '0022_alter_classifierpluginmodel_data_packages_and_more'),
|
("epdb", "0022_alter_classifierpluginmodel_data_packages_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='compoundstructure',
|
name="compoundstructure",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='epmodel',
|
name="epmodel",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='parallelrule',
|
name="parallelrule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='rule',
|
name="rule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='sequentialrule',
|
name="sequentialrule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='simpleambitrule',
|
name="simpleambitrule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='simplerdkitrule',
|
name="simplerdkitrule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='simplerule',
|
name="simplerule",
|
||||||
options={},
|
options={},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='group',
|
model_name="compoundstructure",
|
||||||
name='secret',
|
name="molfile",
|
||||||
field=models.BooleanField(default=False, verbose_name='Secret Group'),
|
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
17
epdb/migrations/0024_user_contacted.py
Normal file
17
epdb/migrations/0024_user_contacted.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-21 19:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0023_alter_compoundstructure_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="contacted",
|
||||||
|
field=models.BooleanField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
||||||
|
|
||||||
|
MAPPING = {
|
||||||
|
"": HalfLifeModel.OTHER,
|
||||||
|
"HS-SFO": HalfLifeModel.HS_SFO,
|
||||||
|
"FOMC": HalfLifeModel.FOMC,
|
||||||
|
"FOTC": HalfLifeModel.DFOP,
|
||||||
|
"FMOC": HalfLifeModel.FOMC,
|
||||||
|
"DFOP": HalfLifeModel.DFOP,
|
||||||
|
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
||||||
|
"first order kinetics": HalfLifeModel.SFO,
|
||||||
|
"SFO²": HalfLifeModel.SFO,
|
||||||
|
"HS": HalfLifeModel.HS,
|
||||||
|
"top down": HalfLifeModel.OTHER,
|
||||||
|
"SFO": HalfLifeModel.SFO,
|
||||||
|
"First Order": HalfLifeModel.SFO,
|
||||||
|
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
||||||
|
"true": HalfLifeModel.SFO,
|
||||||
|
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
||||||
|
|
||||||
|
for hl in hls:
|
||||||
|
data = hl.data
|
||||||
|
data["model"] = MAPPING[data["model"]].value
|
||||||
|
hl.data = HalfLife(**data).model_dump(mode="json")
|
||||||
|
hl.save()
|
||||||
|
|
||||||
|
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
||||||
|
|
||||||
|
for hl in hlws:
|
||||||
|
data = hl.data
|
||||||
|
data["model"] = MAPPING[data["model"]].value
|
||||||
|
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
||||||
|
hl.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0024_user_contacted"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-06-02 17:18
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import DOI
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
refs = AdditionalInformation.objects.filter(type="Reference")
|
||||||
|
|
||||||
|
remaining = []
|
||||||
|
|
||||||
|
for ref in refs:
|
||||||
|
r = ref.data["reference"]
|
||||||
|
try:
|
||||||
|
# PubMed IDs are plain ints, try parsing
|
||||||
|
_ = int(r)
|
||||||
|
# Nothing to do
|
||||||
|
except ValueError:
|
||||||
|
DOMAINS = [
|
||||||
|
"http://dx.doi.org/",
|
||||||
|
"https://dx.doi.org/",
|
||||||
|
"http://doi.org/",
|
||||||
|
"https://doi.org/",
|
||||||
|
]
|
||||||
|
for d in DOMAINS:
|
||||||
|
r = r.replace(d, "")
|
||||||
|
|
||||||
|
if r.startswith("10."):
|
||||||
|
ref.type = DOI.__name__
|
||||||
|
ref.data = {"doi": r}
|
||||||
|
ref.save()
|
||||||
|
else:
|
||||||
|
remaining.append(ref)
|
||||||
|
|
||||||
|
if len(remaining) > 0:
|
||||||
|
raise ValueError(f"Could not parse {len(remaining)} references")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0025_auto_20260511_2025"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-07-01 20:59
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0026_auto_20260602_1718"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="default_structure",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="compound_default_structure",
|
||||||
|
to="epdb.compoundstructure",
|
||||||
|
verbose_name="Default Structure",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="edge_label",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="epdb.reaction",
|
||||||
|
verbose_name="Edge label",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="node",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="node",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="medline_references",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Medline References",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="rules",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="reaction_rule", to="epdb.rule", verbose_name="Rule"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rule",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rule",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
355
epdb/models.py
355
epdb/models.py
@ -31,6 +31,10 @@ from sklearn.model_selection import ShuffleSplit
|
|||||||
|
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from bridge.dto import RunResult, PropertyPrediction
|
from bridge.dto import RunResult, PropertyPrediction
|
||||||
|
from epdb.exceptions import (
|
||||||
|
InvalidMolfileException,
|
||||||
|
InvalidSMILESException,
|
||||||
|
)
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
ApplicabilityDomainPCA,
|
||||||
@ -75,6 +79,7 @@ class User(AbstractUser):
|
|||||||
blank=False,
|
blank=False,
|
||||||
)
|
)
|
||||||
is_reviewer = models.BooleanField(default=False)
|
is_reviewer = models.BooleanField(default=False)
|
||||||
|
contacted = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
USERNAME_FIELD = "email"
|
||||||
REQUIRED_FIELDS = ["username"]
|
REQUIRED_FIELDS = ["username"]
|
||||||
@ -203,7 +208,6 @@ class Group(TimeStampedModel):
|
|||||||
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
||||||
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
||||||
public = models.BooleanField(verbose_name="Public Group", default=False)
|
public = models.BooleanField(verbose_name="Public Group", default=False)
|
||||||
secret = models.BooleanField(verbose_name="Secret Group", default=False)
|
|
||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
||||||
)
|
)
|
||||||
@ -631,7 +635,7 @@ class EnviPathModel(TimeStampedModel):
|
|||||||
|
|
||||||
class AliasMixin(models.Model):
|
class AliasMixin(models.Model):
|
||||||
aliases = ArrayField(
|
aliases = ArrayField(
|
||||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -654,7 +658,9 @@ class AliasMixin(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ScenarioMixin(models.Model):
|
class ScenarioMixin(models.Model):
|
||||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
scenarios = models.ManyToManyField(
|
||||||
|
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||||
@ -780,12 +786,19 @@ class Compound(
|
|||||||
"CompoundStructure",
|
"CompoundStructure",
|
||||||
verbose_name="Default Structure",
|
verbose_name="Default Structure",
|
||||||
related_name="compound_default_structure",
|
related_name="compound_default_structure",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
|
def get_structure_by_smiles(self, smiles: str) -> "CompoundStructure":
|
||||||
|
for struct in self.structures.all():
|
||||||
|
if struct.smiles == smiles:
|
||||||
|
return struct
|
||||||
|
|
||||||
|
raise ValueError(f"No structure with SMILES {smiles} found for {self.get_name()}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def structures(self) -> QuerySet:
|
def structures(self) -> QuerySet:
|
||||||
return CompoundStructure.objects.filter(compound=self)
|
return CompoundStructure.objects.filter(compound=self)
|
||||||
@ -854,16 +867,32 @@ class Compound(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
package: "Package",
|
||||||
|
smiles: str,
|
||||||
|
molfile: str | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles.strip() == "":
|
if smiles is None or smiles.strip() == "":
|
||||||
raise ValueError("SMILES is required")
|
raise InvalidSMILESException("SMILES is required")
|
||||||
|
|
||||||
smiles = smiles.strip()
|
smiles = smiles.strip()
|
||||||
|
|
||||||
parsed = FormatConverter.from_smiles(smiles)
|
parsed = FormatConverter.from_smiles(smiles)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
raise ValueError("Given SMILES is invalid")
|
raise InvalidSMILESException("Given SMILES is invalid")
|
||||||
|
|
||||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
|
|
||||||
@ -875,11 +904,16 @@ class Compound(
|
|||||||
if CompoundStructure.objects.filter(
|
if CompoundStructure.objects.filter(
|
||||||
smiles=standardized_smiles, compound__package=package
|
smiles=standardized_smiles, compound__package=package
|
||||||
).exists():
|
).exists():
|
||||||
# TODO should we add a structure?
|
found_c = CompoundStructure.objects.get(
|
||||||
return CompoundStructure.objects.get(
|
|
||||||
smiles=standardized_smiles, compound__package=package
|
smiles=standardized_smiles, compound__package=package
|
||||||
).compound
|
).compound
|
||||||
|
|
||||||
|
# As a direct match wasn't found add a new structure
|
||||||
|
# TODO return newly created structure
|
||||||
|
_ = found_c.add_structure(smiles, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
|
return found_c
|
||||||
|
|
||||||
# Generate Compound
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
c.package = package
|
c.package = package
|
||||||
@ -911,7 +945,12 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
c, smiles, name=name, description=description, normalized_structure=is_standardized
|
c,
|
||||||
|
smiles,
|
||||||
|
molfile=molfile,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
c.default_structure = cs
|
c.default_structure = cs
|
||||||
@ -924,11 +963,22 @@ class Compound(
|
|||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
|
molfile: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
default_structure: bool = False,
|
default_structure: bool = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "CompoundStructure":
|
) -> "CompoundStructure":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles == "":
|
if smiles is None or smiles == "":
|
||||||
raise ValueError("SMILES is required")
|
raise ValueError("SMILES is required")
|
||||||
|
|
||||||
@ -948,16 +998,28 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_standardized:
|
if is_standardized:
|
||||||
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
||||||
if CompoundStructure.objects.filter(
|
if CompoundStructure.objects.filter(smiles=smiles, compound__package=self.package).exists():
|
||||||
smiles__in=smiles, compound__package=self.package
|
found_cs = CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
).exists():
|
|
||||||
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
|
logger.info(
|
||||||
|
f"Setting molfile for found CompoundStructure(uuid={found_cs.uuid}) as it was empty"
|
||||||
|
)
|
||||||
|
found_cs.molfile = molfile
|
||||||
|
found_cs.save()
|
||||||
|
|
||||||
|
return found_cs
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
self, smiles, name=name, description=description, normalized_structure=is_standardized
|
self,
|
||||||
|
smiles,
|
||||||
|
name=name,
|
||||||
|
molfile=molfile,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
if default_structure:
|
if default_structure:
|
||||||
@ -1113,6 +1175,7 @@ class CompoundStructure(
|
|||||||
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
||||||
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
||||||
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
||||||
|
molfile = models.TextField(blank=True, null=True, verbose_name="Molfile")
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
@ -1137,10 +1200,35 @@ class CompoundStructure(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
compound: Compound, smiles: str, name: str = None, description: str = None, *args, **kwargs
|
compound: Compound,
|
||||||
|
smiles: str,
|
||||||
|
molfile: str = None,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
||||||
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
found_cs = CompoundStructure.objects.get(smiles=smiles, compound=compound)
|
||||||
|
|
||||||
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
|
logger.info(
|
||||||
|
f"Setting molfile for found CompoundStructure(uuid={found_cs.uuid}) as it was empty"
|
||||||
|
)
|
||||||
|
found_cs.molfile = molfile
|
||||||
|
found_cs.save()
|
||||||
|
|
||||||
|
return found_cs
|
||||||
|
|
||||||
if compound.pk is None:
|
if compound.pk is None:
|
||||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||||
@ -1153,8 +1241,12 @@ class CompoundStructure(
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
cs.smiles = smiles
|
|
||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
cs.smiles = smiles
|
||||||
|
|
||||||
|
# Check if molfile is present and valid
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
cs.molfile = molfile
|
||||||
|
|
||||||
if "normalized_structure" in kwargs:
|
if "normalized_structure" in kwargs:
|
||||||
cs.normalized_structure = kwargs["normalized_structure"]
|
cs.normalized_structure = kwargs["normalized_structure"]
|
||||||
@ -1173,6 +1265,8 @@ class CompoundStructure(
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self, width: int = 800, height: int = 400):
|
def as_svg(self, width: int = 800, height: int = 400):
|
||||||
|
if self.molfile is not None and self.molfile.strip() != "":
|
||||||
|
return IndigoUtils.mol_to_svg(self.molfile, width=width, height=height)
|
||||||
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1209,6 +1303,9 @@ class CompoundStructure(
|
|||||||
|
|
||||||
return dict(hls)
|
return dict(hls)
|
||||||
|
|
||||||
|
def d3_json(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
||||||
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
|
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
|
||||||
@ -1606,10 +1703,15 @@ class Reaction(
|
|||||||
products = models.ManyToManyField(
|
products = models.ManyToManyField(
|
||||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||||
)
|
)
|
||||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
rules = models.ManyToManyField(
|
||||||
|
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
||||||
|
)
|
||||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||||
medline_references = ArrayField(
|
medline_references = ArrayField(
|
||||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
models.TextField(blank=False, null=False),
|
||||||
|
null=True,
|
||||||
|
verbose_name="Medline References",
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
@ -1626,7 +1728,7 @@ class Reaction(
|
|||||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||||
products: Union[List[str], List[CompoundStructure]] = None,
|
products: Union[List[str], List[CompoundStructure]] = None,
|
||||||
rules: Union[Rule | List[Rule]] = None,
|
rules: Union[Rule | List[Rule]] = None,
|
||||||
multi_step: bool = True,
|
multi_step: bool = False,
|
||||||
):
|
):
|
||||||
_educts = []
|
_educts = []
|
||||||
_products = []
|
_products = []
|
||||||
@ -1691,7 +1793,7 @@ class Reaction(
|
|||||||
if name is not None and name.strip() != "":
|
if name is not None and name.strip() != "":
|
||||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if description is not None and name.strip() != "":
|
if description is not None and description.strip() != "":
|
||||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
r.multi_step = multi_step
|
r.multi_step = multi_step
|
||||||
@ -1769,7 +1871,7 @@ class Reaction(
|
|||||||
return new_reaction
|
return new_reaction
|
||||||
|
|
||||||
def smirks(self):
|
def smirks(self):
|
||||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
return f"{'.'.join([cs.smiles for cs in self.educts.all().order_by('-pk')])}>>{'.'.join([cs.smiles for cs in self.products.all().order_by('-pk')])}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
@ -1882,6 +1984,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
if n not in queue:
|
if n not in queue:
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
|
|
||||||
|
for i in queue:
|
||||||
|
processed.add(i)
|
||||||
|
|
||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
@ -1922,6 +2027,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
# add links start -> pseudo
|
# add links start -> pseudo
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -1931,6 +2037,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"source": node_url_to_idx[link["start_node_urls"][0]],
|
"source": node_url_to_idx[link["start_node_urls"][0]],
|
||||||
"target": pseudo_idx,
|
"target": pseudo_idx,
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
|
"to_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -1938,6 +2045,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
for target in link["end_node_urls"]:
|
for target in link["end_node_urls"]:
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -1948,6 +2056,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"target": node_url_to_idx[target],
|
"target": node_url_to_idx[target],
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
"multi_step": link["multi_step"],
|
"multi_step": link["multi_step"],
|
||||||
|
"from_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2157,11 +2266,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
def add_node(
|
def add_node(
|
||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
depth: Optional[int] = 0,
|
description: str | None = None,
|
||||||
|
depth: int = -1,
|
||||||
):
|
):
|
||||||
return Node.create(self, smiles, depth, name=name, description=description)
|
return Node.create(self, smiles, depth, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_edge(
|
def add_edge(
|
||||||
@ -2174,6 +2284,68 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
):
|
):
|
||||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||||
|
|
||||||
|
def update_depths(self):
|
||||||
|
# Collect number of in and out links per node
|
||||||
|
in_count = defaultdict(lambda: 0)
|
||||||
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
for e in self.edges:
|
||||||
|
for react in e.start_nodes.all():
|
||||||
|
out_count[str(react.uuid)] += 1
|
||||||
|
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
in_count[str(prod.uuid)] += 1
|
||||||
|
|
||||||
|
depth_map = {}
|
||||||
|
depth_map[0] = list()
|
||||||
|
processed = set()
|
||||||
|
|
||||||
|
data_driven_root_nodes = (
|
||||||
|
self.node_set.all()
|
||||||
|
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts"))
|
||||||
|
.filter(prod_cnt=0, educt_cnt__gt=0)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Eval QuerySet
|
||||||
|
root_nodes_by_depth = list(self.root_nodes)
|
||||||
|
|
||||||
|
data_driven_root_nodes.update(depth=0)
|
||||||
|
root_nodes = [n for n in data_driven_root_nodes]
|
||||||
|
|
||||||
|
for n in root_nodes_by_depth:
|
||||||
|
if n not in root_nodes:
|
||||||
|
if len(n.edge_products.all()) == 0:
|
||||||
|
root_nodes.append(n)
|
||||||
|
|
||||||
|
for n in root_nodes:
|
||||||
|
depth_map[0].append(n)
|
||||||
|
|
||||||
|
# At most depth len(nodes) is possible
|
||||||
|
for i in range(self.nodes.count()):
|
||||||
|
level_nodes = depth_map.get(i, [])
|
||||||
|
|
||||||
|
if len(level_nodes) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
unique_next_level = set()
|
||||||
|
for n in level_nodes:
|
||||||
|
processed.add(n)
|
||||||
|
for e in self.edges:
|
||||||
|
if n in e.start_nodes.all():
|
||||||
|
for p in e.end_nodes.all():
|
||||||
|
if p not in processed:
|
||||||
|
unique_next_level.add(p)
|
||||||
|
|
||||||
|
if len(unique_next_level) > 0:
|
||||||
|
depth_map[i + 1] = list(unique_next_level)
|
||||||
|
|
||||||
|
for depth, nodes in depth_map.items():
|
||||||
|
for n in nodes:
|
||||||
|
if n.depth != depth and depth != 0:
|
||||||
|
n.depth = depth
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
|
||||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2195,17 +2367,19 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name is None or self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.default_node_label.name} (taken from underlying structure)"
|
if include_suffix:
|
||||||
)
|
return f"{self.default_node_label.name} (taken from underlying structure)"
|
||||||
|
else:
|
||||||
|
return self.default_node_label.name
|
||||||
|
|
||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
app_domain_data = self.get_app_domain_assessment_data()
|
app_domain_data = self.get_app_domain_assessment_data()
|
||||||
@ -2215,16 +2389,27 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
if isinstance(ai.get(), PropertyPrediction):
|
if isinstance(ai.get(), PropertyPrediction):
|
||||||
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
||||||
|
|
||||||
return {
|
# If we have Subclasses of a CompoundStructure we can overwrite keys (e.g. images)
|
||||||
|
# by overwriting keys
|
||||||
|
structure_data = self.default_node_label.d3_json()
|
||||||
|
|
||||||
|
res = {
|
||||||
"depth": self.depth,
|
"depth": self.depth,
|
||||||
"stereo_removed": self.stereo_removed,
|
"stereo_removed": self.stereo_removed,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"node_label_id": self.default_node_label.url,
|
"node_label_id": self.default_node_label.url,
|
||||||
"image": f"{self.url}?image=svg",
|
"image": f"{self.url}?image=svg",
|
||||||
"image_svg": IndigoUtils.mol_to_svg(
|
"image_svg": IndigoUtils.mol_to_svg(
|
||||||
self.default_node_label.smiles, width=40, height=40
|
self.default_node_label.molfile
|
||||||
|
if self.default_node_label.molfile is not None
|
||||||
|
and self.default_node_label.molfile.strip()
|
||||||
|
else self.default_node_label.smiles,
|
||||||
|
width=40,
|
||||||
|
height=40,
|
||||||
),
|
),
|
||||||
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"smiles": self.default_node_label.smiles,
|
"smiles": self.default_node_label.smiles,
|
||||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
||||||
"app_domain": {
|
"app_domain": {
|
||||||
@ -2236,48 +2421,71 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"predicted_properties": predicted_properties,
|
"predicted_properties": predicted_properties,
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||||
"timeseries": self.get_timeseries_data(),
|
"timeseries": self.get_timeseries_data(),
|
||||||
|
**structure_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
pathway: "Pathway",
|
pathway: "Pathway",
|
||||||
smiles: str,
|
smiles: str,
|
||||||
depth: int,
|
depth: int,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
):
|
):
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
stereo_removed = False
|
stereo_removed = False
|
||||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
stereo_removed = True
|
stereo_removed = True
|
||||||
|
|
||||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
c = Compound.create(
|
||||||
|
pathway.package, smiles, molfile=molfile, name=name, description=description
|
||||||
|
)
|
||||||
|
|
||||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
structure = c.get_structure_by_smiles(smiles)
|
||||||
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
|
||||||
|
if Node.objects.filter(pathway=pathway, default_node_label=structure).exists():
|
||||||
|
return Node.objects.get(pathway=pathway, default_node_label=structure)
|
||||||
|
|
||||||
n = Node()
|
n = Node()
|
||||||
n.stereo_removed = stereo_removed
|
n.stereo_removed = stereo_removed
|
||||||
n.pathway = pathway
|
n.pathway = pathway
|
||||||
n.depth = depth
|
n.depth = depth
|
||||||
|
|
||||||
n.default_node_label = c.default_structure
|
n.default_node_label = structure
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
n.node_labels.add(c.default_structure)
|
n.node_labels.add(structure)
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
return n
|
return n
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
|
if (
|
||||||
|
self.default_node_label.molfile is not None
|
||||||
|
and self.default_node_label.molfile.strip() != ""
|
||||||
|
):
|
||||||
|
return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
|
||||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||||
|
|
||||||
def get_timeseries_data(self):
|
def get_timeseries_data(self):
|
||||||
for ai in self.additional_information.all():
|
for ai in self.additional_information.all():
|
||||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
if ai.type == "OECD301FTimeSeries":
|
||||||
return ai.model_dump(mode="json")
|
return ai.get().model_dump(mode="json")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -2308,13 +2516,35 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def is_proposed_intermediate(self):
|
||||||
|
collected = defaultdict(dict)
|
||||||
|
for ai in self.additional_information.filter(
|
||||||
|
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"],
|
||||||
|
scenario__isnull=False,
|
||||||
|
):
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||||
|
|
||||||
|
if ai.type == "ProposedIntermediate":
|
||||||
|
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||||
|
|
||||||
|
if ai.type == "Confidence":
|
||||||
|
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||||
|
|
||||||
|
if ai.type == "TransformationProductImportance":
|
||||||
|
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
||||||
|
ai.get().importance.value
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(collected.values())
|
||||||
|
|
||||||
|
|
||||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
edge_label = models.ForeignKey(
|
edge_label = models.ForeignKey(
|
||||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
start_nodes = models.ManyToManyField(
|
start_nodes = models.ManyToManyField(
|
||||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||||
@ -2329,6 +2559,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
edge_json = {
|
edge_json = {
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"id": self.url,
|
"id": self.url,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"image": self.url + "?image=svg",
|
"image": self.url + "?image=svg",
|
||||||
@ -2393,6 +2624,8 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
rule: Optional[Rule] = None,
|
rule: Optional[Rule] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
e = Edge()
|
e = Edge()
|
||||||
e.pathway = pathway
|
e.pathway = pathway
|
||||||
@ -2422,7 +2655,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||||
rules=rule,
|
rules=rule,
|
||||||
multi_step=False,
|
multi_step=kwargs.get("multi_step", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
e.edge_label = r
|
e.edge_label = r
|
||||||
@ -2441,17 +2674,19 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.edge_label.name} (taken from underlying reaction)"
|
if include_suffix:
|
||||||
)
|
return f"{self.edge_label.name} (taken from underlying reaction)"
|
||||||
|
else:
|
||||||
|
return self.edge_label.name
|
||||||
|
|
||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||||
@ -4430,18 +4665,22 @@ class AdditionalInformation(models.Model):
|
|||||||
|
|
||||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||||
|
|
||||||
def get(self) -> "EnviPyModel":
|
@staticmethod
|
||||||
|
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
try:
|
try:
|
||||||
inst = MAPPING[self.type](**self.data)
|
inst = MAPPING[ai_type](**ai_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading {self.type}: {e}")
|
print(f"Error loading {ai_type}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
inst.__dict__["uuid"] = str(self.uuid)
|
return inst
|
||||||
|
|
||||||
|
def get(self) -> "EnviPyModel":
|
||||||
|
inst = AdditionalInformation.from_dict(self.type, self.data)
|
||||||
|
inst.__dict__["uuid"] = str(self.uuid)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
139
epdb/views.py
139
epdb/views.py
@ -19,6 +19,7 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
|
from .exceptions import InvalidMolfileException, InvalidSMILESException
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
EPDBURLParser,
|
||||||
@ -587,38 +588,10 @@ def packages(request):
|
|||||||
"package-description", s.DEFAULT_VALUES["description"]
|
"package-description", s.DEFAULT_VALUES["description"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
data_pool = None
|
|
||||||
package_classification = request.POST.get("package-classification")
|
|
||||||
classification = Package.Classification(int(package_classification))
|
|
||||||
# For SECRET we'll need a data pool which will be an additional perm check later
|
|
||||||
if classification == Package.Classification.SECRET:
|
|
||||||
package_data_pool = request.POST.get("package-data-pool")
|
|
||||||
|
|
||||||
if package_data_pool is None:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool is required!")
|
|
||||||
|
|
||||||
data_pool = GroupManager.get_group_by_url(current_user, package_data_pool)
|
|
||||||
|
|
||||||
if data_pool is None:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool does not exist or no access!")
|
|
||||||
|
|
||||||
if not data_pool.secret:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool is not a secret group!")
|
|
||||||
|
|
||||||
created_package = PackageManager.create_package(
|
created_package = PackageManager.create_package(
|
||||||
current_user, package_name, package_description
|
current_user, package_name, package_description
|
||||||
)
|
)
|
||||||
|
|
||||||
created_package.classification_level = classification
|
|
||||||
|
|
||||||
# Set previously determined data pool
|
|
||||||
if classification == Package.Classification.SECRET:
|
|
||||||
created_package.data_pool = data_pool
|
|
||||||
|
|
||||||
created_package.save()
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
return redirect(created_package.url)
|
return redirect(created_package.url)
|
||||||
|
|
||||||
elif request.method == "OPTIONS":
|
elif request.method == "OPTIONS":
|
||||||
@ -782,6 +755,11 @@ def models(request):
|
|||||||
{"Model": s.SERVER_URL + "/model"},
|
{"Model": s.SERVER_URL + "/model"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
context["entity_type"] = "model"
|
||||||
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||||
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
|
context["list_title"] = "models"
|
||||||
|
|
||||||
# Keep model_types for potential modal/action use
|
# Keep model_types for potential modal/action use
|
||||||
context["model_types"] = {
|
context["model_types"] = {
|
||||||
"ML Relative Reasoning": {
|
"ML Relative Reasoning": {
|
||||||
@ -794,11 +772,13 @@ def models(request):
|
|||||||
"requires_rule_packages": True,
|
"requires_rule_packages": True,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
"EnviFormer": {
|
}
|
||||||
|
|
||||||
|
if s.ENVIFORMER_PRESENT:
|
||||||
|
context["model_types"]["EnviFormer"] = {
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
@ -807,6 +787,9 @@ def models(request):
|
|||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": v.requires_rule_packages(),
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": v.requires_data_packages(),
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
|
"additional_parameters": v.Config.__name__.lower()
|
||||||
|
if v.Config.__name__ != ""
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
for k, v in s.PROPERTY_PLUGINS.items():
|
for k, v in s.PROPERTY_PLUGINS.items():
|
||||||
context["model_types"][v.display()] = {
|
context["model_types"][v.display()] = {
|
||||||
@ -815,12 +798,6 @@ def models(request):
|
|||||||
"requires_data_packages": v.requires_data_packages(),
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Context for paginated template
|
|
||||||
context["entity_type"] = "model"
|
|
||||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "models"
|
|
||||||
|
|
||||||
return render(request, "collections/models_paginated.html", context)
|
return render(request, "collections/models_paginated.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
@ -934,11 +911,12 @@ def package_models(request, package_uuid):
|
|||||||
"requires_rule_packages": True,
|
"requires_rule_packages": True,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
"EnviFormer": {
|
}
|
||||||
|
if s.ENVIFORMER_PRESENT:
|
||||||
|
context["model_types"]["EnviFormer"] = {
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
@ -1115,6 +1093,11 @@ def package_model(request, package_uuid, model_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sort data by prob desc
|
||||||
|
res["pred"] = sorted(
|
||||||
|
res["pred"], key=lambda x: x["probability"], reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse(res, safe=False)
|
return JsonResponse(res, safe=False)
|
||||||
|
|
||||||
elif half_life:
|
elif half_life:
|
||||||
@ -1218,9 +1201,7 @@ def package(request, package_uuid):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
if request.GET.get("export", False) == "true":
|
if request.GET.get("export", False) == "true":
|
||||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||||
pack_json = PackageManager.export_package(
|
pack_json = PackageManager.export_package(current_package)
|
||||||
current_package, include_models=False, include_external_identifiers=False
|
|
||||||
)
|
|
||||||
response = JsonResponse(pack_json, content_type="application/json")
|
response = JsonResponse(pack_json, content_type="application/json")
|
||||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||||
|
|
||||||
@ -1403,12 +1384,18 @@ def package_compounds(request, package_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
compound_name = request.POST.get("compound-name")
|
compound_name = request.POST.get("compound-name")
|
||||||
compound_smiles = request.POST.get("compound-smiles")
|
compound_smiles = request.POST.get("compound-smiles")
|
||||||
|
compound_molfile = request.POST.get("compound-molfile")
|
||||||
compound_description = request.POST.get("compound-description")
|
compound_description = request.POST.get("compound-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
current_package, compound_smiles, compound_name, compound_description
|
current_package,
|
||||||
|
compound_smiles,
|
||||||
|
molfile=compound_molfile,
|
||||||
|
name=compound_name,
|
||||||
|
description=compound_description,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except (InvalidSMILESException, InvalidMolfileException) as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
@ -1534,11 +1521,15 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
structure_name = request.POST.get("structure-name")
|
structure_name = request.POST.get("structure-name")
|
||||||
structure_smiles = request.POST.get("structure-smiles")
|
structure_smiles = request.POST.get("structure-smiles")
|
||||||
|
structure_molfile = request.POST.get("structure-molfile")
|
||||||
structure_description = request.POST.get("structure-description")
|
structure_description = request.POST.get("structure-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cs = current_compound.add_structure(
|
cs = current_compound.add_structure(
|
||||||
structure_smiles, structure_name, structure_description
|
structure_smiles,
|
||||||
|
molfile=structure_molfile,
|
||||||
|
name=structure_name,
|
||||||
|
description=structure_description,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return error(
|
return error(
|
||||||
@ -2109,12 +2100,14 @@ def package_pathways(request, package_uuid):
|
|||||||
else:
|
else:
|
||||||
prediction_setting = current_user.prediction_settings()
|
prediction_setting = current_user.prediction_settings()
|
||||||
|
|
||||||
|
is_predict_mode = pw_mode in {"predict", "incremental"}
|
||||||
|
|
||||||
pw = Pathway.create(
|
pw = Pathway.create(
|
||||||
current_package,
|
current_package,
|
||||||
stand_smiles,
|
stand_smiles if is_predict_mode else smiles,
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
predicted=pw_mode in {"predict", "incremental"},
|
predicted=is_predict_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set mode
|
# set mode
|
||||||
@ -2356,7 +2349,16 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
|||||||
node_description = request.POST.get("node-description")
|
node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
node_smiles = request.POST.get("node-smiles").strip()
|
node_smiles = request.POST.get("node-smiles").strip()
|
||||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
node_molfile = request.POST.get("node-molfile").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_pathway.add_node(
|
||||||
|
node_smiles, molfile=node_molfile, name=node_name, description=node_description
|
||||||
|
)
|
||||||
|
except InvalidSMILESException:
|
||||||
|
return error(
|
||||||
|
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
||||||
|
)
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
@ -2464,7 +2466,26 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
return JsonResponse({"success": current_node.url})
|
return JsonResponse({"success": current_node.url})
|
||||||
|
|
||||||
return HttpResponseBadRequest()
|
new_node_name = request.POST.get("node-name")
|
||||||
|
new_node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
|
if any([new_node_name, new_node_description]):
|
||||||
|
if new_node_name is not None and new_node_name.strip() != "":
|
||||||
|
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
current_node.name = new_node_name
|
||||||
|
|
||||||
|
if new_node_description is not None and new_node_description.strip() != "":
|
||||||
|
new_node_description = nh3.clean(
|
||||||
|
new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
|
||||||
|
).strip()
|
||||||
|
current_node.description = new_node_description
|
||||||
|
|
||||||
|
current_node.save()
|
||||||
|
|
||||||
|
return redirect(current_node.url)
|
||||||
|
|
||||||
|
return error(request, "Node update failed!", "No changes were made to the node")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
|
|
||||||
@ -2534,6 +2555,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
|||||||
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update depths as sideeffect of above operation
|
||||||
|
current_pathway.update_depths()
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -3105,12 +3129,21 @@ def jobs(request):
|
|||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||||
]
|
]
|
||||||
if current_user.is_superuser:
|
# if current_user.is_superuser:
|
||||||
context["jobs"] = JobLog.objects.all().order_by("-created")
|
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||||
else:
|
# else:
|
||||||
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
|
|
||||||
return render(request, "collections/joblog.html", context)
|
# Context for paginated template
|
||||||
|
context["entity_type"] = "joblog"
|
||||||
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
|
||||||
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
|
context["list_title"] = "joblog"
|
||||||
|
context["list_mode"] = "combined"
|
||||||
|
|
||||||
|
return render(request, "collections/joblog_paginated.html", context)
|
||||||
|
|
||||||
|
# return render(request, "collections/joblog.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
job_name = request.POST.get("job-name")
|
job_name = request.POST.get("job-name")
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -65,6 +65,25 @@ def run_both_engines(SMILES, SMIRKS):
|
|||||||
|
|
||||||
|
|
||||||
def migration(request):
|
def migration(request):
|
||||||
|
accepted_diffs = [
|
||||||
|
"bt0055-3469.1",
|
||||||
|
"bt0056-2685",
|
||||||
|
"bt0193-4263",
|
||||||
|
"bt0231-1871.1",
|
||||||
|
"bt0242-3803",
|
||||||
|
"bt0254-4224.1",
|
||||||
|
"bt0254-4224.2",
|
||||||
|
"bt0291-1129",
|
||||||
|
"bt0337-3543",
|
||||||
|
"bt0391-4285",
|
||||||
|
"bt0402-3576",
|
||||||
|
"bt0404-3928",
|
||||||
|
"bt0416-4269",
|
||||||
|
"bt0432-4254",
|
||||||
|
"bt0337-4117",
|
||||||
|
"bt0322-3393",
|
||||||
|
]
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
@ -109,11 +128,11 @@ def migration(request):
|
|||||||
),
|
),
|
||||||
"id": str(r.uuid),
|
"id": str(r.uuid),
|
||||||
"url": r.url,
|
"url": r.url,
|
||||||
"status": res,
|
"status": res or r.name in accepted_diffs,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if res:
|
if res or r.name in accepted_diffs:
|
||||||
success += 1
|
success += 1
|
||||||
else:
|
else:
|
||||||
error += 1
|
error += 1
|
||||||
@ -135,7 +154,16 @@ def migration(request):
|
|||||||
|
|
||||||
for r in migration_status["results"]:
|
for r in migration_status["results"]:
|
||||||
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
||||||
|
if r["name"] in accepted_diffs:
|
||||||
|
r["status"] = True
|
||||||
|
|
||||||
|
migration_status["results"] = sorted(
|
||||||
|
migration_status["results"], key=lambda x: (x["status"], x["name"])
|
||||||
|
)
|
||||||
|
|
||||||
|
num_success = sum([int(x["status"]) for x in migration_status["results"]])
|
||||||
|
migration_status["success"] = num_success
|
||||||
|
migration_status["error"] = migration_status["total"] - num_success
|
||||||
context.update(**migration_status)
|
context.update(**migration_status)
|
||||||
|
|
||||||
return render(request, "migration.html", context)
|
return render(request, "migration.html", context)
|
||||||
|
|||||||
@ -21,5 +21,11 @@
|
|||||||
"django",
|
"django",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"daisyui"
|
"daisyui"
|
||||||
|
],
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"@tailwindcss/oxide"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
|
|
||||||
import matplotlib.patches as mpatches
|
import matplotlib.patches as mpatches
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib import pyplot as plt
|
from matplotlib.figure import Figure
|
||||||
from scipy import stats
|
from scipy import stats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -101,7 +101,8 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
mask_red = x > vp
|
mask_red = x > vp
|
||||||
|
|
||||||
# Plot
|
# Plot
|
||||||
fig, ax = plt.subplots(figsize=(9, 5.5))
|
fig = Figure(figsize=(9, 5.5))
|
||||||
|
ax = fig.subplots()
|
||||||
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
||||||
|
|
||||||
if np.any(mask_green):
|
if np.any(mask_green):
|
||||||
@ -146,13 +147,12 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
]
|
]
|
||||||
ax.legend(handles=patches, frameon=True)
|
ax.legend(handles=patches, frameon=True)
|
||||||
|
|
||||||
plt.tight_layout()
|
fig.tight_layout()
|
||||||
|
|
||||||
# --- Export to SVG string ---
|
# --- Export to SVG string ---
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||||
svg = buf.getvalue()
|
svg = buf.getvalue()
|
||||||
plt.close(fig)
|
|
||||||
buf.close()
|
buf.close()
|
||||||
|
|
||||||
return svg
|
return svg
|
||||||
|
|||||||
@ -187,8 +187,9 @@ class Pepper:
|
|||||||
groups = [group for group in dataset.group_by("structure_id")]
|
groups = [group for group in dataset.group_by("structure_id")]
|
||||||
|
|
||||||
# Unless explicitly set compute everything serial
|
# Unless explicitly set compute everything serial
|
||||||
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
|
||||||
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
if n_threads > 1:
|
||||||
|
results = Parallel(n_jobs=n_threads)(
|
||||||
delayed(compute_bayes_per_group)(group[1])
|
delayed(compute_bayes_per_group)(group[1])
|
||||||
for group in dataset.group_by("structure_id")
|
for group in dataset.group_by("structure_id")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- '@parcel/watcher'
|
- '@parcel/watcher'
|
||||||
- '@tailwindcss/oxide'
|
- '@tailwindcss/oxide'
|
||||||
|
|||||||
@ -34,3 +34,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "./daisyui-theme.css";
|
@import "./daisyui-theme.css";
|
||||||
|
|
||||||
|
select.select[multiple] {
|
||||||
|
display: block;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|||||||
@ -59,6 +59,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
get isEditMode() {
|
get isEditMode() {
|
||||||
return this.mode === "edit";
|
return this.mode === "edit";
|
||||||
},
|
},
|
||||||
|
get isRequired() {
|
||||||
|
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text widget
|
// Text widget
|
||||||
@ -293,6 +296,34 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PubMed link widget
|
||||||
|
Alpine.data(
|
||||||
|
"doiWidget",
|
||||||
|
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||||||
|
...baseWidget(
|
||||||
|
fieldName,
|
||||||
|
data,
|
||||||
|
schema,
|
||||||
|
uiSchema,
|
||||||
|
mode,
|
||||||
|
debugErrors,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.data[this.fieldName] || "";
|
||||||
|
},
|
||||||
|
set value(v) {
|
||||||
|
this.data[this.fieldName] = v;
|
||||||
|
},
|
||||||
|
get doiUrl() {
|
||||||
|
return this.value
|
||||||
|
? `https://dx.doi.org/${this.value}`
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Compound link widget
|
// Compound link widget
|
||||||
Alpine.data(
|
Alpine.data(
|
||||||
"compoundWidget",
|
"compoundWidget",
|
||||||
|
|||||||
@ -5,6 +5,126 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
|
||||||
|
const basePagination = (
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId
|
||||||
|
) => ({
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId,
|
||||||
|
|
||||||
|
get paginatedItems() {
|
||||||
|
return this.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingStart() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return (this.currentPage - 1) * this.perPage + 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingEnd() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||||
|
},
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.fetchPage(this.currentPage + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.fetchPage(this.currentPage - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
if (page >= 1 && page <= this.totalPages) {
|
||||||
|
this.fetchPage(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get pageNumbers() {
|
||||||
|
const pages = [];
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.currentPage;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||||
|
|
||||||
|
let rangeStart;
|
||||||
|
let rangeEnd;
|
||||||
|
|
||||||
|
if (current <= 4) {
|
||||||
|
rangeStart = 2;
|
||||||
|
rangeEnd = 5;
|
||||||
|
} else if (current >= total - 3) {
|
||||||
|
rangeStart = total - 4;
|
||||||
|
rangeEnd = total - 1;
|
||||||
|
} else {
|
||||||
|
rangeStart = current - 1;
|
||||||
|
rangeEnd = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart > 2) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd < total - 1) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Alpine.data('paginatedList',
|
||||||
|
(items, options = {}) => ({
|
||||||
|
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchPage(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPage(page) {
|
||||||
|
this.totalItems = this.items.length;
|
||||||
|
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||||
|
this.currentPage = page
|
||||||
|
|
||||||
|
const start = page * this.perPage - this.perPage;
|
||||||
|
const end = page * this.perPage;
|
||||||
|
return this.items.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||||
items: [],
|
items: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|||||||
265
static/js/pw.js
265
static/js/pw.js
@ -1,5 +1,37 @@
|
|||||||
console.log("loaded pw.js")
|
console.log("loaded pw.js")
|
||||||
|
|
||||||
|
function findPaths(source_idx, links) {
|
||||||
|
const resultLinks = new Set();
|
||||||
|
const visited = new Set();
|
||||||
|
// Helper function for depth-first search
|
||||||
|
function dfs(current) {
|
||||||
|
|
||||||
|
if (visited.has(current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.filter(link => link.target.id === current).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(current);
|
||||||
|
links.forEach(link => {
|
||||||
|
if (
|
||||||
|
link.target.id === current &&
|
||||||
|
!visited.has(link.source.id) &&
|
||||||
|
link.source.id !== link.target.id // Avoid self-loops
|
||||||
|
) {
|
||||||
|
resultLinks.add(link); // Add link to result
|
||||||
|
dfs(link.source.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
visited.delete(current);
|
||||||
|
}
|
||||||
|
// Start DFS
|
||||||
|
dfs(source_idx);
|
||||||
|
return Array.from(resultLinks);
|
||||||
|
}
|
||||||
|
|
||||||
function predictFromNode(url) {
|
function predictFromNode(url) {
|
||||||
fetch("", {
|
fetch("", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -23,6 +55,7 @@ function predictFromNode(url) {
|
|||||||
// elem = 'vizdiv'
|
// elem = 'vizdiv'
|
||||||
function draw(pathway, elem) {
|
function draw(pathway, elem) {
|
||||||
|
|
||||||
|
const initialzoom = 2.5
|
||||||
const nodeRadius = 20;
|
const nodeRadius = 20;
|
||||||
const linkDistance = 100;
|
const linkDistance = 100;
|
||||||
const chargeStrength = -200;
|
const chargeStrength = -200;
|
||||||
@ -34,6 +67,9 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
const depthMap = new Map();
|
||||||
|
|
||||||
|
// Avoid leaving unconnected Nodes leaving the viewport
|
||||||
|
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||||
|
|
||||||
// Sort nodes by depth first to minimize crossings
|
// Sort nodes by depth first to minimize crossings
|
||||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||||
|
|
||||||
@ -63,7 +99,7 @@ function draw(pathway, elem) {
|
|||||||
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.fy = node.depth * levelSpacing + 50;
|
node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
|
||||||
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -101,12 +137,31 @@ function draw(pathway, elem) {
|
|||||||
// Update pseudo node positions first
|
// Update pseudo node positions first
|
||||||
updatePseudoNodePositions();
|
updatePseudoNodePositions();
|
||||||
|
|
||||||
link.attr("x1", d => d.source.x)
|
link.attr("d", d => {
|
||||||
.attr("y1", d => d.source.y)
|
// Check if it's a self-loop (source equals target)
|
||||||
.attr("x2", d => d.target.x)
|
if (d.source.id === d.target.id) {
|
||||||
.attr("y2", d => d.target.y);
|
// Create a bezier curve for self-loops
|
||||||
|
const x = d.source.x;
|
||||||
|
const y = d.source.y;
|
||||||
|
const loopRadius = nodeRadius * 2; // Adjust size of the loop
|
||||||
|
|
||||||
|
// Create a circular path to the left of the node
|
||||||
|
return `M ${x},${y - nodeRadius}
|
||||||
|
C ${x - loopRadius},${y - nodeRadius - loopRadius}
|
||||||
|
${x - loopRadius},${y + nodeRadius + loopRadius}
|
||||||
|
${x},${y + nodeRadius}`;
|
||||||
|
} else {
|
||||||
|
// Regular straight line for normal edges
|
||||||
|
return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
|
|
||||||
|
linkText
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
@ -209,7 +264,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait before showing popup (ms)
|
||||||
var popupWaitBeforeShow = 1000;
|
var popupWaitBeforeShow = 750;
|
||||||
|
|
||||||
// Custom popover element
|
// Custom popover element
|
||||||
let popoverTimeout = null;
|
let popoverTimeout = null;
|
||||||
@ -441,7 +496,7 @@ function draw(pathway, elem) {
|
|||||||
if (n.stereo_removed) {
|
if (n.stereo_removed) {
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||||
}
|
}
|
||||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
@ -463,7 +518,7 @@ function draw(pathway, elem) {
|
|||||||
// TODO needs to be generic once we store it as AddInf
|
// TODO needs to be generic once we store it as AddInf
|
||||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||||
if (s["mean"] != null) {
|
if (s["mean"] != null) {
|
||||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -481,6 +536,21 @@ function draw(pathway, elem) {
|
|||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||||
|
for (var prop in n.proposed) {
|
||||||
|
if (n.proposed[prop].scenarioId === s.url) {
|
||||||
|
if("proposed" in n.proposed[prop]){
|
||||||
|
popupContent += "This compound is a proposed intermediate" + "<br>";
|
||||||
|
}
|
||||||
|
if("Confidence" in n.proposed[prop]){
|
||||||
|
popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
|
||||||
|
}
|
||||||
|
if("Transformation product importance" in n.proposed[prop]){
|
||||||
|
popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContent += "<br>";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,7 +563,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edge_popup(e) {
|
function edge_popup(e) {
|
||||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";
|
||||||
|
|
||||||
if (e.reaction.rules) {
|
if (e.reaction.rules) {
|
||||||
for (var rule of e.reaction.rules) {
|
for (var rule of e.reaction.rules) {
|
||||||
@ -544,7 +614,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
// Add background rectangle FIRST to enable pan/zoom on empty space
|
||||||
// This must be inserted before zoomable group so it's behind everything
|
// This must be inserted before zoomable group so it's behind everything
|
||||||
svg.insert("rect", "#zoomable")
|
const rect = svg.insert("rect", "#zoomable")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
@ -553,6 +623,30 @@ function draw(pathway, elem) {
|
|||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
.style("cursor", "grab");
|
.style("cursor", "grab");
|
||||||
|
|
||||||
|
// Click on empty SVG space (not on a node or link)
|
||||||
|
rect.on("click", function(event) {
|
||||||
|
// Only treat it as a background click if the direct target is the rect itself
|
||||||
|
// (clicks on nodes/links bubble up but originate from different elements)
|
||||||
|
const target = event.target;
|
||||||
|
const isNode = target.closest && (
|
||||||
|
target.closest("circle") !== null ||
|
||||||
|
target.closest("image") !== null ||
|
||||||
|
target.closest(".node") !== null
|
||||||
|
);
|
||||||
|
const isLink = target.closest && (
|
||||||
|
target.closest("path.link") !== null ||
|
||||||
|
target.closest("path.link_no_arrow") !== null
|
||||||
|
);
|
||||||
|
if (!isNode && !isLink) {
|
||||||
|
// console.log("Clicked outside a node or link");
|
||||||
|
// Clear all highlights
|
||||||
|
d3.selectAll("circle").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom Funktion aktivieren
|
// Zoom Funktion aktivieren
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([0.5, 5])
|
.scaleExtent([0.5, 5])
|
||||||
@ -563,6 +657,8 @@ function draw(pathway, elem) {
|
|||||||
// Apply zoom to the SVG element - this enables wheel zoom
|
// Apply zoom to the SVG element - this enables wheel zoom
|
||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
|
|
||||||
|
svg.call(zoom.scaleBy, initialzoom);
|
||||||
|
|
||||||
// Also apply zoom to container to catch events that might not reach SVG
|
// Also apply zoom to container to catch events that might not reach SVG
|
||||||
// This ensures drag-to-pan works even when clicking on empty space
|
// This ensures drag-to-pan works even when clicking on empty space
|
||||||
container.call(zoom);
|
container.call(zoom);
|
||||||
@ -581,11 +677,11 @@ function draw(pathway, elem) {
|
|||||||
for (idx in parents) {
|
for (idx in parents) {
|
||||||
p = nodes[parents[idx]]
|
p = nodes[parents[idx]]
|
||||||
// console.log(p.depth)
|
// console.log(p.depth)
|
||||||
if (p.depth >= n.depth) {
|
// if (p.depth >= n.depth) {
|
||||||
// keep the .5 steps for pseudo nodes
|
// // keep the .5 steps for pseudo nodes
|
||||||
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||||
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -597,15 +693,83 @@ function draw(pathway, elem) {
|
|||||||
.force("center", d3.forceCenter(width / 2, height / 4))
|
.force("center", d3.forceCenter(width / 2, height / 4))
|
||||||
.on("tick", ticked);
|
.on("tick", ticked);
|
||||||
|
|
||||||
// Kanten zeichnen
|
// Draw Edges
|
||||||
const link = zoomable.append("g")
|
const linkGroup = zoomable.append("g")
|
||||||
.selectAll("line")
|
.selectAll("g")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter().append("line")
|
.enter()
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "link-group");
|
||||||
|
|
||||||
|
const link = linkGroup
|
||||||
|
.append("path")
|
||||||
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
|
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||||
|
.attr("fill", "none")
|
||||||
|
|
||||||
// Check if target is pseudo and draw marker only if not pseudo
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
|
.attr("marker-end", d => {
|
||||||
|
if (d.target.pseudo) return '';
|
||||||
|
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||||
|
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.on("click", function(event, d) {
|
||||||
|
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
|
if (!wasHighlighted) {
|
||||||
|
const toHighlight = [];
|
||||||
|
toHighlight.push(d.el);
|
||||||
|
|
||||||
|
if (d.source.pseudo || d.target.pseudo) {
|
||||||
|
if (d.target.pseudo) {
|
||||||
|
d3.selectAll("path").each(e => {
|
||||||
|
if (e !== undefined && e.source.id === d.target.id) {
|
||||||
|
toHighlight.push(e.el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
d3.selectAll("path").each(e => {
|
||||||
|
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
||||||
|
toHighlight.push(e.el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of toHighlight) {
|
||||||
|
d3.select(e).classed("highlighted", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function reaction_name(d) {
|
||||||
|
const to_pseudo = d?.to_pseudo || false;
|
||||||
|
|
||||||
|
if (to_pseudo) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "";
|
||||||
|
if ("reaction_probability" in d && d["reaction_probability"] !== null) {
|
||||||
|
suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
|
||||||
|
}
|
||||||
|
return d.plain_name + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkText = linkGroup
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "link-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("dy", -6)
|
||||||
|
.style("font-size", "4px")
|
||||||
|
.style("pointer-events", "none")
|
||||||
|
.style("display", "none")
|
||||||
|
.text(d => reaction_name(d))
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
@ -614,7 +778,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
pop_add(link, "Reaction", edge_popup);
|
pop_add(link, "Reaction", edge_popup);
|
||||||
|
|
||||||
// Knoten zeichnen
|
// Draw Nodes
|
||||||
const node = zoomable.append("g")
|
const node = zoomable.append("g")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
@ -622,10 +786,41 @@ function draw(pathway, elem) {
|
|||||||
.call(d3.drag()
|
.call(d3.drag()
|
||||||
.on("start", dragstarted)
|
.on("start", dragstarted)
|
||||||
.on("drag", dragged)
|
.on("drag", dragged)
|
||||||
.on("end", dragended))
|
.on("end", dragended)
|
||||||
|
)
|
||||||
.on("click", function (event, d) {
|
.on("click", function (event, d) {
|
||||||
|
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||||
|
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
|
||||||
|
if (!wasHighlighted) {
|
||||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||||
})
|
const inEdges = findPaths(d.id, links);
|
||||||
|
const outEdges = []
|
||||||
|
// Colorize out edges green
|
||||||
|
for (const l of links) {
|
||||||
|
if (l.source.id === d.id) {
|
||||||
|
outEdges.push(l);
|
||||||
|
if (l.target.pseudo) {
|
||||||
|
for (const l2 of links) {
|
||||||
|
if (l.target.id === l2.source.id) {
|
||||||
|
outEdges.push(l2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of inEdges) {
|
||||||
|
d3.select(e.el).classed("inedge", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of outEdges) {
|
||||||
|
d3.select(e.el).classed("outedge", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Kreise für die Knoten hinzufügen
|
// Kreise für die Knoten hinzufügen
|
||||||
node.append("circle")
|
node.append("circle")
|
||||||
@ -637,6 +832,7 @@ function draw(pathway, elem) {
|
|||||||
node.filter(d => !d.pseudo).each(function (d, i) {
|
node.filter(d => !d.pseudo).each(function (d, i) {
|
||||||
const g = d3.select(this);
|
const g = d3.select(this);
|
||||||
|
|
||||||
|
if (d.image_type === "svg") {
|
||||||
// Parse the SVG string
|
// Parse the SVG string
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const svgDoc = parser.parseFromString(d.image_svg, "image/svg+xml");
|
const svgDoc = parser.parseFromString(d.image_svg, "image/svg+xml");
|
||||||
@ -646,19 +842,19 @@ function draw(pathway, elem) {
|
|||||||
const prefix = `node-${i}-`;
|
const prefix = `node-${i}-`;
|
||||||
|
|
||||||
// Rename all IDs and fix <use> references
|
// Rename all IDs and fix <use> references
|
||||||
svgElem.querySelectorAll('[id]').forEach(el => {
|
svgElem.querySelectorAll("[id]").forEach(el => {
|
||||||
const oldId = el.id;
|
const oldId = el.id;
|
||||||
const newId = prefix + oldId;
|
const newId = prefix + oldId;
|
||||||
el.id = newId;
|
el.id = newId;
|
||||||
|
|
||||||
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||||
// Update <use> elements that reference this old ID
|
// Update <use> elements that reference this old ID
|
||||||
const uses = Array.from(svgElem.querySelectorAll('use')).filter(
|
const uses = Array.from(svgElem.querySelectorAll("use")).filter(
|
||||||
u => u.getAttributeNS(XLINK_NS, 'href') === `#${oldId}`
|
u => u.getAttributeNS(XLINK_NS, "href") === `#${oldId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
uses.forEach(u => {
|
uses.forEach(u => {
|
||||||
u.setAttributeNS(XLINK_NS, 'href', `#${newId}`);
|
u.setAttributeNS(XLINK_NS, "href", `#${newId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -675,6 +871,17 @@ function draw(pathway, elem) {
|
|||||||
.attr("height", svgHeight * scale)
|
.attr("height", svgHeight * scale)
|
||||||
.attr("x", -svgWidth * scale / 2)
|
.attr("x", -svgWidth * scale / 2)
|
||||||
.attr("y", -svgHeight * scale / 2);
|
.attr("y", -svgHeight * scale / 2);
|
||||||
|
} else {
|
||||||
|
// We have a image type different than svg
|
||||||
|
// include it via img url
|
||||||
|
g.append("svg:image")
|
||||||
|
.attr("xlink:href", d.image)
|
||||||
|
.attr("width", 40)
|
||||||
|
.attr("height", 40)
|
||||||
|
.attr("x", -20)
|
||||||
|
.attr("y", -20);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// add element to nodes array
|
// add element to nodes array
|
||||||
@ -687,14 +894,14 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
function serializeSVG(svgElement) {
|
function serializeSVG(svgElement) {
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link").forEach(line => {
|
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
line.setAttribute("fill", style.fill);
|
line.setAttribute("fill", style.fill);
|
||||||
});
|
});
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@ -15,7 +17,12 @@
|
|||||||
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a
|
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider"></li>
|
{% epdb_slot_templates "epdb.actions.objects.pathway.add" as action_button_templates %}
|
||||||
|
|
||||||
|
{% for tpl in action_button_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
|
<li role="separator" class="divider h-px"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@ -67,7 +74,7 @@
|
|||||||
Rules</a
|
Rules</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider h-px"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
@ -92,11 +99,16 @@
|
|||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider h-px"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
|
onclick="
|
||||||
|
const modal = document.getElementById('delete_pathway_node_modal');
|
||||||
|
modal.showModal();
|
||||||
|
window.dispatchEvent(new Event('modal-opened'));
|
||||||
|
return false;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
||||||
>
|
>
|
||||||
@ -104,7 +116,12 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
|
onclick="
|
||||||
|
const modal = document.getElementById('delete_pathway_edge_modal');
|
||||||
|
modal.showModal();
|
||||||
|
window.dispatchEvent(new Event('modal-opened'));
|
||||||
|
return false;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
||||||
>
|
>
|
||||||
|
|||||||
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
|
||||||
|
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
||||||
|
{% load envipytags %}
|
||||||
|
{# Loading state #}
|
||||||
|
<div
|
||||||
|
x-show="isLoading"
|
||||||
|
class="mx-auto flex h-32 w-32 items-center justify-center"
|
||||||
|
>
|
||||||
|
{% include "components/loading-spinner.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Error state #}
|
||||||
|
<div
|
||||||
|
x-show="!isLoading && error"
|
||||||
|
class="alert alert-error/50 text-sm"
|
||||||
|
x-text="error"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{# Content #}
|
||||||
|
<template x-if="!isLoading && !error">
|
||||||
|
<div>
|
||||||
|
{# Empty state #}
|
||||||
|
<div
|
||||||
|
x-show="totalItems === 0"
|
||||||
|
class="text-base-content/70 py-8 text-center"
|
||||||
|
>
|
||||||
|
<p>No {{ empty_text|default:"items" }} found.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Items list #}
|
||||||
|
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
|
||||||
|
<table class="table-zebra table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Queued At</th>
|
||||||
|
<th>Done At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="obj in paginatedItems" :key="obj.url">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a :href="obj.user.url"><span x-text="obj.user.name"></span></a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a :href="obj.url"><span x-text="obj.id"></span></a>
|
||||||
|
</td>
|
||||||
|
<td><span x-text="obj.name"></span></td>
|
||||||
|
<td><span x-text="obj.status"></span></td>
|
||||||
|
<td><span x-text="obj.created"></span></td>
|
||||||
|
<td><span x-text="obj.done"></span></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{# Pagination controls #}
|
||||||
|
<div
|
||||||
|
x-show="totalPages > 1"
|
||||||
|
class="mt-4 flex items-center justify-between px-2"
|
||||||
|
>
|
||||||
|
<span class="text-base-content/70 text-sm">
|
||||||
|
Showing <span x-text="showingStart"></span>-<span
|
||||||
|
x-text="showingEnd"
|
||||||
|
></span>
|
||||||
|
of <span x-text="totalItems"></span>
|
||||||
|
</span>
|
||||||
|
<div class="join">
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
@click="prevPage()"
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
<template x-for="item in pageNumbers" :key="item.key">
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:class="{ 'btn-active': item.page === currentPage }"
|
||||||
|
:disabled="item.isEllipsis"
|
||||||
|
@click="!item.isEllipsis && goToPage(item.page)"
|
||||||
|
x-text="item.page"
|
||||||
|
></button>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
@click="nextPage()"
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
13
templates/collections/joblog_paginated.html
Normal file
13
templates/collections/joblog_paginated.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "collections/paginated_base.html" %}
|
||||||
|
|
||||||
|
{% block page_title %}Jobs{% endblock %}
|
||||||
|
|
||||||
|
{% block action_button %}
|
||||||
|
{% endblock action_button %}
|
||||||
|
|
||||||
|
{% block action_modals %}
|
||||||
|
{% endblock action_modals %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
<p>List of Jobs submitted.</p>
|
||||||
|
{% endblock description %}
|
||||||
@ -3,7 +3,8 @@
|
|||||||
{% block page_title %}Models{% endblock %}
|
{% block page_title %}Models{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
|
{% if meta.enabled_features.MODEL_BUILDING %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
@ -12,6 +13,7 @@
|
|||||||
New Model
|
New Model
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
{% block page_title %}Packages{% endblock %}
|
{% block page_title %}Packages{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -71,7 +70,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
|
|||||||
@ -37,7 +37,11 @@
|
|||||||
perPage: {{ per_page|default:50 }}
|
perPage: {{ per_page|default:50 }}
|
||||||
})"
|
})"
|
||||||
>
|
>
|
||||||
|
{% if entity_type == 'joblog' %}
|
||||||
|
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||||
|
{% else %}
|
||||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Pathways{% endblock %}
|
{% block page_title %}Pathways{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Reactions{% endblock %}
|
{% block page_title %}Reactions{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}Rules{% endblock %}
|
{% block page_title %}Rules{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -123,6 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- DOI link widget -->
|
||||||
|
<template
|
||||||
|
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'doi-link'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
x-data="doiWidget(fieldName, data, schema, uiSchema, mode, debugErrors, context)"
|
||||||
|
>
|
||||||
|
{% include "components/widgets/doi_link_widget.html" %}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Compound link widget -->
|
<!-- Compound link widget -->
|
||||||
<template
|
<template
|
||||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
||||||
|
|||||||
69
templates/components/widgets/doi_link_widget.html
Normal file
69
templates/components/widgets/doi_link_widget.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{# DOI link widget - pure HTML template #}
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
|
||||||
|
<!-- Label -->
|
||||||
|
<label class="label sm:w-48 sm:shrink-0">
|
||||||
|
<span
|
||||||
|
class="label-text"
|
||||||
|
:class="{
|
||||||
|
'text-error': $store.validationErrors.hasError(fieldName, context),
|
||||||
|
'text-sm text-base-content/60': isViewMode
|
||||||
|
}"
|
||||||
|
x-text="label"
|
||||||
|
></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Input column -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<!-- Help text -->
|
||||||
|
<template x-if="helpText">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text-alt text-base-content/60"
|
||||||
|
x-text="helpText"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- View mode: display as link -->
|
||||||
|
<template x-if="isViewMode">
|
||||||
|
<div class="mt-1">
|
||||||
|
<template x-if="value && doiUrl">
|
||||||
|
<a
|
||||||
|
:href="doiUrl"
|
||||||
|
class="link link-primary"
|
||||||
|
target="_blank"
|
||||||
|
x-text="value"
|
||||||
|
></a>
|
||||||
|
</template>
|
||||||
|
<template x-if="!value">
|
||||||
|
<span class="text-base-content/50">—</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Edit mode -->
|
||||||
|
<template x-if="isEditMode">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
:class="{ 'input-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||||
|
placeholder="DOI e.g. 10.1016/j.jhazmat.2016.08.036"
|
||||||
|
x-model="value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<template x-if="$store.validationErrors.hasError(fieldName, context)">
|
||||||
|
<div class="label">
|
||||||
|
<template
|
||||||
|
x-for="errMsg in $store.validationErrors.getErrors(fieldName, context)"
|
||||||
|
:key="errMsg"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error" x-text="errMsg"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -44,6 +44,7 @@
|
|||||||
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||||
x-model="value"
|
x-model="value"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
|
:required="isRequired"
|
||||||
>
|
>
|
||||||
<option value="" :selected="!value">Select...</option>
|
<option value="" :selected="!value">Select...</option>
|
||||||
|
|
||||||
|
|||||||
@ -65,11 +65,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
get showMlrr() {
|
get showMlrr() {
|
||||||
return this.selectedType === 'mlrr';
|
return this.selectedType === 'ml-relative-reasoning';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showRbrr() {
|
get showRbrr() {
|
||||||
return this.selectedType === 'rbrr';
|
return this.selectedType === 'rule-based-relative-reasoning';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showEnviformer() {
|
get showEnviformer() {
|
||||||
|
|||||||
@ -203,11 +203,11 @@
|
|||||||
id="model-based-prediction-setting-threshold"
|
id="model-based-prediction-setting-threshold"
|
||||||
name="model-based-prediction-setting-threshold"
|
name="model-based-prediction-setting-threshold"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="0.25"
|
value="0.25"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.05"
|
step="any"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,22 @@
|
|||||||
id="delete_pathway_edge_modal"
|
id="delete_pathway_edge_modal"
|
||||||
class="modal"
|
class="modal"
|
||||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||||
|
@modal-opened.window="
|
||||||
|
const links = d3.selectAll('path.highlighted');
|
||||||
|
if (!links.empty()) {
|
||||||
|
const el = links.node();
|
||||||
|
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||||
|
console.log(el);
|
||||||
|
console.log(el.__data__);
|
||||||
|
for (let option of selectElement.options) {
|
||||||
|
if (option.value === el.__data__.url) {
|
||||||
|
option.selected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
"
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
>
|
>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
|
|||||||
@ -4,6 +4,19 @@
|
|||||||
id="delete_pathway_node_modal"
|
id="delete_pathway_node_modal"
|
||||||
class="modal"
|
class="modal"
|
||||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||||
|
@modal-opened.window="
|
||||||
|
const el = d3.select('circle.highlighted').node();
|
||||||
|
if (el !== null) {
|
||||||
|
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
||||||
|
for (let option of selectElement.options) {
|
||||||
|
if (option.value === el.__data__.url) {
|
||||||
|
option.selected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
"
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
>
|
>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework_modern.html" %}
|
||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -82,6 +83,12 @@
|
|||||||
<div class="collapse-content">{{ compound.description }}</div>
|
<div class="collapse-content">{{ compound.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Extension Slot for Viz -->
|
||||||
|
{% epdb_slot_templates "epdb.objects.compound.viz" as viz_templates %}
|
||||||
|
{% for tpl in viz_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<!-- 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,5 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework_modern.html" %}
|
||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -50,6 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Extension Slot for Viz -->
|
||||||
|
{% epdb_slot_templates "epdb.objects.compound_structure.viz" as viz_templates %}
|
||||||
|
{% for tpl in viz_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<!-- 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 />
|
||||||
|
|||||||
@ -92,7 +92,7 @@
|
|||||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||||
{% for educt in edge.start_nodes.all %}
|
{% for educt in edge.start_nodes.all %}
|
||||||
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
||||||
>{{ educt.name }}</a
|
>{{ educt.get_name }}</a
|
||||||
>
|
>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<svg
|
<svg
|
||||||
@ -112,7 +112,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{% for product in edge.end_nodes.all %}
|
{% for product in edge.end_nodes.all %}
|
||||||
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
||||||
>{{ product.name }}</a
|
>{{ product.get_name }}</a
|
||||||
>
|
>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,7 +56,9 @@
|
|||||||
<ul class="menu bg-base-200 rounded-box">
|
<ul class="menu bg-base-200 rounded-box">
|
||||||
{% for um in group.user_member.all %}
|
{% for um in group.user_member.all %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ um.url }}" class="hover:bg-base-300"
|
<a
|
||||||
|
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
||||||
|
class="hover:bg-base-300"
|
||||||
>{{ um.username }}
|
>{{ um.username }}
|
||||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework_modern.html" %}
|
||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -54,6 +55,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if node.description and node.description != "no description" %}
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
|
<div class="collapse-content">{{ node.description }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|
||||||
|
{% for tpl in viz_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<!-- 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,7 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework_modern.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@ -19,12 +21,13 @@
|
|||||||
.link {
|
.link {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
/* marker-end: url(#arrow); */
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link_no_arrow {
|
.link_no_arrow {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node image {
|
.node image {
|
||||||
@ -70,12 +73,25 @@
|
|||||||
stroke: red;
|
stroke: red;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inedge {
|
||||||
|
stroke: red;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
.outedge {
|
||||||
|
stroke: green;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="{% static 'js/pw.js' %}"></script>
|
<script src="{% static 'js/pw.js' %}"></script>
|
||||||
|
|
||||||
{% 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_edge_modal.html" %}
|
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||||
|
{% epdb_slot_templates "epdb.modals.objects.pathway.add" as add_templates %}
|
||||||
|
{% for tpl in add_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
{% include "modals/objects/download_pathway_csv_modal.html" %}
|
{% include "modals/objects/download_pathway_csv_modal.html" %}
|
||||||
{% include "modals/objects/download_pathway_image_modal.html" %}
|
{% include "modals/objects/download_pathway_image_modal.html" %}
|
||||||
{% include "modals/objects/identify_missing_rules_modal.html" %}
|
{% include "modals/objects/identify_missing_rules_modal.html" %}
|
||||||
@ -100,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Graphical Representation -->
|
<!-- Graphical Representation -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse overflow-y-auto">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Graphical Representation
|
Graphical Representation
|
||||||
@ -134,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-96 p-2"
|
||||||
>
|
>
|
||||||
{% include "actions/objects/pathway.html" %}
|
{% include "actions/objects/pathway.html" %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -162,6 +178,52 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||||
>
|
>
|
||||||
|
<li>
|
||||||
|
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="compound-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Compound Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="reaction-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Reaction Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if pathway.setting.model.app_domain %}
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
@ -361,6 +423,18 @@
|
|||||||
>
|
>
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||||
</marker>
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="curve-arrow"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
refX="10"
|
||||||
|
refY="5"
|
||||||
|
markerWidth="6"
|
||||||
|
markerHeight="6"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||||
|
</marker>
|
||||||
<marker
|
<marker
|
||||||
id="doublearrow"
|
id="doublearrow"
|
||||||
viewBox="0 0 20 30"
|
viewBox="0 0 20 30"
|
||||||
@ -468,6 +542,10 @@
|
|||||||
{{ pathway.d3_json|json_script:"pathway" }}
|
{{ pathway.d3_json|json_script:"pathway" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Global switch for compound names view
|
||||||
|
var compoundNamesViewEnabled = false;
|
||||||
|
// Gloabl switch for reaction names view
|
||||||
|
var reactionNamesViewEnabled = false;
|
||||||
// Global switch for app domain view
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
// Global switch for timeseries view
|
// Global switch for timeseries view
|
||||||
@ -503,6 +581,58 @@
|
|||||||
descContent.innerHTML = newDesc;
|
descContent.innerHTML = newDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compoundNamesBtn = document.getElementById(
|
||||||
|
"compound-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (compoundNamesBtn) {
|
||||||
|
compoundNamesBtn.addEventListener("click", function () {
|
||||||
|
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("compound-names-icon");
|
||||||
|
if (compoundNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
if (x.name) {
|
||||||
|
d3.select(x.el)
|
||||||
|
.append("text")
|
||||||
|
.text((d) => d.plain_name)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("y", -20)
|
||||||
|
.style("font-size", "4px");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
d3.select(x.el).select("text").remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const reactionNamesBtn = document.getElementById(
|
||||||
|
"reaction-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (reactionNamesBtn) {
|
||||||
|
reactionNamesBtn.addEventListener("click", function () {
|
||||||
|
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("reaction-names-icon");
|
||||||
|
if (reactionNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", null);
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", "none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// App domain toggle
|
// App domain toggle
|
||||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||||
if (appDomainBtn) {
|
if (appDomainBtn) {
|
||||||
|
|||||||
@ -105,7 +105,7 @@
|
|||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="select mb-8 w-full">
|
<label class="select mb-8 w-full" id="prediction-setting-label">
|
||||||
<span class="label">Predictor</span>
|
<span class="label">Predictor</span>
|
||||||
<select id="prediction-setting" name="prediction-setting">
|
<select id="prediction-setting" name="prediction-setting">
|
||||||
<option disabled>Select a Setting</option>
|
<option disabled>Select a Setting</option>
|
||||||
@ -148,6 +148,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{# prettier-ignore-start #}
|
{# prettier-ignore-start #}
|
||||||
<script>
|
<script>
|
||||||
|
// Hide predictor selection and update button text if mode is "build"
|
||||||
|
function radioChange(event) {
|
||||||
|
if (event.target.value === "build") {
|
||||||
|
document.getElementById("prediction-setting-label").hidden = true;
|
||||||
|
document.getElementById("predict-submit-button").innerText = "Build";
|
||||||
|
} else {
|
||||||
|
document.getElementById("prediction-setting-label").hidden = false;
|
||||||
|
document.getElementById("predict-submit-button").innerText = "Predict";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const radioButtons = document.querySelectorAll('input[name="predict"]');
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
radio.addEventListener('change', radioChange);
|
||||||
|
});
|
||||||
|
|
||||||
// Helper function to safely get Ketcher instance from iframe
|
// Helper function to safely get Ketcher instance from iframe
|
||||||
function getKetcherInstance(iframeId) {
|
function getKetcherInstance(iframeId) {
|
||||||
const ketcherFrame = document.getElementById(iframeId);
|
const ketcherFrame = document.getElementById(iframeId);
|
||||||
@ -197,7 +212,13 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const button = this;
|
const button = this;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
|
// Set text depending on mode
|
||||||
|
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
||||||
|
button.textContent = "Building...";
|
||||||
|
} else {
|
||||||
button.textContent = "Predicting...";
|
button.textContent = "Predicting...";
|
||||||
|
}
|
||||||
|
|
||||||
// Get SMILES from either input or Ketcher
|
// Get SMILES from either input or Ketcher
|
||||||
const smilesInput = document.getElementById("predict-smiles");
|
const smilesInput = document.getElementById("predict-smiles");
|
||||||
|
|||||||
@ -110,8 +110,6 @@
|
|||||||
<div
|
<div
|
||||||
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
||||||
>
|
>
|
||||||
<a href="/legal" class="link link-hover">Legal</a>
|
|
||||||
<span class="text-base-content/30">•</span>
|
|
||||||
<a href="/terms" class="link link-hover">Terms of Use</a>
|
<a href="/terms" class="link link-hover">Terms of Use</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, CompoundStructure
|
from epdb.models import Compound, User, CompoundStructure
|
||||||
|
|
||||||
@ -17,6 +19,10 @@ class CompoundTest(TestCase):
|
|||||||
cls.user = User.objects.get(username="anonymous")
|
cls.user = User.objects.get(username="anonymous")
|
||||||
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
cls.INVALID_MOLFILE = "this is not a valid molfile"
|
||||||
|
|
||||||
def test_smoke(self):
|
def test_smoke(self):
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
self.package,
|
self.package,
|
||||||
@ -33,13 +39,13 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(c.description, "No Desc")
|
self.assertEqual(c.description, "No Desc")
|
||||||
|
|
||||||
def test_missing_smiles(self):
|
def test_missing_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
def test_smiles_are_trimmed(self):
|
def test_smiles_are_trimmed(self):
|
||||||
@ -96,7 +102,7 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.compounds), 1)
|
self.assertEqual(len(self.package.compounds), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(
|
_ = Compound.create(
|
||||||
self.package,
|
self.package,
|
||||||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||||||
@ -188,3 +194,153 @@ class CompoundTest(TestCase):
|
|||||||
|
|
||||||
c1.set_default_structure(c2)
|
c1.set_default_structure(c2)
|
||||||
self.assertNotEqual(default_structure, c2)
|
self.assertNotEqual(default_structure, c2)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Compound",
|
||||||
|
description="Created from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="Structure from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
# The SMILES must have been derived from the molfile, not the placeholder
|
||||||
|
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
|
||||||
|
self.assertIsNotNone(cs.smiles)
|
||||||
|
self.assertTrue(len(cs.smiles) > 0)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile_stores_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
|
||||||
|
name="Molfile Store Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = c.default_structure
|
||||||
|
|
||||||
|
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
|
||||||
|
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
|
||||||
|
# on the existing structure
|
||||||
|
_ = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Structure with Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch fresh from DB to confirm persistence
|
||||||
|
cs_db = CompoundStructure.objects.get(pk=cs.pk)
|
||||||
|
self.assertIsNotNone(cs_db.molfile)
|
||||||
|
self.assertNotEqual(cs_db.molfile.strip(), "")
|
||||||
|
|
||||||
|
def test_create_structure_from_invalid_molfile_raises(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Invalid Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidMolfileException):
|
||||||
|
CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.INVALID_MOLFILE,
|
||||||
|
name="Bad Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and smiles are supplied, molfile must win."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Precedence Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="C", # intentionally wrong / different SMILES
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Wins",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# SMILES should be derived from molfile, not the supplied "C"
|
||||||
|
self.assertNotEqual(cs.smiles, "C")
|
||||||
|
|
||||||
|
def test_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Empty Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=" ", # whitespace only – should be ignored
|
||||||
|
name="Fallback to SMILES",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_none_molfile_falls_back_to_smiles(self):
|
||||||
|
"""None as molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="None Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=None,
|
||||||
|
name="Fallback None Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_molfile_deduplication(self):
|
||||||
|
"""Creating a structure twice from the same molfile should return the existing object."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Molfile Dedup Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs1 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs2 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs1.pk, cs2.pk)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from epdb.exceptions import InvalidSMILESException
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, Reaction, Rule
|
from epdb.models import Compound, User, Reaction, Rule
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ class ReactionTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.reactions), 1)
|
self.assertEqual(len(self.package.reactions), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Reaction.create(
|
_ = Reaction.create(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
name="Eawag BBD reaction r0001",
|
name="Eawag BBD reaction r0001",
|
||||||
|
|||||||
@ -11,6 +11,9 @@ from epdb.models import Compound, Scenario, ExternalDatabase
|
|||||||
class CompoundViewTest(TestCase):
|
class CompoundViewTest(TestCase):
|
||||||
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(CompoundViewTest, cls).setUpClass()
|
super(CompoundViewTest, cls).setUpClass()
|
||||||
@ -396,3 +399,108 @@ class CompoundViewTest(TestCase):
|
|||||||
|
|
||||||
c = Compound.objects.get(url=compound_url)
|
c = Compound.objects.get(url=compound_url)
|
||||||
self.assertEqual(len(c.aliases), 0)
|
self.assertEqual(len(c.aliases), 0)
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile(self):
|
||||||
|
"""POSTing a valid molfile without a SMILES should create a compound successfully."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Created from molfile",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
self.assertEqual(c.name, "4-Nitrobenzoic acid")
|
||||||
|
self.assertEqual(c.description, "Created from molfile")
|
||||||
|
# SMILES should have been extracted from molfile, so it must be non-empty
|
||||||
|
self.assertIsNotNone(c.default_structure.smiles)
|
||||||
|
self.assertNotEqual(c.default_structure.smiles.strip(), "")
|
||||||
|
|
||||||
|
def test_create_compound_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and SMILES are submitted, the molfile should win."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile precedence test",
|
||||||
|
"compound-smiles": "C", # intentionally wrong/different
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
# The resulting SMILES must NOT be the dummy "C" supplied via compound-smiles
|
||||||
|
self.assertNotEqual(c.default_structure.smiles, "C")
|
||||||
|
|
||||||
|
def test_create_compound_via_invalid_molfile_returns_error(self):
|
||||||
|
"""POSTing an invalid molfile should not create a compound and should return an error response."""
|
||||||
|
initial_count = self.user1_default_package.compounds.count()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Bad Molfile Compound",
|
||||||
|
"compound-description": "Should fail",
|
||||||
|
"compound-molfile": "this is not a molfile at all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# The view should signal an error (non-2xx or a redirect to an error page, not 302 to a new compound)
|
||||||
|
self.assertNotEqual(response.status_code, 302)
|
||||||
|
# No new compound should have been created
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), initial_count)
|
||||||
|
|
||||||
|
def test_create_compound_via_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""Submitting an empty molfile with a valid SMILES should fall back to SMILES."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Fallback SMILES Compound",
|
||||||
|
"compound-description": "Empty molfile fallback",
|
||||||
|
"compound-smiles": "C(CCl)Cl",
|
||||||
|
"compound-molfile": " ", # whitespace only
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile_deduplication(self):
|
||||||
|
"""Submitting the same molfile twice should return the existing compound."""
|
||||||
|
response1 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response1.status_code, 302)
|
||||||
|
compound_url_1 = response1.url
|
||||||
|
|
||||||
|
response2 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response2.status_code, 302)
|
||||||
|
self.assertEqual(response2.url, compound_url_1)
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), 1)
|
||||||
|
|||||||
@ -46,6 +46,14 @@ class ModelViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
|
{
|
||||||
|
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||||
|
"probability": 0.75,
|
||||||
|
"btrule": {
|
||||||
|
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||||
|
"name": "bt0001-3568",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
||||||
"probability": 0.25,
|
"probability": 0.25,
|
||||||
@ -62,14 +70,6 @@ class ModelViewTest(TestCase):
|
|||||||
"name": "bt0243-4301",
|
"name": "bt0243-4301",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
|
||||||
"probability": 0.75,
|
|
||||||
"btrule": {
|
|
||||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
|
||||||
"name": "bt0001-3568",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
actual = response.json()["pred"]
|
actual = response.json()["pred"]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user