Compare commits
6 Commits
develop
...
e2da59634b
| Author | SHA1 | Date | |
|---|---|---|---|
| e2da59634b | |||
| f8d01e4477 | |||
| d381effdaf | |||
| 19d90b51eb | |||
| cca121af21 | |||
| 74489094c9 |
@ -7,10 +7,10 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
exclude: epiuclid/schemas/
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^epiuclid/schemas/|^static/js/ketcher3/
|
||||
exclude: epiuclid/schemas/
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
exclude: ^static/images/|^epiuclid/schemas/|^fixtures/|^static/js/ketcher3/
|
||||
exclude: ^static/images/|^epiuclid/schemas/|^fixtures/
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.3
|
||||
|
||||
@ -35,13 +35,15 @@ RUN mkdir -p -m 0700 /root/.ssh \
|
||||
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
|
||||
|
||||
# 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-bayer:1.0 .
|
||||
RUN --mount=type=ssh \
|
||||
uv sync --locked --extra pepper-plugin
|
||||
uv sync --locked --extra ms-login --extra pepper-plugin
|
||||
|
||||
# Now copy source and do a final sync to install the project itself
|
||||
# Ensure .dockerignore is reasonable
|
||||
COPY bb4g bb4g
|
||||
COPY biotransformer biotransformer
|
||||
COPY bayer bayer
|
||||
COPY bridge bridge
|
||||
COPY envipath envipath
|
||||
COPY epapi epapi
|
||||
|
||||
0
bayer/__init__.py
Normal file
19
bayer/admin.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import (
|
||||
PESCompound,
|
||||
PESStructure
|
||||
)
|
||||
|
||||
|
||||
class PESCompoundAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class PESStructureAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(PESCompound, PESCompoundAdmin)
|
||||
admin.site.register(PESStructure, PESStructureAdmin)
|
||||
6
bayer/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BayerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bayer'
|
||||
39
bayer/epdb_hooks.py
Normal file
@ -0,0 +1,39 @@
|
||||
import logging
|
||||
|
||||
from epdb.template_registry import register_template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# PES Create
|
||||
register_template(
|
||||
"epdb.actions.collections.compound",
|
||||
"actions/collections/new_pes.html",
|
||||
)
|
||||
register_template(
|
||||
"modals.collections.compound",
|
||||
"modals/collections/new_pes_modal.html",
|
||||
)
|
||||
register_template(
|
||||
"epdb.actions.objects.pathway.add",
|
||||
"actions/objects/pathway_add_pes.html",
|
||||
)
|
||||
register_template(
|
||||
"epdb.modals.objects.pathway.add",
|
||||
"modals/objects/add_pathway_pes_node_modal.html"
|
||||
)
|
||||
|
||||
# PES Viz
|
||||
register_template(
|
||||
"epdb.objects.compound.viz",
|
||||
"objects/compound_viz.html",
|
||||
)
|
||||
|
||||
register_template(
|
||||
"epdb.objects.compound_structure.viz",
|
||||
"objects/compound_structure_viz.html",
|
||||
)
|
||||
|
||||
register_template(
|
||||
"epdb.objects.node.viz",
|
||||
"objects/node_viz.html",
|
||||
)
|
||||
35
bayer/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
||||
# 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
22
bayer/migrations/0002_initial.py
Normal file
@ -0,0 +1,22 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,41 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-17 21:22
|
||||
|
||||
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.CreateModel(
|
||||
name='PESCompound',
|
||||
fields=[
|
||||
('compound_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compound')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('epdb.compound',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PESStructure',
|
||||
fields=[
|
||||
('compoundstructure_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compoundstructure')),
|
||||
('pes_link', models.URLField(verbose_name='PES Link')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('epdb.compoundstructure',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='package',
|
||||
name='data_pool',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'),
|
||||
),
|
||||
]
|
||||
0
bayer/migrations/__init__.py
Normal file
237
bayer/models.py
Normal file
@ -0,0 +1,237 @@
|
||||
from typing import List
|
||||
import urllib.parse
|
||||
import nh3
|
||||
from django.conf import settings as s
|
||||
from django.db import models, transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse
|
||||
|
||||
from epdb.models import (
|
||||
EnviPathModel,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
ParallelRule,
|
||||
SequentialRule,
|
||||
SimpleAmbitRule,
|
||||
SimpleRDKitRule,
|
||||
)
|
||||
from utilities.chem import FormatConverter
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class PESCompound(Compound):
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs
|
||||
) -> "Compound":
|
||||
|
||||
pes_url = pes_data["pes_url"]
|
||||
|
||||
# Check if we find a direct match for a given pes_link
|
||||
if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists():
|
||||
# Due to normalization we might end up in having multiple structures
|
||||
# All of them point to the same compound -> pick any
|
||||
return PESStructure.objects.filter(pes_link=pes_url, compound__package=package).first().compound
|
||||
|
||||
# Generate Compound
|
||||
c = PESCompound()
|
||||
c.package = package
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||
|
||||
c.name = name
|
||||
|
||||
# We have a default here only set the value if it carries some payload
|
||||
if description is not None and description.strip() != "":
|
||||
c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
c.save()
|
||||
|
||||
molfile = pes_data.get("representativeStructures", [{}])[0].get("ctab")
|
||||
|
||||
if molfile is None:
|
||||
raise ValueError("PES data does not contain a valid mol file!")
|
||||
|
||||
smiles = FormatConverter.to_smiles(FormatConverter.from_molfile(molfile))
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
|
||||
is_standardized = standardized_smiles == smiles
|
||||
|
||||
if not is_standardized:
|
||||
_ = PESStructure.create(
|
||||
c,
|
||||
pes_url,
|
||||
molfile,
|
||||
standardized_smiles,
|
||||
name="Normalized structure of {}".format(name),
|
||||
description="{} (in its normalized form)".format(description),
|
||||
normalized_structure=True,
|
||||
)
|
||||
|
||||
|
||||
cs = PESStructure.create(
|
||||
c,
|
||||
pes_url,
|
||||
molfile,
|
||||
smiles,
|
||||
name=name,
|
||||
description=description,
|
||||
normalized_structure=is_standardized
|
||||
)
|
||||
|
||||
c.default_structure = cs
|
||||
c.save()
|
||||
|
||||
return c
|
||||
|
||||
|
||||
class PESStructure(CompoundStructure):
|
||||
pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link")
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
compound: Compound,
|
||||
pes_link: str,
|
||||
mol_file: str,
|
||||
smiles: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
if compound.pk is None:
|
||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||
|
||||
cs = PESStructure()
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if description is not None:
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
cs.smiles = smiles
|
||||
cs.mol_file = mol_file
|
||||
cs.pes_link = pes_link
|
||||
cs.compound = compound
|
||||
|
||||
if "normalized_structure" in kwargs:
|
||||
cs.normalized_structure = kwargs["normalized_structure"]
|
||||
|
||||
cs.save()
|
||||
|
||||
return cs
|
||||
|
||||
@transaction.atomic
|
||||
def add_structure(
|
||||
self,
|
||||
smiles: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
default_structure: bool = False,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> "CompoundStructure":
|
||||
raise ValueError("Not supported!")
|
||||
|
||||
def d3_json(self):
|
||||
return {
|
||||
"is_pes": True,
|
||||
"pes_link": self.pes_link,
|
||||
# Will overwrite image from Node
|
||||
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
||||
"image_type": "png",
|
||||
}
|
||||
9
bayer/templates/actions/collections/new_pes.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% if meta.can_edit %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick="document.getElementById('new_pes_modal').showModal(); return false;"
|
||||
>
|
||||
New PES
|
||||
</button>
|
||||
{% endif %}
|
||||
8
bayer/templates/actions/objects/pathway_add_pes.html
Normal file
@ -0,0 +1,8 @@
|
||||
<li>
|
||||
<a
|
||||
class="button"
|
||||
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
|
||||
>
|
||||
<i class="glyphicon glyphicon-plus"></i> Add PES</a
|
||||
>
|
||||
</li>
|
||||
175
bayer/templates/modals/collections/new_package_modal.html
Normal file
@ -0,0 +1,175 @@
|
||||
{% load static %}
|
||||
|
||||
<dialog
|
||||
id="new_package_modal"
|
||||
class="modal"
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
packageClassification: null,
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
this.packageClassification = 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.secret_groups %}
|
||||
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
|
||||
{% 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>
|
||||
174
bayer/templates/modals/collections/new_pes_modal.html
Normal file
@ -0,0 +1,174 @@
|
||||
{% load static %}
|
||||
|
||||
<dialog
|
||||
id="new_pes_modal"
|
||||
class="modal"
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
pesLink: null,
|
||||
pesVizHtml: '',
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
|
||||
get isPESSet() {
|
||||
console.log(this.pesLink);
|
||||
return this.pesLink !== null;
|
||||
},
|
||||
|
||||
updatePesViz() {
|
||||
if (!this.isPESSet) {
|
||||
this.pesVizHtml = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
||||
img.style.width = '100%';
|
||||
img.style.height = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
|
||||
img.onload = () => {
|
||||
this.pesVizHtml = img.outerHTML;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
this.pesVizHtml = `
|
||||
<div class='alert alert-error' role='alert'>
|
||||
<h4 class='alert-heading'>Could not render PES!</h4>
|
||||
<p>Could not render PES - Do you have access?</p>
|
||||
</div>`;
|
||||
};
|
||||
},
|
||||
|
||||
submit(formId) {
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
// Remove previously injected inputs
|
||||
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||
|
||||
// Add values from dynamic form into the html form
|
||||
if (this.formData) {
|
||||
Object.entries(this.formData).forEach(([key, value]) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
input.classList.add('dynamic-param');
|
||||
|
||||
form.appendChild(input);
|
||||
});
|
||||
}
|
||||
|
||||
if (form && form.checkValidity()) {
|
||||
this.isSubmitting = true;
|
||||
form.submit();
|
||||
} else if (form) {
|
||||
form.reportValidity();
|
||||
}
|
||||
}
|
||||
}"
|
||||
@close="reset()"
|
||||
>
|
||||
<div class="modal-box max-w-3xl">
|
||||
<!-- Header -->
|
||||
<h3 class="text-lg font-bold">New PES</h3>
|
||||
|
||||
<!-- Close button (X) -->
|
||||
<form method="dialog">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="py-4">
|
||||
<form
|
||||
id="new-pes-modal-form"
|
||||
accept-charset="UTF-8"
|
||||
action="{% url 'create pes' meta.current_package.uuid %}"
|
||||
method="post"
|
||||
>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="compound-name">
|
||||
<span class="label-text">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="compound-name"
|
||||
class="input input-bordered w-full"
|
||||
name="compound-name"
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="compound-description">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<input
|
||||
id="compound-description"
|
||||
class="input input-bordered w-full"
|
||||
name="compound-description"
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="pes-link">
|
||||
<span class="label-text">Link to PES</span>
|
||||
</label>
|
||||
<input
|
||||
id="pes-link"
|
||||
name="pes-link"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
||||
x-model="pesLink"
|
||||
@input="updatePesViz()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-action">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick="this.closest('dialog').close()"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="submit('new-pes-modal-form')"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<span x-show="!isSubmitting">Submit</span>
|
||||
<span
|
||||
x-show="isSubmitting"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<span x-show="isSubmitting">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button :disabled="isSubmitting">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
174
bayer/templates/modals/objects/add_pathway_pes_node_modal.html
Normal file
@ -0,0 +1,174 @@
|
||||
{% load static %}
|
||||
|
||||
<dialog
|
||||
id="add_pathway_pes_node_modal"
|
||||
class="modal"
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
pesLink: null,
|
||||
pesVizHtml: '',
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
|
||||
get isPESSet() {
|
||||
console.log(this.pesLink);
|
||||
return this.pesLink !== null;
|
||||
},
|
||||
|
||||
updatePesViz() {
|
||||
if (!this.isPESSet) {
|
||||
this.pesVizHtml = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
||||
img.style.width = '100%';
|
||||
img.style.height = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
|
||||
img.onload = () => {
|
||||
this.pesVizHtml = img.outerHTML;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
this.pesVizHtml = `
|
||||
<div class='alert alert-error' role='alert'>
|
||||
<h4 class='alert-heading'>Could not render PES!</h4>
|
||||
<p>Could not render PES - Do you have access?</p>
|
||||
</div>`;
|
||||
};
|
||||
},
|
||||
|
||||
submit(formId) {
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
// Remove previously injected inputs
|
||||
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||
|
||||
// Add values from dynamic form into the html form
|
||||
if (this.formData) {
|
||||
Object.entries(this.formData).forEach(([key, value]) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
input.classList.add('dynamic-param');
|
||||
|
||||
form.appendChild(input);
|
||||
});
|
||||
}
|
||||
|
||||
if (form && form.checkValidity()) {
|
||||
this.isSubmitting = true;
|
||||
form.submit();
|
||||
} else if (form) {
|
||||
form.reportValidity();
|
||||
}
|
||||
}
|
||||
}"
|
||||
@close="reset()"
|
||||
>
|
||||
<div class="modal-box max-w-3xl">
|
||||
<!-- Header -->
|
||||
<h3 class="text-lg font-bold">New PES</h3>
|
||||
|
||||
<!-- Close button (X) -->
|
||||
<form method="dialog">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="py-4">
|
||||
<form
|
||||
id="new-pes-node-modal-form"
|
||||
accept-charset="UTF-8"
|
||||
action="{% url 'create pes node' current_object.package.uuid current_object.uuid %}"
|
||||
method="post"
|
||||
>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="compound-name">
|
||||
<span class="label-text">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="compound-name"
|
||||
class="input input-bordered w-full"
|
||||
name="compound-name"
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="compound-description">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<input
|
||||
id="compound-description"
|
||||
class="input input-bordered w-full"
|
||||
name="compound-description"
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="pes-link">
|
||||
<span class="label-text">Link to PES</span>
|
||||
</label>
|
||||
<input
|
||||
id="pes-link"
|
||||
name="pes-link"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
||||
x-model="pesLink"
|
||||
@input="updatePesViz()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-action">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick="this.closest('dialog').close()"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="submit('new-pes-node-modal-form')"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<span x-show="!isSubmitting">Submit</span>
|
||||
<span
|
||||
x-show="isSubmitting"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<span x-show="isSubmitting">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button :disabled="isSubmitting">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
19
bayer/templates/objects/compound_structure_viz.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% if compound_structure.pes_link %}
|
||||
<!-- PES -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<img src='{% url 'depict_pes' %}?pesLink={{ compound_structure.pes_link|urlencode }}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
19
bayer/templates/objects/compound_viz.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% if compound.default_structure.pes_link %}
|
||||
<!-- PES -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<img src='{% url 'depict_pes' %}?pesLink={{ compound.default_structure.pes_link|urlencode }}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
19
bayer/templates/objects/node_viz.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% if node.default_node_label.pes_link %}
|
||||
<!-- PES -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">
|
||||
<img src='{% url 'depict_pes' %}?pesLink={{ node.default_node_label.pes_link|urlencode }}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
97
bayer/templates/objects/package.html
Normal file
@ -0,0 +1,97 @@
|
||||
{% extends "framework_modern.html" %}
|
||||
{% load static %}
|
||||
{% 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 }} {% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}<img src="{% static 'images/restricted_mid.png' %}" width="100">{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}<img src="{% static 'images/secret_mid.png' %}" width="60">{% endif %}</h2>
|
||||
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
|
||||
<div 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 %}
|
||||
154
bayer/templates/static/login.html
Normal file
@ -0,0 +1,154 @@
|
||||
{% extends "static/login_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}enviPath - Sign In{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
/* Tab styling */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
input[type="radio"].tab-radio {
|
||||
display: none;
|
||||
}
|
||||
.tab-label {
|
||||
cursor: pointer;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.tab-label:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
input[type="radio"].tab-radio:checked + .tab-label {
|
||||
border-bottom-color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<img src="{% static 'images/bayer-logo.svg' %}">
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4 ...">
|
||||
<div><p></p></div>
|
||||
<div><p></p></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="border-base-300 mb-6 border-b" hidden>
|
||||
<div class="flex justify-start">
|
||||
<input
|
||||
type="radio"
|
||||
name="auth-tab"
|
||||
id="tab-sso"
|
||||
class="tab-radio"
|
||||
checked
|
||||
/>
|
||||
<label for="tab-sso" class="tab-label">SSO</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
name="auth-tab"
|
||||
id="tab-signin"
|
||||
class="tab-radio"
|
||||
/>
|
||||
<label for="tab-signin" class="tab-label">Local User</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSO Tab -->
|
||||
<div id="content-sso" class="tab-content active">
|
||||
<button role="link" onclick="window.location.href='/entra/login'" name="sso" class="btn btn-primary w-full">
|
||||
Login with Microsoft
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sign In Tab -->
|
||||
<div id="content-signin" class="tab-content">
|
||||
<form method="post" action="{% url 'login' %}" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="login" value="true" />
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="username">
|
||||
<span class="label-text">Account</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username or Email"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="passwordinput">
|
||||
<span class="label-text">Password</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="passwordinput"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<a href="{% url 'password_reset' %}" class="link link-primary text-sm"
|
||||
>Forgot password?</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
|
||||
<button type="submit" name="signin" class="btn btn-primary w-full">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
// Tab switching functionality
|
||||
document.querySelectorAll('input[name="auth-tab"]').forEach((radio) => {
|
||||
radio.addEventListener("change", function () {
|
||||
// Hide all content
|
||||
document.querySelectorAll(".tab-content").forEach((content) => {
|
||||
content.classList.remove("active");
|
||||
});
|
||||
|
||||
// Show selected content
|
||||
const contentId = "content-" + this.id.replace("tab-", "");
|
||||
document.getElementById(contentId).classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// Check for hash in URL to auto-select tab
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
const hash = window.location.hash.substring(1); // Remove the # symbol
|
||||
if (hash === "signup" || hash === "signin") {
|
||||
const tabRadio = document.getElementById("tab-" + hash);
|
||||
if (tabRadio) {
|
||||
tabRadio.checked = true;
|
||||
// Trigger change event to show correct content
|
||||
tabRadio.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
3
bayer/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
0
bayer/tests/__init__.py
Normal file
0
bayer/tests/pes/__init__.py
Normal file
174
bayer/tests/pes/test_pes.py
Normal file
19
bayer/urls.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views as v
|
||||
|
||||
UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"),
|
||||
re_path(
|
||||
rf"^package/(?P<package_uuid>{UUID})/pes$",
|
||||
v.create_pes,
|
||||
name="create pes",
|
||||
),
|
||||
re_path(
|
||||
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/pes$",
|
||||
v.create_pes_node,
|
||||
name="create pes node",
|
||||
),
|
||||
]
|
||||
163
bayer/views.py
Normal file
@ -0,0 +1,163 @@
|
||||
import base64
|
||||
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from bayer.models import PESCompound
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Pathway, Node
|
||||
from epdb.views import _anonymous_or_real
|
||||
from utilities.decorators import package_permission_required
|
||||
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
|
||||
@package_permission_required()
|
||||
def create_pes(request, package_uuid):
|
||||
current_user = _anonymous_or_real(request)
|
||||
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
|
||||
compound_name = request.POST.get('compound-name')
|
||||
compound_description = request.POST.get('compound-description')
|
||||
pes_link = request.POST.get('pes-link')
|
||||
|
||||
if pes_link:
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return BadRequest(
|
||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
return redirect(pes.url)
|
||||
else:
|
||||
return BadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
@package_permission_required()
|
||||
def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
current_user = _anonymous_or_real(request)
|
||||
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||
current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
|
||||
compound_name = request.POST.get('compound-name')
|
||||
compound_description = request.POST.get('compound-description')
|
||||
pes_link = request.POST.get('pes-link')
|
||||
|
||||
if pes_link:
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return BadRequest(
|
||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure)
|
||||
if node_qs.exists():
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
n = Node()
|
||||
n.stereo_removed = False
|
||||
n.pathway = current_pathway
|
||||
n.depth = 0
|
||||
|
||||
n.default_node_label = pes.default_structure
|
||||
n.save()
|
||||
|
||||
n.node_labels.add(pes.default_structure)
|
||||
n.save()
|
||||
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
else:
|
||||
return BadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def fetch_pes(request, pes_url) -> dict:
|
||||
from epauth.views import get_access_token_from_request
|
||||
token = get_access_token_from_request(request)
|
||||
|
||||
if token:
|
||||
for k, v in s.PES_API_MAPPING.items():
|
||||
if pes_url.startswith(k):
|
||||
pes_id = pes_url.split('/')[-1]
|
||||
|
||||
if pes_id == 'dummy':
|
||||
import json
|
||||
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
||||
res_data["pes_url"] = pes_url
|
||||
return res_data
|
||||
else:
|
||||
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
||||
params = {"pes_reg_entity_corporate_id": pes_id}
|
||||
|
||||
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
||||
|
||||
try:
|
||||
res.raise_for_status()
|
||||
pes_data = res.json()
|
||||
|
||||
if len(pes_data) == 0:
|
||||
raise ValueError(f"PES with id {pes_id} not found")
|
||||
|
||||
res_data = pes_data[0]
|
||||
res_data["pes_url"] = pes_url
|
||||
return res_data
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
|
||||
else:
|
||||
raise ValueError(f"Unknown URL {pes_url}")
|
||||
else:
|
||||
raise ValueError("Could not fetch access token from request.")
|
||||
|
||||
|
||||
def visualize_pes(request):
|
||||
pes_link = request.GET.get('pesLink')
|
||||
|
||||
if pes_link:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
|
||||
representations = pes_data.get('representations')
|
||||
|
||||
for rep in representations:
|
||||
if rep.get('type') == 'color':
|
||||
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
|
||||
return HttpResponse(image_data, content_type="image/png")
|
||||
183
bb4g/__init__.py
Normal file
@ -0,0 +1,183 @@
|
||||
import json
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
import enum
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
|
||||
from envipy_additional_information import register
|
||||
|
||||
from bridge.contracts import Classifier # noqa: I001
|
||||
from bridge.dto import (
|
||||
BuildResult,
|
||||
EnviPyDTO,
|
||||
EvaluationResult,
|
||||
RunResult,
|
||||
TransformationProductPrediction,
|
||||
) # noqa: I001
|
||||
|
||||
class SamplingAlgorithm(enum.Enum):
|
||||
EXACT = "exact"
|
||||
|
||||
|
||||
@register("bb4gconfig")
|
||||
class BB4GConfig(EnviPyModel):
|
||||
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
||||
cutoff: int = -5
|
||||
|
||||
class UI:
|
||||
title = "BB4G Configuration"
|
||||
sampling_algorithm = UIConfig(
|
||||
widget=WidgetType.SELECT,
|
||||
label="BB4G Sampling Algorithm",
|
||||
order=1,
|
||||
placeholder="If unset defaults to 'exact'"
|
||||
)
|
||||
cutoff = UIConfig(
|
||||
widget=WidgetType.NUMBER,
|
||||
label="BB4G Cutoff",
|
||||
order=2,
|
||||
placeholder="If unset defaults to -5"
|
||||
)
|
||||
|
||||
|
||||
# Once stable these will be exposed by enviPy-plugins lib
|
||||
class BB4G(Classifier):
|
||||
Config = BB4GConfig
|
||||
|
||||
def __init__(self, config: BB4GConfig | None = None):
|
||||
super().__init__(config)
|
||||
self.url = f"{s.BB4G_URL}"
|
||||
|
||||
self.token = self.acquire_token()
|
||||
self.header = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def acquire_token(self):
|
||||
BB4G_TENANT_ID = s.BB4G_TENANT_ID
|
||||
BB4G_CLIENT_ID = s.BB4G_CLIENT_ID
|
||||
BB4G_CLIENT_SECRET = s.BB4G_CLIENT_SECRET
|
||||
BB4G_SCOPE = s.BB4G_SCOPE
|
||||
|
||||
BB4G_TOKEN_URL = f"https://login.microsoftonline.com/{BB4G_TENANT_ID}/oauth2/v2.0/token"
|
||||
|
||||
payload = {
|
||||
"client_id": BB4G_CLIENT_ID,
|
||||
"client_secret": BB4G_CLIENT_SECRET,
|
||||
"scope": BB4G_SCOPE,
|
||||
"grant_type": "client_credentials"
|
||||
}
|
||||
|
||||
# No Proxy required, URL is whitelisted
|
||||
res = requests.post(BB4G_TOKEN_URL, data=payload)
|
||||
|
||||
res.raise_for_status()
|
||||
|
||||
return res.json()["access_token"]
|
||||
|
||||
def start(self):
|
||||
header = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
started = False
|
||||
retries = 0
|
||||
while not started and retries < 5:
|
||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
||||
|
||||
if res.status_code == 200:
|
||||
started = True
|
||||
elif res.status_code in [500, 502]:
|
||||
retries += 1
|
||||
import time
|
||||
time.sleep(5)
|
||||
else:
|
||||
raise ValueError(f"Unexpected status code: {res.status_code}")
|
||||
|
||||
@classmethod
|
||||
def requires_rule_packages(cls) -> bool:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def requires_data_packages(cls) -> bool:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def identifier(cls) -> str:
|
||||
return "bb4g"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "BB4G Template Free Model"
|
||||
|
||||
@classmethod
|
||||
def display(cls) -> str:
|
||||
return "BB4G Template Free Model"
|
||||
|
||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||
return
|
||||
|
||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||
|
||||
# Ensure Service is running
|
||||
self.start()
|
||||
|
||||
smiles = [c.smiles for c in eP.get_compounds()]
|
||||
preds = self._post(smiles)
|
||||
|
||||
results = []
|
||||
|
||||
for substrate in preds.keys():
|
||||
results.append(
|
||||
TransformationProductPrediction(
|
||||
substrate=substrate,
|
||||
products=preds[substrate],
|
||||
)
|
||||
)
|
||||
|
||||
return RunResult(
|
||||
producer=eP.get_context().url,
|
||||
description=f"Generated at {datetime.now()}",
|
||||
result=results,
|
||||
)
|
||||
|
||||
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||
pass
|
||||
|
||||
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||
pass
|
||||
|
||||
def _post(self, smiles: List[str]) -> dict[str, dict[str, float]]:
|
||||
header = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
result = {}
|
||||
|
||||
for smi in smiles:
|
||||
data = {
|
||||
"smiles": smi,
|
||||
"sampling_alg": self.config.sampling_algorithm.value,
|
||||
"cutoff": self.config.cutoff,
|
||||
}
|
||||
|
||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
|
||||
|
||||
resp.raise_for_status()
|
||||
|
||||
for substrate, predictions in resp.json().items():
|
||||
preds = {}
|
||||
|
||||
for pred in predictions:
|
||||
prod = pred["prediction"]
|
||||
prob = math.exp(pred["log_likelihood"])
|
||||
preds[prod] = prob
|
||||
|
||||
result[substrate] = preds
|
||||
|
||||
return result
|
||||
@ -261,6 +261,7 @@ class Classifier(Plugin):
|
||||
for k, v in data.items():
|
||||
if v != "":
|
||||
cpy[k] = v
|
||||
|
||||
return cls.Config(**cpy)
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -1,26 +1,54 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:18
|
||||
container_name: envipath-postgres
|
||||
container_name: eppostgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: envipath
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: envipath-redis
|
||||
container_name: epredis
|
||||
ports:
|
||||
- "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:
|
||||
postgres_data:
|
||||
ep_bayer_postgres_data:
|
||||
ep_bayer_redis_data:
|
||||
ep_bayer_data:
|
||||
|
||||
@ -7,7 +7,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- ep_postgres_data:/var/lib/postgresql
|
||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||
interval: 5s
|
||||
@ -18,14 +18,14 @@ services:
|
||||
image: redis:7-alpine
|
||||
container_name: epredis
|
||||
volumes:
|
||||
- ep_redis_data:/data
|
||||
- ep_bayer_redis_data:/data
|
||||
|
||||
biotransformer3:
|
||||
image: envipath/biotransformer3:1.0
|
||||
container_name: epbiotransformer3
|
||||
|
||||
web:
|
||||
image: envipath/envipy:1.0
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epdjango
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
@ -33,18 +33,18 @@ services:
|
||||
- .env
|
||||
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
volumes:
|
||||
- ep_data:/opt/enviPy/
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
celery_worker:
|
||||
image: envipath/envipy:1.0
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epcelery
|
||||
env_file:
|
||||
- .env
|
||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||
volumes:
|
||||
- ep_data:/opt/enviPy/
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
volumes:
|
||||
ep_postgres_data:
|
||||
ep_redis_data:
|
||||
ep_data:
|
||||
ep_bayer_postgres_data:
|
||||
ep_bayer_redis_data:
|
||||
ep_bayer_data:
|
||||
|
||||
@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env")
|
||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev")
|
||||
print(f"Loading env from {ENV_PATH}")
|
||||
load_dotenv(ENV_PATH, override=False)
|
||||
|
||||
@ -143,6 +143,12 @@ if os.environ.get("USE_TEMPLATE_DB", False) == "True":
|
||||
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
"LOCATION": "unique-snowflake",
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
@ -442,3 +448,48 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T
|
||||
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
||||
if BIOTRANSFORMER_ENABLED:
|
||||
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 = {}
|
||||
|
||||
# Entra Groups
|
||||
ENTRA_GROUPS = os.environ.get("ENTRA_GROUPS", None)
|
||||
if ENTRA_GROUPS:
|
||||
import json
|
||||
ENTRA_GROUPS = json.loads(ENTRA_GROUPS)
|
||||
else:
|
||||
ENTRA_GROUPS = {}
|
||||
|
||||
ENTRA_SECRET_GROUPS = os.environ.get("ENTRA_SECRET_GROUPS", None)
|
||||
if ENTRA_SECRET_GROUPS:
|
||||
import json
|
||||
ENTRA_SECRET_GROUPS = json.loads(ENTRA_SECRET_GROUPS)
|
||||
else:
|
||||
ENTRA_SECRET_GROUPS = {}
|
||||
|
||||
# PES Data Pools vs Entra Mapping
|
||||
DATA_POOL_MAPPING = os.environ.get("DATA_POOL_MAPPING", None)
|
||||
if DATA_POOL_MAPPING:
|
||||
import json
|
||||
DATA_POOL_MAPPING = json.loads(DATA_POOL_MAPPING)
|
||||
else:
|
||||
DATA_POOL_MAPPING = {}
|
||||
|
||||
PROXIES = {}
|
||||
if os.environ.get("HTTP_PROXY"):
|
||||
PROXIES["http"] = os.environ.get("HTTP_PROXY")
|
||||
PROXIES["https"] = os.environ.get("HTTPS_PROXY")
|
||||
|
||||
# BB4g
|
||||
BB4G_URL = os.environ.get("BB4G_URL")
|
||||
BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
|
||||
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
|
||||
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
||||
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")
|
||||
|
||||
os.environ["NO_PROXY"] = "localhost,127.0.0.1,epbiotransformer3"
|
||||
@ -117,28 +117,25 @@ class APIPermissionTestBase(TestCase):
|
||||
|
||||
# Create test compounds in each package
|
||||
cls.reviewed_compound = Compound.create(
|
||||
cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
|
||||
cls.reviewed_package, "C", "Reviewed Compound", "Test compound"
|
||||
)
|
||||
cls.owned_compound = Compound.create(
|
||||
cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
|
||||
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound"
|
||||
)
|
||||
cls.read_compound = Compound.create(
|
||||
cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
|
||||
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound"
|
||||
)
|
||||
cls.write_compound = Compound.create(
|
||||
cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
|
||||
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound"
|
||||
)
|
||||
cls.all_compound = Compound.create(
|
||||
cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
|
||||
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound"
|
||||
)
|
||||
cls.no_access_compound = Compound.create(
|
||||
cls.unreviewed_package_no_access,
|
||||
"CCCCCC",
|
||||
name="No Access Compound",
|
||||
description="Test compound",
|
||||
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound"
|
||||
)
|
||||
cls.group_compound = Compound.create(
|
||||
cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
|
||||
cls.group_package, "CCCCCCC", "Group Compound", "Test compound"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -294,8 +294,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
||||
return Compound.create(
|
||||
package,
|
||||
smiles,
|
||||
name=f"Reviewed Compound {idx:03d}",
|
||||
description="Compound for pagination tests",
|
||||
f"Reviewed Compound {idx:03d}",
|
||||
"Compound for pagination tests",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -305,8 +305,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
||||
return Compound.create(
|
||||
package,
|
||||
smiles,
|
||||
name=f"Draft Compound {idx:03d}",
|
||||
description="Compound for pagination tests",
|
||||
f"Draft Compound {idx:03d}",
|
||||
"Compound for pagination tests",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -5,4 +5,5 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("entra/login/", views.entra_login, name="entra_login"),
|
||||
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
||||
path("auth/token/", views.get_token, name="get_token"),
|
||||
]
|
||||
|
||||
@ -2,9 +2,11 @@ import msal
|
||||
from django.conf import settings as s
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import login
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from epdb.logic import UserManager
|
||||
from epdb.logic import UserManager, GroupManager
|
||||
from epdb.models import Group
|
||||
|
||||
|
||||
def get_msal_app_with_cache(request):
|
||||
@ -80,6 +82,33 @@ def entra_callback(request):
|
||||
|
||||
login(request, u)
|
||||
|
||||
# EDIT START
|
||||
|
||||
# Ensure groups exists in eP
|
||||
for id, name in s.ENTRA_SECRET_GROUPS.items():
|
||||
if not Group.objects.filter(uuid=id).exists():
|
||||
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
|
||||
uuid=id)
|
||||
else:
|
||||
g = Group.objects.get(uuid=id)
|
||||
# Ensure its secret
|
||||
g.secret = True
|
||||
g.save()
|
||||
|
||||
for id, name in s.ENTRA_GROUPS.items():
|
||||
if not Group.objects.filter(uuid=id).exists():
|
||||
g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ",
|
||||
uuid=id)
|
||||
else:
|
||||
g = Group.objects.get(uuid=id)
|
||||
|
||||
for group_uuid in claims.get("groups", []):
|
||||
if Group.objects.filter(uuid=group_uuid).exists():
|
||||
g = Group.objects.get(uuid=group_uuid)
|
||||
g.user_member.add(u)
|
||||
|
||||
# EDIT END
|
||||
|
||||
return redirect(s.SERVER_URL) # Handle errors
|
||||
|
||||
|
||||
@ -133,3 +162,9 @@ def get_access_token_from_request(request, scopes=None):
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_token(request):
|
||||
token = get_access_token_from_request(request)
|
||||
msg = f"{token}"
|
||||
return HttpResponse(msg, content_type='text/plain')
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
class InvalidSMILESException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMolfileException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PackageImportException(Exception):
|
||||
pass
|
||||
@ -1,17 +1,20 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import jwt
|
||||
import nh3
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from jwt import InvalidIssuerError
|
||||
from ninja import Field, Form, Query, Router, Schema
|
||||
from ninja.security import SessionAuth
|
||||
from ninja.security import HttpBearer
|
||||
|
||||
from utilities.chem import FormatConverter
|
||||
from utilities.misc import PackageExporter
|
||||
|
||||
from .logic import (
|
||||
EPDBURLParser,
|
||||
GroupManager,
|
||||
@ -46,6 +49,26 @@ from .models import (
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
|
||||
def get_cached_jwks(tenant_id: str, force=False) -> Dict:
|
||||
"""Get JWKS using Django cache"""
|
||||
cache_key = f"jwks_{tenant_id}"
|
||||
|
||||
jwks = cache.get(cache_key)
|
||||
|
||||
if jwks is None or force:
|
||||
# Cache miss, fetch new keys
|
||||
jwks_uri = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
||||
response = requests.get(jwks_uri)
|
||||
response.raise_for_status()
|
||||
|
||||
jwks = response.json()
|
||||
|
||||
# Cache for 1 hour (3600 seconds)
|
||||
cache.set(cache_key, jwks, 3600)
|
||||
|
||||
return jwks
|
||||
|
||||
|
||||
def get_package_for_write(user, package_uuid):
|
||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||
if not PackageManager.writable(user, p):
|
||||
@ -59,7 +82,52 @@ def _anonymous_or_real(request):
|
||||
return get_user_model().objects.get(username="anonymous")
|
||||
|
||||
|
||||
router = Router(auth=SessionAuth(csrf=False))
|
||||
def validate_token(token: str) -> dict:
|
||||
TENANT_ID = s.MS_ENTRA_TENANT_ID
|
||||
CLIENT_ID = s.MS_ENTRA_CLIENT_ID
|
||||
|
||||
jwks = get_cached_jwks(TENANT_ID)
|
||||
|
||||
header = jwt.get_unverified_header(token)
|
||||
|
||||
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(
|
||||
next(k for k in jwks["keys"] if k["kid"] == header["kid"])
|
||||
)
|
||||
|
||||
# Handle V1 and V2 tokens
|
||||
try:
|
||||
claims = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=["RS256"],
|
||||
audience=[CLIENT_ID, f"api://{CLIENT_ID}"],
|
||||
issuer=[
|
||||
f"https://sts.windows.net/{TENANT_ID}/",
|
||||
f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Token verification failed! - {e}")
|
||||
|
||||
return claims
|
||||
|
||||
|
||||
class MSBearerTokenAuth(HttpBearer):
|
||||
|
||||
def authenticate(self, request, token):
|
||||
if token is None:
|
||||
return None
|
||||
|
||||
claims = validate_token(token)
|
||||
|
||||
if not User.objects.filter(uuid=claims['oid']).exists():
|
||||
return None
|
||||
|
||||
request.user = User.objects.get(uuid=claims['oid'])
|
||||
return request.user
|
||||
|
||||
|
||||
router = Router(auth=MSBearerTokenAuth())
|
||||
|
||||
|
||||
class Error(Schema):
|
||||
@ -153,21 +221,6 @@ class SimpleModel(SimpleObject):
|
||||
identifier: str = "relative-reasoning"
|
||||
|
||||
|
||||
################
|
||||
# Login/Logout #
|
||||
################
|
||||
@router.post("/", response={200: SimpleUser, 403: Error}, auth=None)
|
||||
def login(request, loginusername: Form[str], loginpassword: Form[str]):
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
||||
email = User.objects.get(username=loginusername).email
|
||||
user = authenticate(username=email, password=loginpassword)
|
||||
if user:
|
||||
login(request, user)
|
||||
return user
|
||||
else:
|
||||
return 403, {"message": "Invalid username and/or password"}
|
||||
|
||||
|
||||
########
|
||||
# User #
|
||||
@ -791,10 +844,10 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
|
||||
|
||||
class CreateCompound(Schema):
|
||||
compoundSmiles: str
|
||||
compoundMolFile: str | None = None
|
||||
compoundName: str | None = None
|
||||
compoundDescription: str | None = None
|
||||
inchi: str | None = None
|
||||
pesLink: str | None = None
|
||||
|
||||
|
||||
@router.post("/package/{uuid:package_uuid}/compound")
|
||||
@ -805,14 +858,29 @@ def create_package_compound(
|
||||
):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
c = Compound.create(
|
||||
p,
|
||||
c.compoundSmiles,
|
||||
molfile=c.compoundMolFile,
|
||||
name=c.compoundName,
|
||||
description=c.compoundDescription,
|
||||
inchi=c.inchi,
|
||||
)
|
||||
# inchi is not used atm
|
||||
|
||||
if c.pesLink is not None:
|
||||
from bayer.views import fetch_pes
|
||||
from bayer.models import PESCompound
|
||||
|
||||
try:
|
||||
pes_data = fetch_pes(request, c.pesLink)
|
||||
except ValueError as e:
|
||||
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
||||
|
||||
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||
else:
|
||||
c = Compound.create(
|
||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
||||
)
|
||||
return redirect(c.url)
|
||||
except ValueError as e:
|
||||
return 400, {"message": str(e)}
|
||||
@ -1615,13 +1683,13 @@ class PathwayNode(Schema):
|
||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||
id: str = Field(None, alias="url")
|
||||
idcomp: str = Field(None, alias="node_label_id")
|
||||
idreact: str = Field(None, alias="node_label_id")
|
||||
idcomp: str = Field(None, alias="default_node_label.url")
|
||||
idreact: str = Field(None, alias="default_node_label.url")
|
||||
image: str = Field(None, alias="image")
|
||||
imageSize: int = Field(None, alias="image_size")
|
||||
name: str = Field(None, alias="name")
|
||||
proposed: List[Dict[str, str]] = []
|
||||
smiles: str = Field(None, alias="smiles")
|
||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
|
||||
@staticmethod
|
||||
@ -1635,10 +1703,24 @@ class PathwayNode(Schema):
|
||||
# TODO
|
||||
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
|
||||
def resolve_image_size(obj: Node):
|
||||
return 400
|
||||
|
||||
@staticmethod
|
||||
def resolve_proposed_intermediate(obj: Node):
|
||||
# TODO
|
||||
return []
|
||||
|
||||
|
||||
class PathwaySchema(Schema):
|
||||
aliases: List[str] = Field([], alias="aliases")
|
||||
@ -1890,34 +1972,68 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
||||
|
||||
class CreateNode(Schema):
|
||||
nodeAsSmiles: str
|
||||
nodeAsMolFile: str | None = None
|
||||
nodeName: str | None = None
|
||||
nodeReason: str | None = None
|
||||
nodeDepth: str | None = None
|
||||
pesLink: str | None = None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||
response={200: str | Any, 403: Error},
|
||||
response={200: str | Any, 400: Error, 403: Error},
|
||||
)
|
||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||
|
||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||
node_depth = int(float(n.nodeDepth))
|
||||
else:
|
||||
node_depth = -1
|
||||
# TODO Code Dup from bayer.views
|
||||
|
||||
node = Node.create(
|
||||
pw,
|
||||
n.nodeAsSmiles,
|
||||
node_depth,
|
||||
molfile=n.nodeAsMolFile,
|
||||
name=n.nodeName,
|
||||
description=n.nodeReason,
|
||||
)
|
||||
if n.pesLink:
|
||||
from bayer.views import fetch_pes
|
||||
from bayer.models import PESCompound
|
||||
|
||||
try:
|
||||
pes_data = fetch_pes(request, n.pesLink)
|
||||
except ValueError as e:
|
||||
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if p.classification_level != Package.Classification.SECRET:
|
||||
return 400, "Cannot create PESs for non-secret packages."
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||
return 400, {
|
||||
"messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"
|
||||
}
|
||||
|
||||
c = PESCompound.create(p, pes_data, n.nodeName, n.nodeReason)
|
||||
|
||||
node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure)
|
||||
if node_qs.exists():
|
||||
return redirect(pw.url)
|
||||
|
||||
node = Node()
|
||||
node.stereo_removed = False
|
||||
node.pathway = pw
|
||||
node.depth = 0
|
||||
|
||||
node.default_node_label = c.default_structure
|
||||
node.save()
|
||||
|
||||
node.node_labels.add(c.default_structure)
|
||||
node.save()
|
||||
else:
|
||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||
node_depth = int(n.nodeDepth)
|
||||
else:
|
||||
node_depth = -1
|
||||
|
||||
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
||||
|
||||
return redirect(node.url)
|
||||
except ValueError:
|
||||
@ -2029,13 +2145,16 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
educts = []
|
||||
products = []
|
||||
|
||||
subclasses = CompoundStructure.__subclasses__()
|
||||
|
||||
if e.edgeAsSmirks:
|
||||
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
||||
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
||||
educts.append(
|
||||
Node.objects.get(
|
||||
pathway=pw,
|
||||
default_node_label=CompoundStructure.objects.get(
|
||||
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||
get(
|
||||
compound__package=p, smiles=stand_ed
|
||||
).compound.default_structure,
|
||||
)
|
||||
@ -2046,7 +2165,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
products.append(
|
||||
Node.objects.get(
|
||||
pathway=pw,
|
||||
default_node_label=CompoundStructure.objects.get(
|
||||
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||
get(
|
||||
compound__package=p, smiles=stand_pr
|
||||
).compound.default_structure,
|
||||
)
|
||||
@ -2268,26 +2388,3 @@ def get_setting(request, setting_uuid):
|
||||
return 403, {
|
||||
"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!"
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import nh3
|
||||
from django.conf import settings as s
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from pydantic import ValidationError
|
||||
|
||||
from epdb.models import (
|
||||
@ -364,6 +365,14 @@ class PackageManager(object):
|
||||
|
||||
groups = GroupManager.get_groups(user)
|
||||
|
||||
# EDIT START
|
||||
|
||||
if package.classification_level == Package.Classification.SECRET:
|
||||
if package.data_pool not in groups:
|
||||
return False
|
||||
|
||||
# EDIT END
|
||||
|
||||
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
||||
|
||||
valid_perms = perms.get(permission)
|
||||
@ -406,6 +415,7 @@ class PackageManager(object):
|
||||
try:
|
||||
p = Package.objects.get(uuid=package_id)
|
||||
if PackageManager.readable(user, p):
|
||||
p = PackageManager.check_package_classification(user, p)
|
||||
return p
|
||||
else:
|
||||
# FIXME: use custom exception to be translatable to 403 in API
|
||||
@ -415,6 +425,37 @@ class PackageManager(object):
|
||||
except Package.DoesNotExist:
|
||||
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
||||
|
||||
# EDIT START
|
||||
|
||||
@staticmethod
|
||||
def check_package_classification(user, pack: Package):
|
||||
if pack.classification_level == Package.Classification.SECRET:
|
||||
if pack.data_pool.user_member.filter(id=user.id).exists():
|
||||
return pack
|
||||
|
||||
raise ValueError("Package is secret and not accessible to user!")
|
||||
|
||||
else:
|
||||
return pack
|
||||
|
||||
|
||||
@staticmethod
|
||||
def check_package_classifications(user, package_qs: QuerySet[Package]):
|
||||
non_secret = package_qs.exclude(classification_level=Package.Classification.SECRET)
|
||||
secret = package_qs.filter(classification_level=Package.Classification.SECRET)
|
||||
|
||||
# TODO we should be able to do via the db
|
||||
accessible_secret = []
|
||||
|
||||
for s_package in secret:
|
||||
if s_package.data_pool.user_member.filter(id=user.id).exists():
|
||||
accessible_secret.append(s_package.pk)
|
||||
|
||||
# Cannot combine a unique query with a non-unique query -> we have to call distinct
|
||||
return Package.objects.filter(pk__in=accessible_secret).distinct() | non_secret.distinct()
|
||||
|
||||
# EDIT END
|
||||
|
||||
@staticmethod
|
||||
def get_all_readable_packages(user, include_reviewed=False):
|
||||
# UserPermission only exists if at least read is granted...
|
||||
@ -441,6 +482,10 @@ class PackageManager(object):
|
||||
|
||||
qs = qs.distinct()
|
||||
|
||||
# EDIT START
|
||||
qs = PackageManager.check_package_classifications(user, qs)
|
||||
# EDIT END
|
||||
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
@ -487,11 +532,11 @@ class PackageManager(object):
|
||||
|
||||
qs = qs.distinct()
|
||||
|
||||
return qs
|
||||
# EDIT START
|
||||
qs = PackageManager.check_package_classifications(user, qs)
|
||||
# EDIT END
|
||||
|
||||
@staticmethod
|
||||
def get_packages():
|
||||
return Package.objects.all()
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
@ -596,6 +641,25 @@ class PackageManager(object):
|
||||
else:
|
||||
pack.reviewed = False
|
||||
|
||||
# EDIT START
|
||||
if data.get("classification"):
|
||||
if data["classification"] == "INTERNAL":
|
||||
pack.classification = Package.Classification.RESTRICTED
|
||||
elif data["classification"] == "RESTRICTED":
|
||||
pack.classification = Package.Classification.RESTRICTED
|
||||
elif data["classification"] == "SECRET":
|
||||
pack.classification = Package.Classification.SECRET
|
||||
|
||||
if not "datapool" in data:
|
||||
raise ValueError("Missing datapool in package")
|
||||
|
||||
g = Group.objects.get(uuid=data["datapool"].split('/')[-1])
|
||||
pack.data_pool = g
|
||||
else:
|
||||
raise ValueError(f"Invalid classification {data['classification']}")
|
||||
|
||||
# EDIT END
|
||||
|
||||
pack.description = data["description"]
|
||||
pack.save()
|
||||
|
||||
@ -681,7 +745,13 @@ class PackageManager(object):
|
||||
default_structure = None
|
||||
|
||||
for structure in compound["structures"]:
|
||||
struc = CompoundStructure()
|
||||
if structure.get("pesLink"):
|
||||
from bayer.models import PESStructure
|
||||
struc = PESStructure()
|
||||
struc.pes_link = structure["pesLink"]
|
||||
else:
|
||||
struc = CompoundStructure()
|
||||
|
||||
# struc.object_url = Command.get_id(structure, keep_ids)
|
||||
struc.compound = comp
|
||||
struc.uuid = UUID(structure["id"].split("/")[-1]) if keep_ids else uuid4()
|
||||
@ -1008,8 +1078,10 @@ class PackageManager(object):
|
||||
data: Dict[str, Any],
|
||||
owner: User,
|
||||
preserve_uuids=False,
|
||||
add_import_timestamp=True,
|
||||
trust_reviewed=False,
|
||||
) -> Package:
|
||||
importer = PackageImporter(data, preserve_uuids)
|
||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
||||
imported_package = importer.do_import()
|
||||
|
||||
up = UserPackagePermission()
|
||||
@ -1826,12 +1898,6 @@ class SPathway(object):
|
||||
"to": to_indices,
|
||||
}
|
||||
|
||||
if edge.rule:
|
||||
e["rule"] = edge.rule.simple_json()
|
||||
|
||||
if edge.probability:
|
||||
e["probability"] = edge.probability
|
||||
|
||||
edges.append(e)
|
||||
|
||||
return {
|
||||
|
||||
@ -99,15 +99,11 @@ class Command(BaseCommand):
|
||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||
new_license.save()
|
||||
|
||||
def import_package(self, data, owner, all_envipath_user_group):
|
||||
p = PackageManager.import_legacy_package(
|
||||
def import_package(self, data, owner):
|
||||
return PackageManager.import_legacy_package(
|
||||
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):
|
||||
s = SettingManager.create_setting(
|
||||
owner,
|
||||
@ -202,7 +198,7 @@ class Command(BaseCommand):
|
||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||
).read()
|
||||
)
|
||||
imported_package = self.import_package(package_data, admin, g)
|
||||
imported_package = self.import_package(package_data, admin)
|
||||
mapping[p.replace(".json", "")] = imported_package
|
||||
|
||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||
|
||||
@ -1,594 +0,0 @@
|
||||
# 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',),
|
||||
),
|
||||
]
|
||||
@ -1,128 +0,0 @@
|
||||
# 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,228 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -1,55 +0,0 @@
|
||||
# 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',
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -1,23 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@ -1,53 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -1,64 +0,0 @@
|
||||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,66 +0,0 @@
|
||||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@ -1,59 +0,0 @@
|
||||
# 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)]
|
||||
@ -1,22 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@ -1,25 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,17 +0,0 @@
|
||||
# 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",
|
||||
),
|
||||
]
|
||||
@ -1,17 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@ -1,179 +0,0 @@
|
||||
# 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",
|
||||
),
|
||||
]
|
||||
@ -1,93 +0,0 @@
|
||||
# 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",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,132 +0,0 @@
|
||||
# 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,20 +1,741 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
||||
|
||||
from django.db import migrations
|
||||
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 = [
|
||||
("epdb", "0018_auto_20260220_1203"),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scenario",
|
||||
name="additional_information",
|
||||
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')),
|
||||
('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.RemoveField(
|
||||
model_name="scenario",
|
||||
name="parent",
|
||||
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')),
|
||||
('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',),
|
||||
),
|
||||
]
|
||||
|
||||
@ -46,4 +46,9 @@ class Migration(migrations.Migration):
|
||||
name="molfile",
|
||||
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="group",
|
||||
name="secret",
|
||||
field=models.BooleanField(default=False, verbose_name="Secret Group"),
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
# 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"
|
||||
),
|
||||
),
|
||||
]
|
||||
365
epdb/models.py
@ -31,10 +31,6 @@ from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
from bridge.contracts import Property
|
||||
from bridge.dto import RunResult, PropertyPrediction
|
||||
from epdb.exceptions import (
|
||||
InvalidMolfileException,
|
||||
InvalidSMILESException,
|
||||
)
|
||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||
from utilities.ml import (
|
||||
ApplicabilityDomainPCA,
|
||||
@ -208,6 +204,7 @@ class Group(TimeStampedModel):
|
||||
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
||||
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
||||
public = models.BooleanField(verbose_name="Public Group", default=False)
|
||||
secret = models.BooleanField(verbose_name="Secret Group", default=False)
|
||||
description = models.TextField(
|
||||
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
||||
)
|
||||
@ -635,7 +632,7 @@ class EnviPathModel(TimeStampedModel):
|
||||
|
||||
class AliasMixin(models.Model):
|
||||
aliases = ArrayField(
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
@ -658,9 +655,7 @@ class AliasMixin(models.Model):
|
||||
|
||||
|
||||
class ScenarioMixin(models.Model):
|
||||
scenarios = models.ManyToManyField(
|
||||
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||
)
|
||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
||||
|
||||
@transaction.atomic
|
||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||
@ -786,19 +781,12 @@ class Compound(
|
||||
"CompoundStructure",
|
||||
verbose_name="Default Structure",
|
||||
related_name="compound_default_structure",
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
)
|
||||
|
||||
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
|
||||
def structures(self) -> QuerySet:
|
||||
return CompoundStructure.objects.filter(compound=self)
|
||||
@ -867,73 +855,47 @@ class Compound(
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
package: "Package",
|
||||
smiles: str,
|
||||
molfile: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
) -> "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() == "":
|
||||
raise InvalidSMILESException("SMILES is required")
|
||||
raise ValueError("SMILES is required")
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise InvalidSMILESException("Given SMILES is invalid")
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
raise ValueError("Given SMILES is invalid")
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
|
||||
subclasses = CompoundStructure.__subclasses__()
|
||||
|
||||
qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package)
|
||||
if subclasses:
|
||||
qs = qs.not_instance_of(*subclasses)
|
||||
|
||||
# Check if we find a direct match for a given SMILES
|
||||
if qs.exists():
|
||||
found_structure = qs.first()
|
||||
found_compound = found_structure.compound
|
||||
return qs.first().compound
|
||||
|
||||
if name:
|
||||
found_structure.add_alias(name)
|
||||
found_compound.add_alias(name)
|
||||
|
||||
return found_compound
|
||||
|
||||
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||
if subclasses:
|
||||
qs = qs.not_instance_of(*subclasses)
|
||||
|
||||
# Check if we can find the standardized one
|
||||
if qs.exists():
|
||||
found_structure = qs.first()
|
||||
found_compound = found_structure.compound
|
||||
|
||||
# We've only found the standardized one, create the very structure
|
||||
_ = found_compound.add_structure(
|
||||
smiles, molfile=molfile, name=name, description=description
|
||||
)
|
||||
|
||||
if name:
|
||||
found_compound.add_alias(name)
|
||||
|
||||
return found_compound
|
||||
# TODO should we add a structure?
|
||||
return qs.first().compound
|
||||
|
||||
# Generate Compound
|
||||
c = Compound()
|
||||
c.package = package
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -957,12 +919,7 @@ class Compound(
|
||||
)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
c,
|
||||
smiles,
|
||||
molfile=molfile,
|
||||
name=name,
|
||||
description=description,
|
||||
normalized_structure=is_standardized,
|
||||
c, smiles, name=name, description=description, normalized_structure=is_standardized
|
||||
)
|
||||
|
||||
c.default_structure = cs
|
||||
@ -975,22 +932,11 @@ class Compound(
|
||||
self,
|
||||
smiles: str,
|
||||
name: str = None,
|
||||
molfile: str = None,
|
||||
description: str = None,
|
||||
default_structure: bool = False,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> "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 == "":
|
||||
raise ValueError("SMILES is required")
|
||||
|
||||
@ -1010,28 +956,16 @@ class Compound(
|
||||
)
|
||||
|
||||
if is_standardized:
|
||||
CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
||||
|
||||
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
||||
if CompoundStructure.objects.filter(smiles=smiles, compound__package=self.package).exists():
|
||||
found_cs = CompoundStructure.objects.get(smiles=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
|
||||
if CompoundStructure.objects.filter(
|
||||
smiles__in=smiles, compound__package=self.package
|
||||
).exists():
|
||||
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
||||
|
||||
cs = CompoundStructure.create(
|
||||
self,
|
||||
smiles,
|
||||
name=name,
|
||||
molfile=molfile,
|
||||
description=description,
|
||||
normalized_structure=is_standardized,
|
||||
self, smiles, name=name, description=description, normalized_structure=is_standardized
|
||||
)
|
||||
|
||||
if default_structure:
|
||||
@ -1212,42 +1146,10 @@ class CompoundStructure(
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
compound: Compound,
|
||||
smiles: str,
|
||||
molfile: str = None,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
compound: Compound, smiles: str, 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)
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
||||
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
|
||||
if name:
|
||||
found_cs.add_alias(name)
|
||||
|
||||
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
|
||||
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
|
||||
if compound.pk is None:
|
||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||
@ -1255,18 +1157,13 @@ class CompoundStructure(
|
||||
cs = CompoundStructure()
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
cs.name = name
|
||||
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
# We have a default here only set the value if it carries some payload
|
||||
if description is not None and description.strip() != "":
|
||||
if description is not None:
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
cs.compound = compound
|
||||
cs.smiles = smiles
|
||||
|
||||
# If molfile is not None, it hase to be a valid Molfile as we've survived the parsing check
|
||||
if molfile is not None:
|
||||
cs.molfile = molfile
|
||||
cs.compound = compound
|
||||
|
||||
if "normalized_structure" in kwargs:
|
||||
cs.normalized_structure = kwargs["normalized_structure"]
|
||||
@ -1285,8 +1182,6 @@ class CompoundStructure(
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -1484,9 +1379,6 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if not FormatConverter.is_valid_smirks(smirks):
|
||||
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
|
||||
|
||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
||||
@ -1498,17 +1390,14 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if query.exists():
|
||||
if query.count() > 1:
|
||||
logger.error(f"More than one rule matched this one! {query}")
|
||||
|
||||
found_rule = query.first()
|
||||
|
||||
if name:
|
||||
found_rule.add_alias(name)
|
||||
|
||||
return found_rule
|
||||
return query.first()
|
||||
|
||||
r = SimpleAmbitRule()
|
||||
r.package = package
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1641,9 +1530,6 @@ class ParallelRule(Rule):
|
||||
f"Simple rule {sr.uuid} does not belong to package {package.uuid}!"
|
||||
)
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
# Deduplication check
|
||||
query = ParallelRule.objects.annotate(
|
||||
srs_count=Count("simple_rules", filter=Q(simple_rules__in=simple_rules), distinct=True)
|
||||
@ -1655,19 +1541,15 @@ class ParallelRule(Rule):
|
||||
|
||||
if existing_rule_qs.exists():
|
||||
if existing_rule_qs.count() > 1:
|
||||
logger.error(
|
||||
f"Found more than one ParallelRule for given input! {existing_rule_qs}"
|
||||
)
|
||||
|
||||
found_rule = existing_rule_qs.first()
|
||||
if name:
|
||||
found_rule.add_alias(name)
|
||||
|
||||
return found_rule
|
||||
logger.error(f"Found more than one reaction for given input! {existing_rule_qs}")
|
||||
return existing_rule_qs.first()
|
||||
|
||||
r = ParallelRule()
|
||||
r.package = package
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1736,15 +1618,10 @@ class Reaction(
|
||||
products = models.ManyToManyField(
|
||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||
)
|
||||
rules = models.ManyToManyField(
|
||||
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
||||
)
|
||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||
medline_references = ArrayField(
|
||||
models.TextField(blank=False, null=False),
|
||||
null=True,
|
||||
verbose_name="Medline References",
|
||||
blank=True,
|
||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
||||
)
|
||||
|
||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||
@ -1761,12 +1638,8 @@ class Reaction(
|
||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||
products: Union[List[str], List[CompoundStructure]] = None,
|
||||
rules: Union[Rule | List[Rule]] = None,
|
||||
multi_step: bool = False,
|
||||
multi_step: bool = True,
|
||||
):
|
||||
# Clean for potential XSS
|
||||
if name is not None and name.strip() != "":
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
_educts = []
|
||||
_products = []
|
||||
|
||||
@ -1821,21 +1694,14 @@ class Reaction(
|
||||
logger.error(
|
||||
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
||||
)
|
||||
|
||||
found_reaction = existing_reaction_qs.first()
|
||||
|
||||
if name:
|
||||
found_reaction.add_alias(name)
|
||||
|
||||
return found_reaction
|
||||
return existing_reaction_qs.first()
|
||||
|
||||
r = Reaction()
|
||||
r.package = package
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Reaction {Reaction.objects.filter(package=package).count() + 1}"
|
||||
|
||||
r.name = name
|
||||
# Clean for potential XSS
|
||||
if name is not None and name.strip() != "":
|
||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
@ -2071,7 +1937,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
# add links start -> pseudo
|
||||
new_link = {
|
||||
"name": link["name"],
|
||||
"plain_name": link["plain_name"],
|
||||
"id": link["id"],
|
||||
"url": link["url"],
|
||||
"image": link["image"],
|
||||
@ -2081,7 +1946,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
"source": node_url_to_idx[link["start_node_urls"][0]],
|
||||
"target": pseudo_idx,
|
||||
"app_domain": link.get("app_domain", None),
|
||||
"to_pseudo": True,
|
||||
}
|
||||
adjusted_links.append(new_link)
|
||||
|
||||
@ -2089,7 +1953,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
for target in link["end_node_urls"]:
|
||||
new_link = {
|
||||
"name": link["name"],
|
||||
"plain_name": link["plain_name"],
|
||||
"id": link["id"],
|
||||
"url": link["url"],
|
||||
"image": link["image"],
|
||||
@ -2100,7 +1963,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
"target": node_url_to_idx[target],
|
||||
"app_domain": link.get("app_domain", None),
|
||||
"multi_step": link["multi_step"],
|
||||
"from_pseudo": True,
|
||||
}
|
||||
adjusted_links.append(new_link)
|
||||
|
||||
@ -2310,12 +2172,11 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
def add_node(
|
||||
self,
|
||||
smiles: str,
|
||||
molfile: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
depth: int = -1,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
depth: Optional[int] = 0,
|
||||
):
|
||||
return Node.create(self, smiles, depth, molfile=molfile, name=name, description=description)
|
||||
return Node.create(self, smiles, depth, name=name, description=description)
|
||||
|
||||
@transaction.atomic
|
||||
def add_edge(
|
||||
@ -2344,25 +2205,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
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:
|
||||
for n in self.root_nodes:
|
||||
depth_map[0].append(n)
|
||||
|
||||
# At most depth len(nodes) is possible
|
||||
@ -2411,19 +2254,17 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
def _url(self):
|
||||
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
||||
|
||||
def get_name(self, include_suffix=True):
|
||||
def get_name(self):
|
||||
non_generic_name = True
|
||||
|
||||
if self.name is None or self.name == "no name":
|
||||
if self.name == "no name":
|
||||
non_generic_name = False
|
||||
|
||||
if non_generic_name:
|
||||
return self.name
|
||||
else:
|
||||
if include_suffix:
|
||||
return f"{self.default_node_label.name} (taken from underlying structure)"
|
||||
else:
|
||||
return self.default_node_label.name
|
||||
return (
|
||||
self.name
|
||||
if non_generic_name
|
||||
else f"{self.default_node_label.name} (taken from underlying structure)"
|
||||
)
|
||||
|
||||
def d3_json(self):
|
||||
app_domain_data = self.get_app_domain_assessment_data()
|
||||
@ -2444,16 +2285,10 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
"node_label_id": self.default_node_label.url,
|
||||
"image": f"{self.url}?image=svg",
|
||||
"image_svg": IndigoUtils.mol_to_svg(
|
||||
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,
|
||||
self.default_node_label.smiles, width=40, height=40
|
||||
),
|
||||
"image_type": "svg",
|
||||
"name": self.get_name(),
|
||||
"plain_name": self.get_name(include_suffix=False),
|
||||
"smiles": self.default_node_label.smiles,
|
||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
||||
"app_domain": {
|
||||
@ -2464,7 +2299,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
},
|
||||
"predicted_properties": predicted_properties,
|
||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||
"proposed": self.get_proposed_info(),
|
||||
"timeseries": self.get_timeseries_data(),
|
||||
**structure_data,
|
||||
}
|
||||
@ -2477,60 +2311,40 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
pathway: "Pathway",
|
||||
smiles: str,
|
||||
depth: int,
|
||||
molfile: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = 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
|
||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
stereo_removed = True
|
||||
|
||||
c = Compound.create(
|
||||
pathway.package, smiles, molfile=molfile, name=name, description=description
|
||||
)
|
||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
||||
|
||||
structure = c.get_structure_by_smiles(smiles)
|
||||
|
||||
if Node.objects.filter(pathway=pathway, default_node_label=structure).exists():
|
||||
return Node.objects.get(pathway=pathway, default_node_label=structure)
|
||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
||||
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
||||
|
||||
n = Node()
|
||||
n.stereo_removed = stereo_removed
|
||||
n.pathway = pathway
|
||||
n.depth = depth
|
||||
|
||||
n.default_node_label = structure
|
||||
n.default_node_label = c.default_structure
|
||||
n.save()
|
||||
|
||||
n.node_labels.add(structure)
|
||||
n.node_labels.add(c.default_structure)
|
||||
n.save()
|
||||
|
||||
return n
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
def get_timeseries_data(self):
|
||||
for ai in self.additional_information.all():
|
||||
if ai.type == "OECD301FTimeSeries":
|
||||
return ai.get().model_dump(mode="json")
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
@ -2561,35 +2375,13 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
return res
|
||||
|
||||
def get_proposed_info(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):
|
||||
pathway = models.ForeignKey(
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
edge_label = models.ForeignKey(
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.CASCADE
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
start_nodes = models.ManyToManyField(
|
||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||
@ -2604,7 +2396,6 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
def d3_json(self):
|
||||
edge_json = {
|
||||
"name": self.get_name(),
|
||||
"plain_name": self.get_name(include_suffix=False),
|
||||
"id": self.url,
|
||||
"url": self.url,
|
||||
"image": self.url + "?image=svg",
|
||||
@ -2719,19 +2510,17 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
return res
|
||||
|
||||
def get_name(self, include_suffix=True):
|
||||
def get_name(self):
|
||||
non_generic_name = True
|
||||
|
||||
if self.name == "no name":
|
||||
non_generic_name = False
|
||||
|
||||
if non_generic_name:
|
||||
return self.name
|
||||
else:
|
||||
if include_suffix:
|
||||
return f"{self.edge_label.name} (taken from underlying reaction)"
|
||||
else:
|
||||
return self.edge_label.name
|
||||
return (
|
||||
self.name
|
||||
if non_generic_name
|
||||
else f"{self.edge_label.name} (taken from underlying reaction)"
|
||||
)
|
||||
|
||||
|
||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||
|
||||
128
epdb/views.py
@ -19,7 +19,6 @@ from sentry_sdk import capture_exception
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .exceptions import InvalidMolfileException, InvalidSMILESException
|
||||
|
||||
from .logic import (
|
||||
EPDBURLParser,
|
||||
@ -389,6 +388,9 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
||||
"debug": s.DEBUG,
|
||||
"external_databases": ExternalDatabase.get_databases(),
|
||||
"site_id": s.MATOMO_SITE_ID,
|
||||
# EDIT START
|
||||
"secret_groups": Group.objects.filter(secret=True),
|
||||
# EDIT END
|
||||
},
|
||||
}
|
||||
|
||||
@ -588,10 +590,38 @@ def packages(request):
|
||||
"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(
|
||||
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)
|
||||
|
||||
elif request.method == "OPTIONS":
|
||||
@ -755,11 +785,6 @@ def models(request):
|
||||
{"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
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": {
|
||||
@ -772,14 +797,12 @@ def models(request):
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
@ -787,9 +810,6 @@ def models(request):
|
||||
"type": k,
|
||||
"requires_rule_packages": v.requires_rule_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():
|
||||
context["model_types"][v.display()] = {
|
||||
@ -798,6 +818,12 @@ def models(request):
|
||||
"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)
|
||||
|
||||
elif request.method == "POST":
|
||||
@ -912,12 +938,13 @@ def package_models(request, package_uuid):
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
},
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
@ -1093,11 +1120,6 @@ 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)
|
||||
|
||||
elif half_life:
|
||||
@ -1201,7 +1223,9 @@ def package(request, package_uuid):
|
||||
if request.method == "GET":
|
||||
if request.GET.get("export", False) == "true":
|
||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||
pack_json = PackageManager.export_package(current_package)
|
||||
pack_json = PackageManager.export_package(
|
||||
current_package, include_models=False, include_external_identifiers=False
|
||||
)
|
||||
response = JsonResponse(pack_json, content_type="application/json")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
|
||||
@ -1384,18 +1408,12 @@ def package_compounds(request, package_uuid):
|
||||
elif request.method == "POST":
|
||||
compound_name = request.POST.get("compound-name")
|
||||
compound_smiles = request.POST.get("compound-smiles")
|
||||
compound_molfile = request.POST.get("compound-molfile")
|
||||
compound_description = request.POST.get("compound-description")
|
||||
|
||||
try:
|
||||
c = Compound.create(
|
||||
current_package,
|
||||
compound_smiles,
|
||||
molfile=compound_molfile,
|
||||
name=compound_name,
|
||||
description=compound_description,
|
||||
current_package, compound_smiles, compound_name, compound_description
|
||||
)
|
||||
except (InvalidSMILESException, InvalidMolfileException) as e:
|
||||
except ValueError as e:
|
||||
raise BadRequest(str(e))
|
||||
|
||||
return redirect(c.url)
|
||||
@ -1521,15 +1539,11 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
||||
elif request.method == "POST":
|
||||
structure_name = request.POST.get("structure-name")
|
||||
structure_smiles = request.POST.get("structure-smiles")
|
||||
structure_molfile = request.POST.get("structure-molfile")
|
||||
structure_description = request.POST.get("structure-description")
|
||||
|
||||
try:
|
||||
cs = current_compound.add_structure(
|
||||
structure_smiles,
|
||||
molfile=structure_molfile,
|
||||
name=structure_name,
|
||||
description=structure_description,
|
||||
structure_smiles, structure_name, structure_description
|
||||
)
|
||||
except ValueError:
|
||||
return error(
|
||||
@ -1917,9 +1931,9 @@ def package_reactions(request, package_uuid):
|
||||
elif request.method == "POST":
|
||||
reaction_name = request.POST.get("reaction-name")
|
||||
reaction_description = request.POST.get("reaction-description")
|
||||
reaction_smiles = request.POST.get("reaction-smiles")
|
||||
educts = reaction_smiles.split(">>")[0].split(".")
|
||||
products = reaction_smiles.split(">>")[1].split(".")
|
||||
reactions_smirks = request.POST.get("reaction-smirks")
|
||||
educts = reactions_smirks.split(">>")[0].split(".")
|
||||
products = reactions_smirks.split(">>")[1].split(".")
|
||||
|
||||
r = Reaction.create(
|
||||
current_package,
|
||||
@ -2100,14 +2114,12 @@ def package_pathways(request, package_uuid):
|
||||
else:
|
||||
prediction_setting = current_user.prediction_settings()
|
||||
|
||||
is_predict_mode = pw_mode in {"predict", "incremental"}
|
||||
|
||||
pw = Pathway.create(
|
||||
current_package,
|
||||
stand_smiles if is_predict_mode else smiles,
|
||||
stand_smiles,
|
||||
name=name,
|
||||
description=description,
|
||||
predicted=is_predict_mode,
|
||||
predicted=pw_mode in {"predict", "incremental"},
|
||||
)
|
||||
|
||||
# set mode
|
||||
@ -2348,17 +2360,8 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
||||
node_name = request.POST.get("node-name")
|
||||
node_description = request.POST.get("node-description")
|
||||
|
||||
node_smiles = request.POST.get("node-smiles")
|
||||
node_molfile = request.POST.get("node-molfile")
|
||||
|
||||
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"
|
||||
)
|
||||
node_smiles = request.POST.get("node-smiles").strip()
|
||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
@ -2466,26 +2469,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
||||
|
||||
return JsonResponse({"success": current_node.url})
|
||||
|
||||
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")
|
||||
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotAllowed(["GET", "POST"])
|
||||
|
||||
|
||||
1
fixtures/Fixture_Package.json
Normal file
@ -65,25 +65,6 @@ def run_both_engines(SMILES, SMIRKS):
|
||||
|
||||
|
||||
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":
|
||||
context = get_base_context(request)
|
||||
|
||||
@ -128,11 +109,11 @@ def migration(request):
|
||||
),
|
||||
"id": str(r.uuid),
|
||||
"url": r.url,
|
||||
"status": res or r.name in accepted_diffs,
|
||||
"status": res,
|
||||
}
|
||||
)
|
||||
|
||||
if res or r.name in accepted_diffs:
|
||||
if res:
|
||||
success += 1
|
||||
else:
|
||||
error += 1
|
||||
@ -154,16 +135,7 @@ def migration(request):
|
||||
|
||||
for r in migration_status["results"]:
|
||||
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)
|
||||
|
||||
return render(request, "migration.html", context)
|
||||
|
||||
@ -34,12 +34,3 @@
|
||||
}
|
||||
|
||||
@import "./daisyui-theme.css";
|
||||
|
||||
select.select[multiple] {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
p a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
BIN
static/images/Restricted.gif
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
30
static/images/bayer-logo.svg
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 76 76" style="enable-background:new 0 0 76 76;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#10384F;}
|
||||
.st1{fill:#89D329;}
|
||||
.st2{fill:#00BCFF;}
|
||||
</style>
|
||||
<g id="Bayer_Cross_1_">
|
||||
<path class="st0" d="M35.9,11.3h4.4c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9h-4.4V11.3z M35.9,15.5h4.5c0.6,0,1-0.4,1-1
|
||||
c0-0.6-0.4-1-1-1h-4.5V15.5z M43,12.3c0.6,0.6,1,1.4,1,2.3c0,1.8-1.4,3.2-3.2,3.2h-7.3V7.3l7.2,0c1.7,0,3.1,1.4,3.1,3.1
|
||||
C43.7,11.1,43.4,11.8,43,12.3z M44.7,30.3H42l-0.8-1.8h-5.9l-0.8,1.8h-2.7L37,19.8h2.4L44.7,30.3z M38.2,22.5l-1.8,3.8H40
|
||||
L38.2,22.5z M41.8,32.6h3l-5.3,6.8v3.7h-2.5v-3.7l-5.3-6.8h3l3.6,4.8L41.8,32.6z M55.7,32.6v2.3h-7v1.8l6.8,0v2.3h-6.8v2h7v2.3
|
||||
h-9.5V32.6H55.7z M63.4,39.1h-1.9v4h-2.5V32.6h6.4c1.8,0,3.2,1.5,3.2,3.3c0,1.5-1,2.7-2.3,3.1l3.1,4.1h-3L63.4,39.1z M65.2,34.8
|
||||
h-3.6v2h3.6c0.6,0,1-0.5,1-1C66.2,35.3,65.7,34.8,65.2,34.8z M32.8,43.1h-2.7l-0.8-1.8h-5.9l-0.8,1.8h-2.7l5.3-10.5h2.4L32.8,43.1z
|
||||
M26.3,35.3l-1.8,3.8h3.7L26.3,35.3z M10.4,36.6h4.4c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9l-4.4,0V36.6z M10.4,40.8h4.5
|
||||
c0.6,0,1-0.4,1-1c0-0.6-0.4-1-1-1h-4.5V40.8z M17.5,37.6c0.6,0.6,1,1.4,1,2.3c0,1.8-1.4,3.2-3.2,3.2H7.9V32.6h7.2
|
||||
c1.7,0,3.1,1.4,3.1,3.1C18.2,36.4,17.9,37.1,17.5,37.6z M43,45.3v2.3h-7v1.8l6.8,0v2.3h-6.8v2h7v2.3h-9.5V45.3H43z M41.2,61.6
|
||||
c0-0.6-0.4-1-1-1h-4.3v2h4.3C40.8,62.6,41.2,62.2,41.2,61.6z M33.4,68.9V58.4h7c1.8,0,3.2,1.5,3.2,3.3c0,1.4-0.8,2.5-2,3l3.2,4.2
|
||||
h-3l-3-4h-2.9v4H33.4z"/>
|
||||
<path class="st1" d="M76.1,35.6C74.9,15.8,58.4,0,38.2,0C18,0,1.5,15.8,0.3,35.6c0,0.8,0.1,1.6,0.2,2.4c0.8,6.6,3.3,12.7,7.1,17.8
|
||||
c6.9,9.4,18,15.5,30.6,15.5c-17.6,0-32-13.7-33.2-30.9c-0.1-0.8-0.1-1.6-0.1-2.4c0-0.8,0-1.6,0.1-2.4C6.2,18.4,20.6,4.7,38.2,4.7
|
||||
c12.6,0,23.7,6.1,30.6,15.5c3.8,5.1,6.3,11.2,7.1,17.8c0.1,0.8,0.2,1.6,0.2,2.3c0-0.8,0.1-1.6,0.1-2.4
|
||||
C76.2,37.2,76.2,36.4,76.1,35.6"/>
|
||||
<path class="st2" d="M0.3,40.4C1.5,60.2,18,76,38.2,76c20.2,0,36.7-15.8,37.9-35.6c0-0.8-0.1-1.6-0.2-2.4
|
||||
c-0.8-6.6-3.3-12.7-7.1-17.8c-6.9-9.4-18-15.5-30.6-15.5c17.6,0,32,13.7,33.2,30.9c0.1,0.8,0.1,1.6,0.1,2.4c0,0.8,0,1.6-0.1,2.4
|
||||
c-1.2,17.3-15.6,30.9-33.2,30.9c-12.6,0-23.7-6.1-30.6-15.5C3.8,50.7,1.3,44.6,0.5,38c-0.1-0.8-0.2-1.6-0.2-2.3
|
||||
c0,0.8-0.1,1.6-0.1,2.4C0.2,38.8,0.2,39.6,0.3,40.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/restricted_mid.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/images/secret_mid.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/secret_small.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
@ -59,9 +59,6 @@ document.addEventListener("alpine:init", () => {
|
||||
get isEditMode() {
|
||||
return this.mode === "edit";
|
||||
},
|
||||
get isRequired() {
|
||||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
||||
}
|
||||
});
|
||||
|
||||
// Text widget
|
||||
|
||||
@ -5,126 +5,6 @@
|
||||
*/
|
||||
|
||||
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 = {}) => ({
|
||||
items: [],
|
||||
currentPage: 1,
|
||||
|
||||
57
static/js/ketcher2/DEVNOTES.md
Normal file
@ -0,0 +1,57 @@
|
||||
## Prerequisites
|
||||
|
||||
Stable [Node.js](https://nodejs.org) version
|
||||
|
||||
## Build instructions
|
||||
|
||||
npm install
|
||||
npm start
|
||||
|
||||
For production build:
|
||||
|
||||
npm run build
|
||||
|
||||
You could also build only the style with command
|
||||
|
||||
npm run style
|
||||
|
||||
## Indigo Service
|
||||
|
||||
Ketcher uses Indigo Service for server operations.
|
||||
You can use `--api-path` parameter to start with it:
|
||||
|
||||
npm start -- --api-path=<server-url>
|
||||
For production build:
|
||||
|
||||
npm run build -- --api-path=<server-url>
|
||||
|
||||
You can find the instruction for service installation
|
||||
[here](http://lifescience.opensource.epam.com/indigo/service/index.html).
|
||||
|
||||
## Tests instructions
|
||||
|
||||
You can start tests for input/output `.mol`-files and render.
|
||||
|
||||
npm test
|
||||
|
||||
Tests are started for all structures in `test/fixtures` directory.
|
||||
|
||||
To start the tests separately:
|
||||
|
||||
npm run test-io
|
||||
npm run test-render
|
||||
|
||||
#### Parameters
|
||||
|
||||
You can use following parameters to start the tests:
|
||||
- `--fixtures` - for the choice of a specific directory with molecules
|
||||
- `--headless` - for start of the browser in headless mode
|
||||
|
||||
```
|
||||
npm run test-render -- --fixtures=fixtures/super --headless
|
||||
```
|
||||
|
||||
If you have added new structures for testing to the `test/fixtures` directory
|
||||
you have to generate `svg` from them for correct render-test with:
|
||||
|
||||
npm run generate-svg
|
||||
184
static/js/ketcher2/LICENSE
Normal file
@ -0,0 +1,184 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 EPAM Systems
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
static/js/ketcher2/LICENSE-history
Normal file
@ -0,0 +1,5 @@
|
||||
Ketcher version 1 was released under GNU Affero General Public License v3.0
|
||||
Ketcher version 2 was re-licensed under Apache License, Version 2.
|
||||
|
||||
Current version is distributed by the terms of the Apache License, Version 2.
|
||||
which is included in the file LICENSE, found at the root of the Ketcher source tree.
|
||||
19
static/js/ketcher2/NOTICE
Normal file
@ -0,0 +1,19 @@
|
||||
Ketcher
|
||||
Copyright (C) 2017 EPAM Systems
|
||||
This product includes software developed at EPAM Systems, Inc.
|
||||
|
||||
In addition, this product contains dependencies on files licensed under:
|
||||
|
||||
The FreeBSD Documentation License https://www.freebsd.org/copyright/freebsd-doc-license.html
|
||||
The MIT License https://opensource.org/licenses/MIT
|
||||
X11 License http://www.xfree86.org/3.3.6/COPYRIGHT2.html
|
||||
Academic Free License https://opensource.org/licenses/AFL-3.0
|
||||
Apache License, Version 1.0 http://www.apache.org/licenses/LICENSE-1.0
|
||||
Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
|
||||
The 2-Clause BSD License https://opensource.org/licenses/BSD-2-Clause
|
||||
The 3-Clause BSD License https://opensource.org/licenses/BSD-3-Clause
|
||||
ISC License (ISC) https://opensource.org/licenses/ISC
|
||||
GNU Lesser General Public License version 2.1 https://opensource.org/licenses/LGPL-2.1
|
||||
The Mozilla Public License https://opensource.org/licenses/MPL-1.0
|
||||
Public Domain https://wiki.creativecommons.org/wiki/Public_domain
|
||||
Unlicense http://unlicense.org/
|
||||
35
static/js/ketcher2/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# EPAM Ketcher projects
|
||||
Copyright (c) 2017 EPAM Systems, Inc
|
||||
|
||||
Ketcher is an open-source web-based chemical structure editor incorporating high performance, good portability, light weight, and ability to easily integrate into a custom web-application. Ketcher is designed for chemists, laboratory scientists and technicians who draw structures and reactions.
|
||||
|
||||
## KEY FEATURES
|
||||
* Fast 2D structure representation that satisfies common chemical drawing standards
|
||||
* 3D structure visualization
|
||||
* Draw and edit structures using major tools: Atom Tool, Bond Tool, and Template Tool
|
||||
* Template library (including custom and user's templates)
|
||||
* Add atom and bond basic properties and query features, add aliases and Generic groups
|
||||
* Select, modify, and erase connected and unconnected atoms and bonds using Selection Tool, or using Shift key
|
||||
* Simple Structure Clean up Tool (checks bonds length, angles and spatial arrangement of atoms) and Advanced Structure Clean up Tool (+ stereochemistry checking and structure layout)
|
||||
* Aromatize/De-aromatize Tool
|
||||
* Calculate CIP Descriptors Tool
|
||||
* Structure Check Tool
|
||||
* MW and Structure Parameters Calculate Tool
|
||||
* Stereochemistry support during editing, loading, and saving chemical structures
|
||||
* Storing history of actions, with the ability to rollback to previous state
|
||||
* Ability to load and save structures and reactions in MDL Molfile or RXN file format, InChI String, ChemAxon Extended SMILES, ChemAxon Extended CML file formats
|
||||
* Easy to use R-Group and S-Group tools (Generic, Multiple group, SRU polymer, peratom, Data S-Group)
|
||||
* Reaction Tool (reaction generating, manual and automatic atom-to-atom mapping)
|
||||
* Flip/Rotate Tool
|
||||
* Zoom in/out, hotkeys, cut/copy/paste
|
||||
* OCR - ability to recognize structures at pictures (image files) and reproduce them
|
||||
* Copy and paste between different chemical editors
|
||||
* Settings support (Rendering, Displaying, Debugging)
|
||||
* Use of SVG to achieve best quality in-browser chemical structure rendering
|
||||
* Languages: JavaScript with third-party libraries
|
||||
|
||||
## Build instructions
|
||||
Please read [DEVNOTES.md](DEVNOTES.md) for details.
|
||||
|
||||
## License
|
||||
Please read [LICENSE](LICENSE) and [NOTICE](NOTICE) for details.
|
||||
BIN
static/js/ketcher2/doc/analyse.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/js/ketcher2/doc/atom-dialog.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/js/ketcher2/doc/attpoints-dialog.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/js/ketcher2/doc/bond-dialog.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/js/ketcher2/doc/bond-types.png
Normal file
|
After Width: | Height: | Size: 1014 B |
BIN
static/js/ketcher2/doc/bond.png
Normal file
|
After Width: | Height: | Size: 630 B |
BIN
static/js/ketcher2/doc/bonds.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/js/ketcher2/doc/chain.png
Normal file
|
After Width: | Height: | Size: 430 B |
BIN
static/js/ketcher2/doc/charge.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/js/ketcher2/doc/check.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
static/js/ketcher2/doc/collapsed.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/js/ketcher2/doc/expanded.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/js/ketcher2/doc/generic-groups.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
570
static/js/ketcher2/doc/help.md
Normal file
@ -0,0 +1,570 @@
|
||||
**Ketcher** is a tool to draw molecular structures and chemical
|
||||
reactions.
|
||||
|
||||
# Ketcher Overview
|
||||
|
||||
**Ketcher** is a tool to draw molecular structures and chemical
|
||||
reactions. Ketcher operates in two modes, the Server mode with most
|
||||
functions available and the client mode with limited functions
|
||||
available.
|
||||
|
||||
**Ketcher** consists of the following elements:
|
||||
|
||||

|
||||
|
||||
**Note** : Depending on the screen size, some tools on the _Tool
|
||||
palette_ can be displayed in expanded or collapsed forms.
|
||||
|
||||
Using the _Tool palette_, you can
|
||||
|
||||
* draw and edit a molecule or reaction by clicking on and dragging
|
||||
atoms, bonds, and other elements provided with the buttons on the
|
||||
_Atoms_ toolbar and _Tool palette_;
|
||||
|
||||
* delete any element of the drawing (atom or bond) by clicking on it
|
||||
with the Erase tool;
|
||||
|
||||
* delete the entire molecule or its fragment by a lasso,
|
||||
rectangular, or fragment selection with the Erase tool;
|
||||
|
||||
* draw special structures (see the following sections);
|
||||
|
||||
* select the entire molecule or its fragment in one of the following
|
||||
ways (click on the button to see the list of available options):
|
||||
|
||||
* in the expanded form
|
||||
|
||||

|
||||
|
||||
* in the collapsed form
|
||||
|
||||

|
||||
|
||||
To select one atom or bond, click Lasso or Rectangle Selection tool,
|
||||
and then click the atom or bond.
|
||||
|
||||
To select the entire structure:
|
||||
|
||||
* Select the Fragment Selection tool and then click the object.
|
||||
|
||||
* Select the Lasso or Rectangle Selection tool, and then drag the
|
||||
mouse to select the object.
|
||||
|
||||
* `Ctrl-click` with the Lasso or Rectangle Selection tool.
|
||||
|
||||
To select multiple atoms, bonds, structures, or other objects, do one
|
||||
of the following:
|
||||
|
||||
* `Shift-click` with the Lasso or Rectangle Selection tool selects
|
||||
some (connected or not) atoms/bonds.
|
||||
|
||||
* With the Lasso or Rectangle Selection tool click and drag the
|
||||
mouse around the atoms, bonds, or structures that you want to
|
||||
select.
|
||||
|
||||
**Note** : `Ctrl+Shift-click` with the Lasso or Rectangle Selection tool
|
||||
selects several structures.
|
||||
|
||||
You can use the buttons of the _Main_ toolbar:
|
||||
|
||||
|
||||

|
||||
|
||||
* **Clear Canvas** (1) button to start drawing a new molecule; this
|
||||
command clears the drawing area;
|
||||
|
||||
* **Open…** (2) and **Save As…** (3) buttons to import a molecule
|
||||
from a molecular file or save it to a supported molecular file
|
||||
format;
|
||||
|
||||
* **Undo** / **Redo** (4), **Cut** (5), **Copy** (6), **Paste** (7),
|
||||
**Zoom In** / **Out** (8), and **Scaling** (9) buttons to perform
|
||||
the corresponding actions;
|
||||
|
||||
* **Layout** button (10) to change the position of the structure to
|
||||
work with it with the most convenience;
|
||||
|
||||
* **Clean Up** button (11) to improve the appearance of the
|
||||
structure by assigning them uniform bond lengths and angles.
|
||||
|
||||
* **Aromatize** / **Dearomatize** buttons (12) to mark aromatic
|
||||
structures (to convert a structure to the Aromatic or Kekule
|
||||
presentation);
|
||||
|
||||
* **Calculate CIP** button (13) to determine R/S and E/Z
|
||||
configurations;
|
||||
|
||||
* **Check Structure** button (14) to check the following properties
|
||||
of the structure:
|
||||
|
||||

|
||||
|
||||
* **Calculated Values** button (15) to display some properties of
|
||||
the structure:
|
||||
|
||||

|
||||
|
||||
* **Recognize Molecule** button (16) to recognize a structure in the
|
||||
image file and load it to the canvas;
|
||||
|
||||
* **3D Viewer** button (17) to open the structure in the
|
||||
three-dimensional Viewer;
|
||||
|
||||
* **Settings** button (18) to make some settings for molecular
|
||||
files:
|
||||
|
||||

|
||||
|
||||
* **Help** button (19) to view Help;
|
||||
|
||||
* **About** button (20) to display version and copyright information
|
||||
of the program.
|
||||
|
||||
**Note** : **Layout,** **Clean Up,** **Aromatize** / **Dearomatize,**
|
||||
**Calculate CIP,** **Check Structure,** **Calculated Values,**
|
||||
**Recognize Molecule** and **3D View** buttons are active only in the
|
||||
Server mode.
|
||||
|
||||
# 3D Viewer
|
||||
|
||||
The structure appears in a modal window after clicking on the **3D
|
||||
Viewer** button:
|
||||
|
||||

|
||||
|
||||
You can perform the following actions:
|
||||
|
||||
* Rotate the structure holding the left mouse button;
|
||||
|
||||
* Zoom In/Out the structure;
|
||||
|
||||
Ketcher Settings allow to change the appearance of the structure and background coloring.
|
||||
|
||||
"Lines" drawing method, "Bright" atom name coloring
|
||||
method and "Light" background coloring are default.
|
||||
|
||||
# Drawing Atoms
|
||||
|
||||
To draw/edit atoms you can:
|
||||
|
||||
* select an atom in the Atoms toolbar and click inside the drawing
|
||||
area;
|
||||
|
||||
* if the desired atom is absent in the toolbar, click on
|
||||
the  button to invoke the Periodic Table and
|
||||
click on the desired atom (available options: _Single_ – selection
|
||||
of a single atom, _List_ – choose an atom from the list of selected
|
||||
options (To allow one atom from a list of atoms of your choice at
|
||||
that position), _Not List_ - exclude any atom on your list at that
|
||||
position).
|
||||
|
||||

|
||||
|
||||
* add an atom to the existing molecule by selecting an atom in the
|
||||
_Atoms_ toolbar, clicking on an atom in the molecule, and dragging
|
||||
the cursor; the atom will be added with a single bond; vacant
|
||||
valences will be filled with the corresponding number of hydrogen
|
||||
atoms;
|
||||
|
||||
* change an atom by selecting an atom in the _Atoms_ toolbar and
|
||||
clicking on the atom to be changed; in the case a wrong valence thus
|
||||
appears the atom will be underlined in red;
|
||||
|
||||
* change an atom by clicking on an existing atom with the
|
||||
_Selection_ tool and waiting for a couple of seconds for the text
|
||||
box to appear; type another atom symbol in the text box:
|
||||
|
||||

|
||||
|
||||
* change the charge of an atom by selecting the Charge Plus or
|
||||
Charge Minus tool and clicking consecutively on an atom to
|
||||
increase/decrease its charge
|
||||
|
||||

|
||||
|
||||
* change an atom or its properties by double-clicking on the atom to
|
||||
invoke the Atom Properties dialog (the dialog also provides atom
|
||||
query features):
|
||||
|
||||

|
||||
|
||||
* click on the Periodic Table button, open the Extended table and
|
||||
select a corresponding Generic group or Special Node:
|
||||
|
||||

|
||||
|
||||
# Drawing Bonds
|
||||
|
||||
To draw/edit bonds you can:
|
||||
|
||||
* Click an arrow on the Bond tool  in the Tools palette
|
||||
to open the drop-down list with the following bond types:
|
||||
|
||||

|
||||
|
||||
For the full screen format, the Bond tool from the Tools palette
|
||||
splits into three: _Single Bond,__Single Up Bond,_ and _Any
|
||||
Bond_,which include the corresponding bond types:
|
||||
|
||||

|
||||
|
||||
* select a bond type from the drop down list and click inside the
|
||||
drawing area; a bond of the selected type will be drawn;
|
||||
|
||||
* click on an atom in the molecule; a bond of the selected type will
|
||||
be added to the atom at the angle of 120 degrees;
|
||||
|
||||
* add a bond to the existing molecule by clicking on an atom in the
|
||||
molecule and dragging the cursor; in this case you can set the angle
|
||||
manually;
|
||||
|
||||
* change the bond type by clicking on it;
|
||||
|
||||
* use the Chain Tool  to draw consecutive single
|
||||
bonds;
|
||||
|
||||
* change a bond or its properties by double-clicking on the bond to
|
||||
invoke the Bond Properties dialog:
|
||||
|
||||

|
||||
|
||||
* clicking on a drawn stereo bond changes its direction.
|
||||
|
||||
* clicking with the Single Bond tool or Chain tool switches the bond type
|
||||
cyclically: Single-Double-Triple-Single.
|
||||
|
||||
# Drawing R-Groups
|
||||
|
||||
Use the _R-Group_ toolbox  to draw R-groups in Markush
|
||||
structures:
|
||||
|
||||

|
||||
|
||||
Selecting the _R-Group_ _Label_ Tool and clicking on an atom in the
|
||||
structure invokes the dialog to select the R-Group label for a current
|
||||
atom position in the structure:
|
||||
|
||||

|
||||
|
||||
Selecting the R-Group label and clicking **OK** converts the structure
|
||||
into a Markush structure with the selected R-Group label:
|
||||
|
||||

|
||||
|
||||
**Note** : You can choose several R-Group labels simultaneously:
|
||||
|
||||

|
||||
|
||||
Particular chemical fragments that may be substituted for a given
|
||||
R-Group form a set of R-Group members. R-Group members can be any
|
||||
structural fragment, including functional groups and single atoms or
|
||||
atom lists.
|
||||
|
||||
To create a set of R-Group members:
|
||||
|
||||
1. Draw a structure to become an R-Group member.
|
||||
|
||||
2. Select the structure using the _R-Group Fragment Tool_ to invoke
|
||||
the R-Group dialog; in this dialog select the label of the
|
||||
R-Group to assign the fragment to.
|
||||
|
||||
3. Click on **OK** to convert the structure into an R-Group member.
|
||||
|
||||
An R-Group attachment point is the atom in an R-Group member fragment
|
||||
that attaches the fragment to the initial Markush structure.
|
||||
|
||||
Selecting the _Attachment Point Tool_ and clicking on an atom in the
|
||||
R-Group fragment converts this atom into an attachment point. If the
|
||||
R-Group contains more than one attachment point, you can specify one
|
||||
of them as primary and the other as secondary. You can select between
|
||||
either the primary or secondary attachment point using the dialog that
|
||||
appears after clicking on the atom:
|
||||
|
||||

|
||||
|
||||
If there are two attachment points on an R-Group member, there must be
|
||||
two corresponding attachments (bonds) to the R-Group atom that has the
|
||||
same R-Group label. Clicking on **OK** in the above dialog creates the
|
||||
attachment point.
|
||||
|
||||
Schematically, the entire process of the R-Group member creation can
|
||||
be presented as:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# R-Group Logic
|
||||
|
||||
**Ketcher** enables one to add logic when using R-Groups. To access
|
||||
the R-Group logic:
|
||||
|
||||
1. Create an R-Group member fragment as described above.
|
||||
|
||||
2. Move the cursor over the entire fragment for the green frame to
|
||||
appear, then click inside the fragment. The following dialog
|
||||
appears:
|
||||
|
||||

|
||||
|
||||
3. Specify **Occurrence** to define how many of an R-Group
|
||||
occurs. If an R-Group atom appears several times in the initial
|
||||
structure, you will specify **Occurrence**">n", n
|
||||
being the number of occurrences; if it appears once, you see
|
||||
"R1 > 0".
|
||||
|
||||
4. Specify H at **unoccupied** R-Group sites ( **RestH** ): check or
|
||||
clear the checkbox.
|
||||
|
||||
5. Specify the logical **Condition**. Use the R-Group condition **If
|
||||
R(i) Then** to specify whether the presence of an R-Group is
|
||||
dependent on the presence of another R-Group.
|
||||
|
||||
|
||||
# Marking S-Groups
|
||||
|
||||
To mark S-Groups, use the _S-Group tool_  and the
|
||||
following dialog that appears after selecting a fragment with this
|
||||
tool:
|
||||
|
||||

|
||||
|
||||
Available S-Group types:
|
||||
|
||||
_Generic_
|
||||
|
||||
Generic is a pair of brackets without any labels.
|
||||
|
||||
_Multiple group_
|
||||
|
||||
A Multiple group indicates a number of replications of a fragment or a part of a
|
||||
structure in contracted form.
|
||||
|
||||
_SRU Polymer_
|
||||
|
||||
The Structural Repeating Unit (SRU) brackets enclose the structural
|
||||
repeating of a polymer. You have three available patterns:
|
||||
head-to-tail (the default), head-to-head, and either/unknown.
|
||||
|
||||
_Superatom_
|
||||
|
||||
An abbreviated structure (abbreviation) is all or part of a structure
|
||||
(molecule or reaction component) that has been abbreviated to a text
|
||||
label. Structures that you abbreviate keep their chemical
|
||||
significance, but their underlying structure is hidden. The current
|
||||
version can't display contracted structures but correctly
|
||||
saves/reads them into/from files.
|
||||
|
||||
# Data S-Groups
|
||||
|
||||
The _Data S-Groups Tool_  is a separate tool for
|
||||
comfortable use with the accustomed set of descriptors (like Attached
|
||||
Data in **Marvin** Editor).
|
||||
|
||||
You can attach data to an atom, a fragment, a single bond, or a
|
||||
group. The defined set of _Names_ and _Values_ is introduced for each
|
||||
type of selected elements:
|
||||
|
||||

|
||||
|
||||
* Select the appropriate S-Group Field Name.
|
||||
|
||||
* Select or type the appropriate Field Value.
|
||||
|
||||
* Labels can be specified as Absolute, Relative or Attached.
|
||||
|
||||
# Changing Structure Display
|
||||
|
||||
Use the _Flip/Rotate_ tool  to change the structure
|
||||
display:
|
||||
|
||||

|
||||
|
||||
For the full screen format, the _Flip/Rotate_ tool is split into
|
||||
separate buttons:
|
||||
|
||||

|
||||
|
||||
_Rotate Tool_
|
||||
|
||||
This tool allows rotating objects.
|
||||
* If some objects are selected, the tool rotates the selected objects.
|
||||
* If no objects are selected, or all objects are selected, the tool rotates the whole canvas
|
||||
* The default rotation step is 15 degrees.
|
||||
* Press and hold the Ctrl key for more gradual continuous rotation with 1 degree rotation step
|
||||
|
||||
Select any bond on the structure and click Alt+H to rotate the structure so that the selected bond is placed horizontally.
|
||||
Select any bond on the structure and click Alt+V to rotate the structure so that the selected bond is placed vertically.
|
||||
|
||||
_Flip Tool_
|
||||
|
||||
This tool flips the objects horizontally or vertically.
|
||||
* If some objects are selected, the Horizontal Flip tool (or Alt+H) flips the selected objects horizontally
|
||||
* If no objects are selected, or all objects are selected, the Horizontal Flip tool (or Alt+H) flips each structure horizontally
|
||||
* If some objects are selected, the Vertical Flip tool (or Alt+V) flips the selected objects vertically
|
||||
* If no objects are selected, or all objects are selected, the Vertical Flip tool (or Alt+V) flips each structure vertically
|
||||
|
||||
# Drawing Reactions
|
||||
|
||||
To draw/edit reactions you can
|
||||
|
||||
* draw reagents and products as described above;
|
||||
* use options of the _Reaction Arrow Tool_  to draw an
|
||||
arrow and pluses in the reaction equation and map same atoms in
|
||||
reagents and products.
|
||||
|
||||

|
||||
|
||||
**Note** : Reaction Auto-Mapping Tool is available only in the Server
|
||||
mode.
|
||||
|
||||
# Templates toolbar
|
||||
|
||||
You can add templates (rings or other predefined structures) to the
|
||||
structure using the _Templates_ toolbar together with the _Custom
|
||||
Templates_ button located at the bottom:
|
||||
|
||||

|
||||
|
||||
To add a ring to the molecule, select a ring from the toolbar and
|
||||
click inside the drawing area, or click on an atom or a bond in the
|
||||
molecule.
|
||||
|
||||
Rules of using templates:
|
||||
|
||||
* Selecting a template and clicking on an atom in the existing
|
||||
structure adds the template to the structure connected with a single
|
||||
bond:
|
||||
|
||||

|
||||
|
||||
* Selecting a template and dragging the cursor from an atom in the
|
||||
existing structure adds the template directly to this atom resulting
|
||||
in the fused structure:
|
||||
|
||||

|
||||
|
||||
* Dragging the cursor from an atom in the existing structure results
|
||||
in the single bond attachment if the cursor is dragged to more than
|
||||
the bond length; otherwise the fused structure is drawn.
|
||||
* Selecting a template and clicking on a bond in the existing
|
||||
structure created a bond-to-bond fused structure:
|
||||
|
||||

|
||||
|
||||
* The bond in the initial structure is replaced with the bond in the
|
||||
template.
|
||||
|
||||
* This procedure doesn't change the length of the bond in the
|
||||
initial structure.
|
||||
|
||||
* Dragging the cursor relative to the initial bond applies the
|
||||
template at the corresponding side of the bond.
|
||||
|
||||
**Note** : The added template will be fused by the default attachment
|
||||
atom or bond preset in the program.
|
||||
|
||||
**Note** : User is able to define the attachment atom and bond by clicking
|
||||
the Edit button for template structure.
|
||||
|
||||
|
||||
The _Custom Templates_ button invokes the scrolling
|
||||
list of templates available in the program; both built-in and created
|
||||
by user:
|
||||
|
||||

|
||||
|
||||
|
||||
To create a user template:
|
||||
* draw a structure.
|
||||
* click the Save as button.
|
||||
* click the Save to Templates button.
|
||||
* enter a name and define the attachment atom and bond.
|
||||
|
||||
# Working with Files
|
||||
|
||||
Ketcher supports the following molecular formats that can be entered
|
||||
either manually or from files:
|
||||
|
||||
* MDL Molfile or RXN file;
|
||||
|
||||
* Daylight SMILES (Server mode only);
|
||||
|
||||
* Daylight SMARTS (Server mode only);
|
||||
|
||||
* InChi string (Server mode only);
|
||||
|
||||
* CML file (Server mode only).
|
||||
|
||||
You can use the **Open…** and **Save As…** buttons of the _Main_
|
||||
toolbar to import a molecule from a molecular file or save it to a
|
||||
supported molecular file format. The _Open Structure_ dialog enables
|
||||
one to either browse for a file (Server mode) or manually input, e.g.,
|
||||
the Molfile ctable for the molecule to be imported:
|
||||
|
||||

|
||||
|
||||
The _Save Structure_ dialog enables one to save the molecular file:
|
||||
|
||||

|
||||
|
||||
**Note** : In the standalone version only mol/rxn are supported for
|
||||
Open and mol/rxn/SMILES for Save.
|
||||
|
||||
|
||||
# Hotkeys
|
||||
|
||||
You can use keyboard hotkeys (including Numeric keypad) for some
|
||||
features/commands of the Editor. To display the hotkeys just place the
|
||||
cursor over a toolbar button. If a hotkey is available for the button,
|
||||
it will appear in brackets after the description of the button.
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| `Esc` | Switching between the Lasso/Rectangle/Fragment Selection tools |
|
||||
| `Del` | Delete the selected objects |
|
||||
| `0` | Draw Any bond. |
|
||||
| `1` | Single / Single Up / Single Down / Single Up/Down bond. Consecutive pressing switches between these types. |
|
||||
| `2` | Double / Double Cis/Trans bond |
|
||||
| `3` | Draw a triple bond. |
|
||||
| `4` | Draw an aromatic bond. |
|
||||
| `5` | Charge Plus/Charge Minus |
|
||||
| `A` | Draw any atom |
|
||||
| `H` | Draw a hydrogen |
|
||||
| `C` | Draw a carbon |
|
||||
| `N` | Draw a nitrogen |
|
||||
| `O` | Draw an oxygen |
|
||||
| `S` | Draw a sulfur |
|
||||
| `F` | Draw a fluorine |
|
||||
| `P` | Draw a phosphorus |
|
||||
| `I` | Draw an iodine |
|
||||
| `T` | Basic templates. Consecutive pressing switches between different templates |
|
||||
| `Shift+t` | Open template library |
|
||||
| `Alt+r` | Rotate tool |
|
||||
| `Alt+v` | Flip vertically |
|
||||
| `Alt+h` | Flip horizontally |
|
||||
| `Ctrl+g` | S-Group tool / Data S-Group tool |
|
||||
| `Ctrl+d` | Align and select all S-Group data
|
||||
| `Ctrl+r` | Switching between the R-Group Label Tool/R-Group Fragment Tool/Attachment Point Tool |
|
||||
| `Ctrl+Shift+r` | R-Group Fragment Tool |
|
||||
| `Ctrl+Del` | Clear canvas |
|
||||
| `Ctrl+o` | Open |
|
||||
| `Ctrl+s` | Save As |
|
||||
| `Ctrl+z` | Undo |
|
||||
| `Ctrl+Shift+z` | Redo |
|
||||
| `Ctrl+x` | Cut selected objects |
|
||||
| `Ctrl+c` | Copy selected objects |
|
||||
| `Ctrl+v` | Paste selected objects |
|
||||
| `+` | Zoom In |
|
||||
| `-` | Zoom Out |
|
||||
| `Ctrl+l` | Layout |
|
||||
| `Ctrl+Shift+l` | Clean Up |
|
||||
| `Ctrl+p` | Calculate CIP |
|
||||
| `?` | Help |
|
||||
|
||||
**Note** : Please, use `Ctrl+V` to paste the selected object in
|
||||
Google Chrome and Mozilla Firefox browsers.
|
||||
|
||||
**Note 2** : Probably, you have forbidden access to the local storage.
|
||||
If you are using IE10 or IE11 and didn't forbid access to local storage
|
||||
intentionally, you can pay attention here: https://stackoverflow.com/a/20848924
|
||||
BIN
static/js/ketcher2/doc/inline-edit.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/js/ketcher2/doc/main.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
static/js/ketcher2/doc/miew-menu.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/js/ketcher2/doc/miew.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/js/ketcher2/doc/open.png
Normal file
|
After Width: | Height: | Size: 18 KiB |