forked from enviPath/enviPy
Compare commits
3 Commits
5758fc67fe
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 72a63b4876 | |||
| 2c2437e3f5 | |||
| 9bc9f86ff1 |
@ -35,15 +35,13 @@ RUN mkdir -p -m 0700 /root/.ssh \
|
|||||||
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
|
&& 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:
|
# 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-bayer:1.0 .
|
# docker build --ssh default -t envipath/envipy:1.0 .
|
||||||
RUN --mount=type=ssh \
|
RUN --mount=type=ssh \
|
||||||
uv sync --locked --extra ms-login --extra pepper-plugin
|
uv sync --locked --extra pepper-plugin
|
||||||
|
|
||||||
# Now copy source and do a final sync to install the project itself
|
# Now copy source and do a final sync to install the project itself
|
||||||
# Ensure .dockerignore is reasonable
|
# Ensure .dockerignore is reasonable
|
||||||
COPY bb4g bb4g
|
|
||||||
COPY biotransformer biotransformer
|
COPY biotransformer biotransformer
|
||||||
COPY bayer bayer
|
|
||||||
COPY bridge bridge
|
COPY bridge bridge
|
||||||
COPY envipath envipath
|
COPY envipath envipath
|
||||||
COPY epapi epapi
|
COPY epapi epapi
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
import enum
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from envipy_additional_information import GroupEnum as G
|
|
||||||
from envipy_additional_information import SubcategoryEnum as S
|
|
||||||
from envipy_additional_information import (
|
|
||||||
register,
|
|
||||||
register_parser_command,
|
|
||||||
EnviPyModel,
|
|
||||||
# EnviPyModelParser,
|
|
||||||
Interval,
|
|
||||||
UIConfig,
|
|
||||||
IntervalConfig,
|
|
||||||
WidgetType,
|
|
||||||
registry,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="compoundlabel", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class CompoundLabel(EnviPyModel):
|
|
||||||
label: str
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Compound Label"
|
|
||||||
label = UIConfig(widget=WidgetType.TEXT, label="Label", order=1)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("compoundlabel")
|
|
||||||
class CompoundLabelParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> CompoundLabel:
|
|
||||||
return CompoundLabel(label=data)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="studywaterstoragecapacity", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class StudyWaterStorageCapacity(EnviPyModel):
|
|
||||||
capacity: str
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Study Water Storage Capacity"
|
|
||||||
capacity = UIConfig(widget=WidgetType.TEXT, label="Study Water Storage Capacity", order=1)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("studywst")
|
|
||||||
class StudyWaterStorageCapacityParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> StudyWaterStorageCapacity:
|
|
||||||
return StudyWaterStorageCapacity(capacity=data)
|
|
||||||
|
|
||||||
|
|
||||||
class ObservationType(enum.Enum):
|
|
||||||
OBSERVED = "observed"
|
|
||||||
APPLIED = "applied"
|
|
||||||
NA = 'NA'
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="observation", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class Observation(EnviPyModel):
|
|
||||||
type: ObservationType
|
|
||||||
min_value: Optional[float] = None
|
|
||||||
max_value: Optional[float] = None
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Observation"
|
|
||||||
type = UIConfig(widget=WidgetType.SELECT, label="Observed or Applied", order=1)
|
|
||||||
min_value = UIConfig(widget=WidgetType.NUMBER, label="Min Value", order=2)
|
|
||||||
max_value = UIConfig(widget=WidgetType.NUMBER, label="Max Value", order=3)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("observation")
|
|
||||||
class ObservationParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> Observation:
|
|
||||||
parts = data.split(";")
|
|
||||||
|
|
||||||
observation_type = ObservationType(parts[0])
|
|
||||||
min_value = None
|
|
||||||
if parts[1]:
|
|
||||||
try:
|
|
||||||
min_value = float(parts[1])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
max_value = None
|
|
||||||
if parts[2]:
|
|
||||||
try:
|
|
||||||
max_value = float(parts[2])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return Observation(type=observation_type, min_value=min_value, max_value=max_value)
|
|
||||||
|
|
||||||
|
|
||||||
@register(keyname="kinetics", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
|
||||||
class Kinetics(EnviPyModel):
|
|
||||||
dt50: Interval[float]
|
|
||||||
normalized_dt50: bool
|
|
||||||
chi2err: Optional[float] = None
|
|
||||||
t_test: Optional[float] = None
|
|
||||||
swarc: Optional[float] = None
|
|
||||||
visual_fit: Optional[int] = None
|
|
||||||
comment: str
|
|
||||||
source: str
|
|
||||||
kinetic_model: str
|
|
||||||
k1: Optional[float] = None
|
|
||||||
k2: Optional[float] = None
|
|
||||||
g: Optional[float] = None
|
|
||||||
tb: Optional[float] = None
|
|
||||||
alpha: Optional[float] = None
|
|
||||||
beta: Optional[float] = None
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
title = "Kinetics"
|
|
||||||
|
|
||||||
# Field config
|
|
||||||
dt50 = IntervalConfig(label="DT50 Range", order=1, unit="d")
|
|
||||||
normalized_dt50 = UIConfig(widget=WidgetType.CHECKBOX, label="Normalized DT50", order=2)
|
|
||||||
chi2err = UIConfig(widget=WidgetType.NUMBER, label="Chi2err", order=3)
|
|
||||||
t_test = UIConfig(widget=WidgetType.NUMBER, label="T-Test", order=4)
|
|
||||||
swarc = UIConfig(widget=WidgetType.NUMBER, label="SWARC", order=5)
|
|
||||||
visual_fit = UIConfig(widget=WidgetType.NUMBER, label="Visual Fit", order=6)
|
|
||||||
comment = UIConfig(widget=WidgetType.TEXTAREA, label="Comments", order=7)
|
|
||||||
source = UIConfig(widget=WidgetType.TEXT, label="Source", order=8)
|
|
||||||
kinetic_model = UIConfig(widget=WidgetType.SELECT, label="Kinetic Model", order=9)
|
|
||||||
k1 = UIConfig(widget=WidgetType.NUMBER, label="K1", order=10)
|
|
||||||
k2 = UIConfig(widget=WidgetType.NUMBER, label="K2", order=11)
|
|
||||||
g = UIConfig(widget=WidgetType.NUMBER, label="G", order=12)
|
|
||||||
tb = UIConfig(widget=WidgetType.NUMBER, label="TB", order=13)
|
|
||||||
alpha = UIConfig(widget=WidgetType.NUMBER, label="Alpha", order=14)
|
|
||||||
beta = UIConfig(widget=WidgetType.NUMBER, label="Beta", order=15)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Expose EnviPyModelParser in lib and subclass
|
|
||||||
@register_parser_command("kineticevaluation")
|
|
||||||
class KinecticsParser:
|
|
||||||
@staticmethod
|
|
||||||
def from_string(data: str) -> Kinetics:
|
|
||||||
parts = data.split(";")
|
|
||||||
dt50 = registry.get_parser("interval").from_string(parts[0])
|
|
||||||
normalized_dt50 = parts[1] == "true"
|
|
||||||
chi2err = float(parts[2]) if parts[2] else None
|
|
||||||
t_test = float(parts[3]) if parts[3] else None
|
|
||||||
swarc = float(parts[4]) if parts[4] else None
|
|
||||||
visual_fit = int(parts[5]) if parts[5] else None
|
|
||||||
comment = parts[6]
|
|
||||||
source = parts[7]
|
|
||||||
kinetic_model = parts[8]
|
|
||||||
k1 = float(parts[9]) if parts[9] else None
|
|
||||||
k2 = float(parts[10]) if parts[10] else None
|
|
||||||
g = float(parts[11]) if parts[11] else None
|
|
||||||
tb = float(parts[12]) if parts[12] else None
|
|
||||||
alpha = float(parts[13]) if parts[13] else None
|
|
||||||
beta = float(parts[14]) if parts[14] else None
|
|
||||||
|
|
||||||
return Kinetics(
|
|
||||||
dt50=dt50,
|
|
||||||
normalized_dt50=normalized_dt50,
|
|
||||||
chi2err=chi2err,
|
|
||||||
t_test=t_test,
|
|
||||||
swarc=swarc,
|
|
||||||
visual_fit=visual_fit,
|
|
||||||
comment=comment,
|
|
||||||
source=source,
|
|
||||||
kinetic_model=kinetic_model,
|
|
||||||
k1=k1,
|
|
||||||
k2=k2,
|
|
||||||
g=g,
|
|
||||||
tb=tb,
|
|
||||||
alpha=alpha,
|
|
||||||
beta=beta,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print(KinecticsParser.from_string("187.0 - 187.0;false;;;;;;;AFO;;;;;;"))
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BayerConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'bayer'
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from bayer import additional_information # noqa: F401
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
|
||||||
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Package',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
|
||||||
('classification_level', models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted'), (20, 'Secret')], default=10)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'db_table': 'epdb_package',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('bayer', '0001_initial'),
|
|
||||||
('epdb', '0019_remove_scenario_additional_information_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='package',
|
|
||||||
name='license',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
237
bayer/models.py
237
bayer/models.py
@ -1,237 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
{% 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>
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
{% 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>
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
|
|
||||||
<dialog
|
|
||||||
id="add_pathway_pes_node_modal"
|
|
||||||
class="modal"
|
|
||||||
x-data="{
|
|
||||||
isSubmitting: false,
|
|
||||||
pesLink: null,
|
|
||||||
pesVizHtml: '',
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.isSubmitting = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
get isPESSet() {
|
|
||||||
console.log(this.pesLink);
|
|
||||||
return this.pesLink !== null;
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePesViz() {
|
|
||||||
if (!this.isPESSet) {
|
|
||||||
this.pesVizHtml = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
|
||||||
img.style.width = '100%';
|
|
||||||
img.style.height = '100%';
|
|
||||||
img.style.objectFit = 'cover';
|
|
||||||
|
|
||||||
img.onload = () => {
|
|
||||||
this.pesVizHtml = img.outerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = () => {
|
|
||||||
this.pesVizHtml = `
|
|
||||||
<div class='alert alert-error' role='alert'>
|
|
||||||
<h4 class='alert-heading'>Could not render PES!</h4>
|
|
||||||
<p>Could not render PES - Do you have access?</p>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
submit(formId) {
|
|
||||||
const form = document.getElementById(formId);
|
|
||||||
|
|
||||||
// Remove previously injected inputs
|
|
||||||
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
|
||||||
|
|
||||||
// Add values from dynamic form into the html form
|
|
||||||
if (this.formData) {
|
|
||||||
Object.entries(this.formData).forEach(([key, value]) => {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'hidden';
|
|
||||||
input.name = key;
|
|
||||||
input.value = value;
|
|
||||||
input.classList.add('dynamic-param');
|
|
||||||
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form && form.checkValidity()) {
|
|
||||||
this.isSubmitting = true;
|
|
||||||
form.submit();
|
|
||||||
} else if (form) {
|
|
||||||
form.reportValidity();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
@close="reset()"
|
|
||||||
>
|
|
||||||
<div class="modal-box max-w-3xl">
|
|
||||||
<!-- Header -->
|
|
||||||
<h3 class="text-lg font-bold">New PES</h3>
|
|
||||||
|
|
||||||
<!-- Close button (X) -->
|
|
||||||
<form method="dialog">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Body -->
|
|
||||||
<div class="py-4">
|
|
||||||
<form
|
|
||||||
id="new-pes-node-modal-form"
|
|
||||||
accept-charset="UTF-8"
|
|
||||||
action="{% url 'create pes node' current_object.package.uuid current_object.uuid %}"
|
|
||||||
method="post"
|
|
||||||
>
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="compound-name">
|
|
||||||
<span class="label-text">Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="compound-name"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
name="compound-name"
|
|
||||||
placeholder="Name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="compound-description">
|
|
||||||
<span class="label-text">Description</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="compound-description"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
name="compound-description"
|
|
||||||
placeholder="Description"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control mb-3">
|
|
||||||
<label class="label" for="pes-link">
|
|
||||||
<span class="label-text">Link to PES</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="pes-link"
|
|
||||||
name="pes-link"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
|
||||||
x-model="pesLink"
|
|
||||||
@input="updatePesViz()"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="modal-action">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
onclick="this.closest('dialog').close()"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click="submit('new-pes-node-modal-form')"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
<span x-show="!isSubmitting">Submit</span>
|
|
||||||
<span
|
|
||||||
x-show="isSubmitting"
|
|
||||||
class="loading loading-spinner loading-sm"
|
|
||||||
></span>
|
|
||||||
<span x-show="isSubmitting">Creating...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button :disabled="isSubmitting">close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{% 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">
|
|
||||||
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
|
||||||
</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 %}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{% 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">
|
|
||||||
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
|
||||||
</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 %}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1,19 +0,0 @@
|
|||||||
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",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
170
bayer/views.py
170
bayer/views.py
@ -1,170 +0,0 @@
|
|||||||
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():
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 is None:
|
|
||||||
token = pes_url.split('/')[-1] == 'dummy'
|
|
||||||
|
|
||||||
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")
|
|
||||||
198
bb4g/__init__.py
198
bb4g/__init__.py
@ -1,198 +0,0 @@
|
|||||||
import enum
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.conf import settings as s
|
|
||||||
from envipy_additional_information import register, EnviPyModel, UIConfig, WidgetType
|
|
||||||
|
|
||||||
from bridge.contracts import Classifier # noqa: I001
|
|
||||||
from bridge.dto import (
|
|
||||||
BuildResult,
|
|
||||||
EnviPyDTO,
|
|
||||||
EvaluationResult,
|
|
||||||
RunResult,
|
|
||||||
TransformationProductPrediction,
|
|
||||||
) # noqa: I001
|
|
||||||
|
|
||||||
logger = logging.getLogger("epdb")
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
while not started:
|
|
||||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
|
||||||
logger.info(f"Starting BB4G: {res.status_code}")
|
|
||||||
if res.status_code == 200:
|
|
||||||
started = True
|
|
||||||
elif res.status_code in [500, 502]:
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
retries = 0
|
|
||||||
while retries < 100:
|
|
||||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
|
|
||||||
proxies=s.PROXIES or None)
|
|
||||||
|
|
||||||
if resp.status_code == 418:
|
|
||||||
retries += 1
|
|
||||||
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
|
|
||||||
import time
|
|
||||||
time.sleep(3)
|
|
||||||
continue
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return result
|
|
||||||
@ -1,43 +1,26 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:18
|
image: postgres:18
|
||||||
container_name: eppostgres
|
container_name: envipath-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: envipath
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
- postgres_data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: epredis
|
container_name: envipath-redis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
|
||||||
- ep_bayer_redis_data:/data
|
|
||||||
|
|
||||||
biotransformer3:
|
|
||||||
image: git.envipath.com/envipath/biotransformer3:1.0
|
|
||||||
container_name: epbiotransformer3
|
|
||||||
|
|
||||||
celery_worker:
|
|
||||||
image: git.envipath.com/envipath/envipy-bayer:1.2
|
|
||||||
container_name: epcelery
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
|
||||||
volumes:
|
|
||||||
- ep_bayer_data:/opt/enviPy/
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ep_bayer_postgres_data:
|
postgres_data:
|
||||||
ep_bayer_redis_data:
|
|
||||||
ep_bayer_data:
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
- ep_postgres_data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@ -18,14 +18,14 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: epredis
|
container_name: epredis
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_redis_data:/data
|
- ep_redis_data:/data
|
||||||
|
|
||||||
biotransformer3:
|
biotransformer3:
|
||||||
image: envipath/biotransformer3:1.0
|
image: envipath/biotransformer3:1.0
|
||||||
container_name: epbiotransformer3
|
container_name: epbiotransformer3
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: envipath/envipy-bayer:1.0
|
image: envipath/envipy:1.0
|
||||||
container_name: epdjango
|
container_name: epdjango
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8000:8000"
|
- "127.0.0.1:8000:8000"
|
||||||
@ -33,18 +33,18 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_data:/opt/enviPy/
|
- ep_data:/opt/enviPy/
|
||||||
|
|
||||||
celery_worker:
|
celery_worker:
|
||||||
image: envipath/envipy-bayer:1.0
|
image: envipath/envipy:1.0
|
||||||
container_name: epcelery
|
container_name: epcelery
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_data:/opt/enviPy/
|
- ep_data:/opt/enviPy/
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ep_bayer_postgres_data:
|
ep_postgres_data:
|
||||||
ep_bayer_redis_data:
|
ep_redis_data:
|
||||||
ep_bayer_data:
|
ep_data:
|
||||||
|
|||||||
@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier
|
|||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev")
|
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env")
|
||||||
print(f"Loading env from {ENV_PATH}")
|
print(f"Loading env from {ENV_PATH}")
|
||||||
load_dotenv(ENV_PATH, override=False)
|
load_dotenv(ENV_PATH, override=False)
|
||||||
|
|
||||||
@ -143,12 +143,6 @@ if os.environ.get("USE_TEMPLATE_DB", False) == "True":
|
|||||||
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
"TEMPLATE": os.environ["TEMPLATE_DB"],
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
||||||
"LOCATION": "unique-snowflake",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
@ -275,12 +269,6 @@ LOGGING = {
|
|||||||
"filename": os.path.join(LOG_DIR, "debug.log"),
|
"filename": os.path.join(LOG_DIR, "debug.log"),
|
||||||
"formatter": "simple",
|
"formatter": "simple",
|
||||||
},
|
},
|
||||||
"auth_file": {
|
|
||||||
"level": "INFO", # Or higher
|
|
||||||
"class": "logging.FileHandler",
|
|
||||||
"filename": os.path.join(LOG_DIR, "auth.log"),
|
|
||||||
"formatter": "simple",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
# For everything under epdb/ loaded via getlogger(__name__)
|
# For everything under epdb/ loaded via getlogger(__name__)
|
||||||
@ -301,11 +289,6 @@ LOGGING = {
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||||
},
|
},
|
||||||
"auth": {
|
|
||||||
"handlers": ["auth_file"],
|
|
||||||
"propagate": True,
|
|
||||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +337,7 @@ DEFAULT_MODEL_PARAMS = {
|
|||||||
"num_chains": 10,
|
"num_chains": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_MAX_NUMBER_OF_NODES = 9999
|
DEFAULT_MAX_NUMBER_OF_NODES = 50
|
||||||
DEFAULT_MAX_DEPTH = 8
|
DEFAULT_MAX_DEPTH = 8
|
||||||
DEFAULT_MODEL_THRESHOLD = 0.25
|
DEFAULT_MODEL_THRESHOLD = 0.25
|
||||||
|
|
||||||
@ -459,48 +442,3 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T
|
|||||||
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
||||||
if BIOTRANSFORMER_ENABLED:
|
if BIOTRANSFORMER_ENABLED:
|
||||||
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
||||||
|
|
||||||
# PES
|
|
||||||
PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None)
|
|
||||||
if PES_API_MAPPING:
|
|
||||||
import json
|
|
||||||
PES_API_MAPPING = json.loads(PES_API_MAPPING)
|
|
||||||
else:
|
|
||||||
PES_API_MAPPING = {}
|
|
||||||
|
|
||||||
# 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,25 +117,28 @@ class APIPermissionTestBase(TestCase):
|
|||||||
|
|
||||||
# Create test compounds in each package
|
# Create test compounds in each package
|
||||||
cls.reviewed_compound = Compound.create(
|
cls.reviewed_compound = Compound.create(
|
||||||
cls.reviewed_package, "C", "Reviewed Compound", "Test compound"
|
cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.owned_compound = Compound.create(
|
cls.owned_compound = Compound.create(
|
||||||
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound"
|
cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.read_compound = Compound.create(
|
cls.read_compound = Compound.create(
|
||||||
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound"
|
cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.write_compound = Compound.create(
|
cls.write_compound = Compound.create(
|
||||||
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound"
|
cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.all_compound = Compound.create(
|
cls.all_compound = Compound.create(
|
||||||
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound"
|
cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.no_access_compound = Compound.create(
|
cls.no_access_compound = Compound.create(
|
||||||
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound"
|
cls.unreviewed_package_no_access,
|
||||||
|
"CCCCCC",
|
||||||
|
name="No Access Compound",
|
||||||
|
description="Test compound",
|
||||||
)
|
)
|
||||||
cls.group_compound = Compound.create(
|
cls.group_compound = Compound.create(
|
||||||
cls.group_package, "CCCCCCC", "Group Compound", "Test compound"
|
cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -294,8 +294,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Reviewed Compound {idx:03d}",
|
name=f"Reviewed Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -305,8 +305,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Draft Compound {idx:03d}",
|
name=f"Draft Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,5 +5,4 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("entra/login/", views.entra_login, name="entra_login"),
|
path("entra/login/", views.entra_login, name="entra_login"),
|
||||||
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
||||||
path("auth/token/", views.get_token, name="get_token"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,11 +2,9 @@ import msal
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from epdb.logic import UserManager, GroupManager
|
from epdb.logic import UserManager
|
||||||
from epdb.models import Group
|
|
||||||
|
|
||||||
|
|
||||||
def get_msal_app_with_cache(request):
|
def get_msal_app_with_cache(request):
|
||||||
@ -82,33 +80,6 @@ def entra_callback(request):
|
|||||||
|
|
||||||
login(request, u)
|
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
|
return redirect(s.SERVER_URL) # Handle errors
|
||||||
|
|
||||||
|
|
||||||
@ -162,9 +133,3 @@ def get_access_token_from_request(request, scopes=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_token(request):
|
|
||||||
token = get_access_token_from_request(request)
|
|
||||||
msg = f"{token}"
|
|
||||||
return HttpResponse(msg, content_type='text/plain')
|
|
||||||
|
|||||||
@ -1,2 +1,10 @@
|
|||||||
class InvalidSMILESException(Exception):
|
class InvalidSMILESException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMolfileException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PackageImportException(Exception):
|
||||||
|
pass
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import jwt
|
|
||||||
import nh3
|
import nh3
|
||||||
import requests
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.cache import cache
|
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from jwt import InvalidIssuerError
|
|
||||||
from ninja import Field, Form, Query, Router, Schema
|
from ninja import Field, Form, Query, Router, Schema
|
||||||
from ninja.security import HttpBearer
|
from ninja.security import SessionAuth
|
||||||
|
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
from utilities.misc import PackageExporter
|
from utilities.misc import PackageExporter
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
EPDBURLParser,
|
||||||
GroupManager,
|
GroupManager,
|
||||||
@ -49,26 +46,6 @@ from .models import (
|
|||||||
Package = s.GET_PACKAGE_MODEL()
|
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):
|
def get_package_for_write(user, package_uuid):
|
||||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||||
if not PackageManager.writable(user, p):
|
if not PackageManager.writable(user, p):
|
||||||
@ -82,52 +59,7 @@ def _anonymous_or_real(request):
|
|||||||
return get_user_model().objects.get(username="anonymous")
|
return get_user_model().objects.get(username="anonymous")
|
||||||
|
|
||||||
|
|
||||||
def validate_token(token: str) -> dict:
|
router = Router(auth=SessionAuth(csrf=False))
|
||||||
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):
|
class Error(Schema):
|
||||||
@ -221,6 +153,21 @@ class SimpleModel(SimpleObject):
|
|||||||
identifier: str = "relative-reasoning"
|
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 #
|
# User #
|
||||||
@ -844,10 +791,10 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
|
|||||||
|
|
||||||
class CreateCompound(Schema):
|
class CreateCompound(Schema):
|
||||||
compoundSmiles: str
|
compoundSmiles: str
|
||||||
|
compoundMolFile: str | None = None
|
||||||
compoundName: str | None = None
|
compoundName: str | None = None
|
||||||
compoundDescription: str | None = None
|
compoundDescription: str | None = None
|
||||||
inchi: str | None = None
|
inchi: str | None = None
|
||||||
pesLink: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/compound")
|
@router.post("/package/{uuid:package_uuid}/compound")
|
||||||
@ -858,32 +805,13 @@ def create_package_compound(
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
# inchi is not used atm
|
|
||||||
|
|
||||||
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():
|
|
||||||
|
|
||||||
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, c.compoundName, c.compoundDescription)
|
|
||||||
else:
|
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
p,
|
||||||
|
c.compoundSmiles,
|
||||||
|
molfile=c.compoundMolFile,
|
||||||
|
name=c.compoundName,
|
||||||
|
description=c.compoundDescription,
|
||||||
|
inchi=c.inchi,
|
||||||
)
|
)
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -1707,24 +1635,10 @@ class PathwayNode(Schema):
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_engineered_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_image(obj: Node):
|
|
||||||
return f"{obj.default_node_label.url}?image=svg"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_image_size(obj: Node):
|
def resolve_image_size(obj: Node):
|
||||||
return 400
|
return 400
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_proposed_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class PathwaySchema(Schema):
|
class PathwaySchema(Schema):
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
@ -1976,70 +1890,36 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
class CreateNode(Schema):
|
class CreateNode(Schema):
|
||||||
nodeAsSmiles: str
|
nodeAsSmiles: str
|
||||||
|
nodeAsMolFile: str | None = None
|
||||||
nodeName: str | None = None
|
nodeName: str | None = None
|
||||||
nodeReason: str | None = None
|
nodeReason: str | None = None
|
||||||
nodeDepth: str | None = None
|
nodeDepth: str | None = None
|
||||||
pesLink: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||||
response={200: str | Any, 400: Error, 403: Error},
|
response={200: str | Any, 403: Error},
|
||||||
)
|
)
|
||||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
|
||||||
# TODO Code Dup from bayer.views
|
|
||||||
|
|
||||||
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() != "":
|
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||||
node_depth = int(n.nodeDepth)
|
node_depth = int(float(n.nodeDepth))
|
||||||
else:
|
else:
|
||||||
node_depth = -1
|
node_depth = -1
|
||||||
|
|
||||||
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
n = Node.create(
|
||||||
|
pw,
|
||||||
|
n.nodeAsSmiles,
|
||||||
|
node_depth,
|
||||||
|
molfile=n.nodeAsMolFile,
|
||||||
|
name=n.nodeName,
|
||||||
|
description=n.nodeReason,
|
||||||
|
)
|
||||||
|
|
||||||
return redirect(node.url)
|
return redirect(n.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding node failed!"}
|
||||||
|
|
||||||
@ -2149,16 +2029,13 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
educts = []
|
educts = []
|
||||||
products = []
|
products = []
|
||||||
|
|
||||||
subclasses = CompoundStructure.__subclasses__()
|
|
||||||
|
|
||||||
if e.edgeAsSmirks:
|
if e.edgeAsSmirks:
|
||||||
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
||||||
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
||||||
educts.append(
|
educts.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
default_node_label=CompoundStructure.objects.get(
|
||||||
get(
|
|
||||||
compound__package=p, smiles=stand_ed
|
compound__package=p, smiles=stand_ed
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
@ -2169,8 +2046,7 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
products.append(
|
products.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
default_node_label=CompoundStructure.objects.get(
|
||||||
get(
|
|
||||||
compound__package=p, smiles=stand_pr
|
compound__package=p, smiles=stand_pr
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
@ -2392,3 +2268,26 @@ def get_setting(request, setting_uuid):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Util #
|
||||||
|
########
|
||||||
|
class NonPersistent(Schema):
|
||||||
|
smiles: str
|
||||||
|
setting_url: str = Field(..., alias="settingUri")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/util", response={200: Any, 403: Error})
|
||||||
|
def predict(request, np: Form[NonPersistent]):
|
||||||
|
try:
|
||||||
|
from epdb.logic import SPathway
|
||||||
|
|
||||||
|
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||||
|
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||||
|
spw.predict()
|
||||||
|
return spw.to_json()
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import nh3
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import QuerySet
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
@ -365,14 +364,6 @@ class PackageManager(object):
|
|||||||
|
|
||||||
groups = GroupManager.get_groups(user)
|
groups = GroupManager.get_groups(user)
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
|
|
||||||
if package.classification_level == Package.Classification.SECRET:
|
|
||||||
if package.data_pool not in groups:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
||||||
|
|
||||||
valid_perms = perms.get(permission)
|
valid_perms = perms.get(permission)
|
||||||
@ -415,7 +406,6 @@ class PackageManager(object):
|
|||||||
try:
|
try:
|
||||||
p = Package.objects.get(uuid=package_id)
|
p = Package.objects.get(uuid=package_id)
|
||||||
if PackageManager.readable(user, p):
|
if PackageManager.readable(user, p):
|
||||||
p = PackageManager.check_package_classification(user, p)
|
|
||||||
return p
|
return p
|
||||||
else:
|
else:
|
||||||
# FIXME: use custom exception to be translatable to 403 in API
|
# FIXME: use custom exception to be translatable to 403 in API
|
||||||
@ -425,37 +415,6 @@ class PackageManager(object):
|
|||||||
except Package.DoesNotExist:
|
except Package.DoesNotExist:
|
||||||
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_package_classification(user, pack: Package):
|
|
||||||
if pack.classification_level == Package.Classification.SECRET:
|
|
||||||
if pack.data_pool.user_member.filter(id=user.id).exists():
|
|
||||||
return pack
|
|
||||||
|
|
||||||
raise ValueError("Package is secret and not accessible to user!")
|
|
||||||
|
|
||||||
else:
|
|
||||||
return pack
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_package_classifications(user, package_qs: QuerySet[Package]):
|
|
||||||
non_secret = package_qs.exclude(classification_level=Package.Classification.SECRET)
|
|
||||||
secret = package_qs.filter(classification_level=Package.Classification.SECRET)
|
|
||||||
|
|
||||||
# TODO we should be able to do via the db
|
|
||||||
accessible_secret = []
|
|
||||||
|
|
||||||
for s_package in secret:
|
|
||||||
if s_package.data_pool.user_member.filter(id=user.id).exists():
|
|
||||||
accessible_secret.append(s_package.pk)
|
|
||||||
|
|
||||||
# Cannot combine a unique query with a non-unique query -> we have to call distinct
|
|
||||||
return Package.objects.filter(pk__in=accessible_secret).distinct() | non_secret.distinct()
|
|
||||||
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_readable_packages(user, include_reviewed=False):
|
def get_all_readable_packages(user, include_reviewed=False):
|
||||||
# UserPermission only exists if at least read is granted...
|
# UserPermission only exists if at least read is granted...
|
||||||
@ -482,10 +441,6 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
qs = PackageManager.check_package_classifications(user, qs)
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -532,12 +487,12 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
qs = PackageManager.check_package_classifications(user, qs)
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_packages():
|
||||||
|
return Package.objects.all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_package(current_user, name: str, description: str = None):
|
def create_package(current_user, name: str, description: str = None):
|
||||||
@ -641,25 +596,6 @@ class PackageManager(object):
|
|||||||
else:
|
else:
|
||||||
pack.reviewed = False
|
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.description = data["description"]
|
||||||
pack.save()
|
pack.save()
|
||||||
|
|
||||||
@ -745,13 +681,7 @@ class PackageManager(object):
|
|||||||
default_structure = None
|
default_structure = None
|
||||||
|
|
||||||
for structure in compound["structures"]:
|
for structure in compound["structures"]:
|
||||||
if structure.get("pesLink"):
|
|
||||||
from bayer.models import PESStructure
|
|
||||||
struc = PESStructure()
|
|
||||||
struc.pes_link = structure["pesLink"]
|
|
||||||
else:
|
|
||||||
struc = CompoundStructure()
|
struc = CompoundStructure()
|
||||||
|
|
||||||
# struc.object_url = Command.get_id(structure, keep_ids)
|
# struc.object_url = Command.get_id(structure, keep_ids)
|
||||||
struc.compound = comp
|
struc.compound = comp
|
||||||
struc.uuid = UUID(structure["id"].split("/")[-1]) if keep_ids else uuid4()
|
struc.uuid = UUID(structure["id"].split("/")[-1]) if keep_ids else uuid4()
|
||||||
@ -1081,7 +1011,7 @@ class PackageManager(object):
|
|||||||
add_import_timestamp=True,
|
add_import_timestamp=True,
|
||||||
trust_reviewed=False,
|
trust_reviewed=False,
|
||||||
) -> Package:
|
) -> Package:
|
||||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
importer = PackageImporter(data, preserve_uuids)
|
||||||
imported_package = importer.do_import()
|
imported_package = importer.do_import()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@ -1898,6 +1828,11 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if edge.rule:
|
||||||
|
e["rule"] = edge.rule.simple_json()
|
||||||
|
if edge.probability:
|
||||||
|
e["probability"] = edge.probability
|
||||||
|
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
594
epdb/migrations/0001_initial.py
Normal file
594
epdb/migrations/0001_initial.py
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-07-22 20:58
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Compound',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EPModel',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Permission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='License',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('link', models.URLField(verbose_name='link')),
|
||||||
|
('image_link', models.URLField(verbose_name='Image link')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Rule',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('hashed_key', models.CharField(max_length=128, unique=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
|
||||||
|
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompoundStructure',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('smiles', models.TextField(verbose_name='SMILES')),
|
||||||
|
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
||||||
|
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
||||||
|
('normalized_structure', models.BooleanField(default=False)),
|
||||||
|
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='default_structure',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Edge',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EnviFormer',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PluginModel',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleBaseRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(verbose_name='Group name')),
|
||||||
|
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
||||||
|
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_group',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Node',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('depth', models.IntegerField(verbose_name='Node depth')),
|
||||||
|
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
||||||
|
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
||||||
|
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='end_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='start_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Package',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
||||||
|
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_package',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Pathway',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Reaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
||||||
|
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
||||||
|
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
||||||
|
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='edge_label',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Scenario',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('scenario_date', models.CharField(default='No date', max_length=256)),
|
||||||
|
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
||||||
|
('additional_information', models.JSONField(verbose_name='Additional Information')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
||||||
|
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Setting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('public', models.BooleanField(default=False)),
|
||||||
|
('global_default', models.BooleanField(default=False)),
|
||||||
|
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
||||||
|
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
||||||
|
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
||||||
|
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='setting',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_setting',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MLRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApplicabilityDomain',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('num_neighbours', models.FloatField(default=5)),
|
||||||
|
('reliability_threshold', models.FloatField(default=0.5)),
|
||||||
|
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
||||||
|
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleAmbitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('smirks', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
||||||
|
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRDKitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRuleOrdering',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order_index', models.IntegerField()),
|
||||||
|
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
||||||
|
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sequentialrule',
|
||||||
|
name='simple_rules',
|
||||||
|
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ParallelRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='compound',
|
||||||
|
unique_together={('uuid', 'package')},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GroupPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'group')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSettingPermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('setting', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,128 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-08-25 18:07
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('epdb', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalDatabase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
||||||
|
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
||||||
|
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Database',
|
||||||
|
'verbose_name_plural': 'External Databases',
|
||||||
|
'db_table': 'epdb_external_database',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='apitoken',
|
||||||
|
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='edge',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='edge',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, help_text='Whether this token is active'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='modified',
|
||||||
|
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='functional_groups',
|
||||||
|
field=models.JSONField(blank=True, default=dict, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='app_domain',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='created',
|
||||||
|
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='expires_at',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='hashed_key',
|
||||||
|
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='num_neighbours',
|
||||||
|
field=models.IntegerField(default=5),
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='apitoken',
|
||||||
|
table='epdb_api_token',
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalIdentifier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('object_id', models.IntegerField()),
|
||||||
|
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
||||||
|
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
||||||
|
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Identifier',
|
||||||
|
'verbose_name_plural': 'External Identifiers',
|
||||||
|
'db_table': 'epdb_external_identifier',
|
||||||
|
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
||||||
|
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,228 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-08-26 17:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_url(apps, schema_editor):
|
||||||
|
MODELS = [
|
||||||
|
'User',
|
||||||
|
'Group',
|
||||||
|
'Package',
|
||||||
|
'Compound',
|
||||||
|
'CompoundStructure',
|
||||||
|
'Pathway',
|
||||||
|
'Edge',
|
||||||
|
'Node',
|
||||||
|
'Reaction',
|
||||||
|
'SimpleAmbitRule',
|
||||||
|
'SimpleRDKitRule',
|
||||||
|
'ParallelRule',
|
||||||
|
'SequentialRule',
|
||||||
|
'Scenario',
|
||||||
|
'Setting',
|
||||||
|
'MLRelativeReasoning',
|
||||||
|
'EnviFormer',
|
||||||
|
'ApplicabilityDomain',
|
||||||
|
]
|
||||||
|
for model in MODELS:
|
||||||
|
obj_cls = apps.get_model("epdb", model)
|
||||||
|
for obj in obj_cls.objects.all():
|
||||||
|
obj.url = assemble_url(obj)
|
||||||
|
if obj.url is None:
|
||||||
|
raise ValueError(f"Could not assemble url for {obj}")
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def assemble_url(obj):
|
||||||
|
from django.conf import settings as s
|
||||||
|
match obj.__class__.__name__:
|
||||||
|
case 'User':
|
||||||
|
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Group':
|
||||||
|
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Package':
|
||||||
|
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case 'Compound':
|
||||||
|
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'CompoundStructure':
|
||||||
|
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
|
||||||
|
case 'SimpleAmbitRule':
|
||||||
|
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'SimpleRDKitRule':
|
||||||
|
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'ParallelRule':
|
||||||
|
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'SequentialRule':
|
||||||
|
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
|
||||||
|
case 'Reaction':
|
||||||
|
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Pathway':
|
||||||
|
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Node':
|
||||||
|
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
|
||||||
|
case 'Edge':
|
||||||
|
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
|
||||||
|
case 'MLRelativeReasoning':
|
||||||
|
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'EnviFormer':
|
||||||
|
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'ApplicabilityDomain':
|
||||||
|
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
|
||||||
|
case 'Scenario':
|
||||||
|
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
|
||||||
|
case 'Setting':
|
||||||
|
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown model {obj.__class__.__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='scenario',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='setting',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
|
||||||
|
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compound',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='edge',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='epmodel',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='node',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='package',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rule',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='scenario',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='setting',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='url',
|
||||||
|
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-09 09:21
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='mlrelativereasoning',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='data_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='eval_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='rule_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleBasedRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
||||||
|
('min_count', models.IntegerField(default=10)),
|
||||||
|
('max_count', models.IntegerField(default=0)),
|
||||||
|
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='RuleBaseRelativeReasoning',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
epdb/migrations/0005_alter_group_group_member.py
Normal file
18
epdb/migrations/0005_alter_group_group_member.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-11 06:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0004_alter_mlrelativereasoning_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='group_member',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-09-18 06:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0005_alter_group_group_member'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mlrelativereasoning',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rulebasedrelativereasoning',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-10-07 08:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='enviformer',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='app_domain',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='data_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='eval_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='eval_results',
|
||||||
|
field=models.JSONField(blank=True, default=dict, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='model_status',
|
||||||
|
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='multigen_eval',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='enviformer',
|
||||||
|
name='rule_packages',
|
||||||
|
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
||||||
|
),
|
||||||
|
]
|
||||||
64
epdb/migrations/0008_enzymelink.py
Normal file
64
epdb/migrations/0008_enzymelink.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-10 06:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EnzymeLink",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
model_utils.fields.AutoLastModifiedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField(default="no name", verbose_name="Name")),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(default="no description", verbose_name="Descriptions"),
|
||||||
|
),
|
||||||
|
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
||||||
|
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("ec_number", models.TextField(verbose_name="EC Number")),
|
||||||
|
("classification_level", models.IntegerField(verbose_name="Classification Level")),
|
||||||
|
("linking_method", models.TextField(verbose_name="Linking Method")),
|
||||||
|
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
|
||||||
|
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
|
||||||
|
(
|
||||||
|
"rule",
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
66
epdb/migrations/0009_joblog.py
Normal file
66
epdb/migrations/0009_joblog.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-27 09:39
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0008_enzymelink"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="JobLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
model_utils.fields.AutoLastModifiedField(
|
||||||
|
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("task_id", models.UUIDField(unique=True)),
|
||||||
|
("job_name", models.TextField()),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("INITIAL", "Initial"),
|
||||||
|
("SUCCESS", "Success"),
|
||||||
|
("FAILURE", "Failure"),
|
||||||
|
("REVOKED", "Revoked"),
|
||||||
|
("IGNORED", "Ignored"),
|
||||||
|
],
|
||||||
|
default="INITIAL",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
|
||||||
|
("task_result", models.TextField(blank=True, default=None, null=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
epdb/migrations/0010_license_cc_string.py
Normal file
18
epdb/migrations/0010_license_cc_string.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-11 14:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0009_joblog"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="license",
|
||||||
|
name="cc_string",
|
||||||
|
field=models.TextField(default="by-nc-sa", verbose_name="CC string"),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
59
epdb/migrations/0011_auto_20251111_1413.py
Normal file
59
epdb/migrations/0011_auto_20251111_1413.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-11 14:13
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Min
|
||||||
|
|
||||||
|
|
||||||
|
def set_cc(apps, schema_editor):
|
||||||
|
License = apps.get_model("epdb", "License")
|
||||||
|
|
||||||
|
# For all existing licenses extract cc_string from link
|
||||||
|
for license in License.objects.all():
|
||||||
|
pattern = r"/licenses/([^/]+)/4\.0"
|
||||||
|
match = re.search(pattern, license.link)
|
||||||
|
if match:
|
||||||
|
license.cc_string = match.group(1)
|
||||||
|
license.save()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Could not find license for {license.link}")
|
||||||
|
|
||||||
|
# Ensure we have all licenses
|
||||||
|
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
|
||||||
|
for cc_string in cc_strings:
|
||||||
|
if not License.objects.filter(cc_string=cc_string).exists():
|
||||||
|
new_license = License()
|
||||||
|
new_license.cc_string = cc_string
|
||||||
|
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
|
||||||
|
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||||
|
new_license.save()
|
||||||
|
|
||||||
|
# As we might have existing Licenses representing the same License,
|
||||||
|
# get min pk and all pks as a list
|
||||||
|
license_lookup_qs = License.objects.values("cc_string").annotate(
|
||||||
|
lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",))
|
||||||
|
)
|
||||||
|
|
||||||
|
license_lookup = {
|
||||||
|
row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs
|
||||||
|
}
|
||||||
|
|
||||||
|
Packages = apps.get_model("epdb", "Package")
|
||||||
|
|
||||||
|
for k, v in license_lookup.items():
|
||||||
|
# Set min pk to all packages pointing to any of the duplicates
|
||||||
|
Packages.objects.filter(pk__in=v[1]).update(license_id=v[0])
|
||||||
|
# remove the min pk from "other" pks as we use them for deletion
|
||||||
|
v[1].remove(v[0])
|
||||||
|
# Delete redundant License objects
|
||||||
|
License.objects.filter(pk__in=v[1]).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0010_license_cc_string"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(set_cc)]
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-02 13:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0011_auto_20251111_1413"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="node",
|
||||||
|
name="stereo_removed",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="predicted",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
epdb/migrations/0013_setting_expansion_schema.py
Normal file
25
epdb/migrations/0013_setting_expansion_schema.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-14 11:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0012_node_stereo_removed_pathway_predicted"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="setting",
|
||||||
|
name="expansion_schema",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("BFS", "Breadth First Search"),
|
||||||
|
("DFS", "Depth First Search"),
|
||||||
|
("GREEDY", "Greedy"),
|
||||||
|
],
|
||||||
|
default="BFS",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-14 16:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0013_setting_expansion_schema"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="setting",
|
||||||
|
old_name="expansion_schema",
|
||||||
|
new_name="expansion_scheme",
|
||||||
|
),
|
||||||
|
]
|
||||||
17
epdb/migrations/0015_user_is_reviewer.py
Normal file
17
epdb/migrations/0015_user_is_reviewer.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-19 19:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0014_rename_expansion_schema_setting_expansion_scheme"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_reviewer",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-12 09:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0015_user_is_reviewer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="model_status",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="epmodel",
|
||||||
|
name="model_status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("INITIAL", "Initial"),
|
||||||
|
("INITIALIZING", "Model is initializing."),
|
||||||
|
("BUILDING", "Model is building."),
|
||||||
|
(
|
||||||
|
"BUILT_NOT_EVALUATED",
|
||||||
|
"Model is built and can be used for predictions, Model is not evaluated yet.",
|
||||||
|
),
|
||||||
|
("EVALUATING", "Model is evaluating"),
|
||||||
|
("FINISHED", "Model has finished building and evaluation."),
|
||||||
|
("ERROR", "Model has failed."),
|
||||||
|
],
|
||||||
|
default="INITIAL",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="enviformer",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="eval_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
name="rule_packages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PropertyPluginModel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"epmodel_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="epdb.epmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("threshold", models.FloatField(default=0.5)),
|
||||||
|
("eval_results", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("multigen_eval", models.BooleanField(default=False)),
|
||||||
|
("plugin_identifier", models.CharField(max_length=255)),
|
||||||
|
(
|
||||||
|
"app_domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="epdb.applicabilitydomain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"data_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_data_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Data Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"eval_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Evaluation Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"rule_packages",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Rule Packages",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("epdb.epmodel",),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="setting",
|
||||||
|
name="property_models",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="settings",
|
||||||
|
to="epdb.propertypluginmodel",
|
||||||
|
verbose_name="Setting Property Models",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="PluginModel",
|
||||||
|
),
|
||||||
|
]
|
||||||
93
epdb/migrations/0017_additionalinformation.py
Normal file
93
epdb/migrations/0017_additionalinformation.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-20 12:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("epdb", "0016_remove_enviformer_model_status_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AdditionalInformation",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
||||||
|
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("type", models.TextField(verbose_name="Additional Information Type")),
|
||||||
|
("data", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
("object_id", models.PositiveBigIntegerField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"content_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"package",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.EPDB_PACKAGE_MODEL,
|
||||||
|
verbose_name="Package",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"scenario",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="scenario_additional_information",
|
||||||
|
to="epdb.scenario",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"indexes": [
|
||||||
|
models.Index(fields=["type"], name="epdb_additi_type_394349_idx"),
|
||||||
|
models.Index(
|
||||||
|
fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["scenario", "content_type", "object_id"],
|
||||||
|
name="epdb_additi_scenari_ef2bf5_idx",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(
|
||||||
|
models.Q(("content_type__isnull", True), ("object_id__isnull", True)),
|
||||||
|
models.Q(("content_type__isnull", False), ("object_id__isnull", False)),
|
||||||
|
_connector="OR",
|
||||||
|
),
|
||||||
|
name="ck_addinfo_gfk_pair",
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(
|
||||||
|
("scenario__isnull", False),
|
||||||
|
("content_type__isnull", False),
|
||||||
|
_connector="OR",
|
||||||
|
),
|
||||||
|
name="ck_addinfo_not_both_null",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-20 12:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def get_additional_information(scenario):
|
||||||
|
from envipy_additional_information import registry
|
||||||
|
from envipy_additional_information.parsers import TypeOfAerationParser
|
||||||
|
|
||||||
|
for k, vals in scenario.additional_information.items():
|
||||||
|
if k == "enzyme":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k == "SpikeConentration":
|
||||||
|
k = "SpikeConcentration"
|
||||||
|
|
||||||
|
if k == "AerationType":
|
||||||
|
k = "TypeOfAeration"
|
||||||
|
|
||||||
|
for v in vals:
|
||||||
|
# Per default additional fields are ignored
|
||||||
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
|
try:
|
||||||
|
inst = MAPPING[k](**v)
|
||||||
|
except Exception:
|
||||||
|
if k == "TypeOfAeration":
|
||||||
|
toa = TypeOfAerationParser()
|
||||||
|
inst = toa.from_string(v["type"])
|
||||||
|
|
||||||
|
# Add uuid to uniquely identify objects for manipulation
|
||||||
|
if "uuid" in v:
|
||||||
|
inst.__dict__["uuid"] = v["uuid"]
|
||||||
|
|
||||||
|
yield inst
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
Scenario = apps.get_model("epdb", "Scenario")
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
bulk = []
|
||||||
|
related = []
|
||||||
|
ctype = {o.model: o for o in ContentType.objects.all()}
|
||||||
|
parents = Scenario.objects.prefetch_related(
|
||||||
|
"compound_set",
|
||||||
|
"compoundstructure_set",
|
||||||
|
"reaction_set",
|
||||||
|
"rule_set",
|
||||||
|
"pathway_set",
|
||||||
|
"node_set",
|
||||||
|
"edge_set",
|
||||||
|
).filter(parent__isnull=True)
|
||||||
|
|
||||||
|
for i, scenario in enumerate(parents):
|
||||||
|
print(f"{i + 1}/{len(parents)}", end="\r")
|
||||||
|
if scenario.parent is not None:
|
||||||
|
related.append(scenario.parent)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ai in get_additional_information(scenario):
|
||||||
|
bulk.append(
|
||||||
|
AdditionalInformation(
|
||||||
|
package=scenario.package,
|
||||||
|
scenario=scenario,
|
||||||
|
type=ai.__class__.__name__,
|
||||||
|
data=ai.model_dump(mode="json"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n", len(bulk))
|
||||||
|
|
||||||
|
related = Scenario.objects.prefetch_related(
|
||||||
|
"compound_set",
|
||||||
|
"compoundstructure_set",
|
||||||
|
"reaction_set",
|
||||||
|
"rule_set",
|
||||||
|
"pathway_set",
|
||||||
|
"node_set",
|
||||||
|
"edge_set",
|
||||||
|
).filter(parent__isnull=False)
|
||||||
|
|
||||||
|
for i, scenario in enumerate(related):
|
||||||
|
print(f"{i + 1}/{len(related)}", end="\r")
|
||||||
|
parent = scenario.parent
|
||||||
|
# Check to which objects this scenario is attached to
|
||||||
|
for ai in get_additional_information(scenario):
|
||||||
|
rel_objs = [
|
||||||
|
"compound",
|
||||||
|
"compoundstructure",
|
||||||
|
"reaction",
|
||||||
|
"rule",
|
||||||
|
"pathway",
|
||||||
|
"node",
|
||||||
|
"edge",
|
||||||
|
]
|
||||||
|
for rel_obj in rel_objs:
|
||||||
|
for o in getattr(scenario, f"{rel_obj}_set").all():
|
||||||
|
bulk.append(
|
||||||
|
AdditionalInformation(
|
||||||
|
package=scenario.package,
|
||||||
|
scenario=parent,
|
||||||
|
type=ai.__class__.__name__,
|
||||||
|
data=ai.model_dump(mode="json"),
|
||||||
|
content_type=ctype[rel_obj],
|
||||||
|
object_id=o.pk,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Start creating additional information objects...")
|
||||||
|
AdditionalInformation.objects.bulk_create(bulk)
|
||||||
|
print("Done!")
|
||||||
|
print(len(bulk))
|
||||||
|
|
||||||
|
Scenario.objects.filter(parent__isnull=False).delete()
|
||||||
|
# Call ai save to fix urls
|
||||||
|
ais = AdditionalInformation.objects.all()
|
||||||
|
total = ais.count()
|
||||||
|
|
||||||
|
for i, ai in enumerate(ais):
|
||||||
|
print(f"{i + 1}/{total}", end="\r")
|
||||||
|
ai.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0017_additionalinformation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -1,741 +1,20 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
||||||
|
|
||||||
import django.contrib.auth.models
|
from django.db import migrations
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.contrib.postgres.fields
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
("epdb", "0018_auto_20260220_1203"),
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.RemoveField(
|
||||||
name='ApplicabilityDomain',
|
model_name="scenario",
|
||||||
fields=[
|
name="additional_information",
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('num_neighbours', models.IntegerField(default=5)),
|
|
||||||
('reliability_threshold', models.FloatField(default=0.5)),
|
|
||||||
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
|
||||||
('functional_groups', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.RemoveField(
|
||||||
name='Edge',
|
model_name="scenario",
|
||||||
fields=[
|
name="parent",
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EPModel',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalDatabase',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
|
||||||
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
|
||||||
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
|
||||||
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Database',
|
|
||||||
'verbose_name_plural': 'External Databases',
|
|
||||||
'db_table': 'epdb_external_database',
|
|
||||||
'ordering': ['name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Permission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='License',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('cc_string', models.TextField(verbose_name='CC string')),
|
|
||||||
('link', models.URLField(verbose_name='link')),
|
|
||||||
('image_link', models.URLField(verbose_name='Image link')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Rule',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('email', models.EmailField(max_length=254, unique=True)),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('is_reviewer', models.BooleanField(default=False)),
|
|
||||||
('default_package', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Default Package')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'user',
|
|
||||||
'verbose_name_plural': 'users',
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='APIToken',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('hashed_key', models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)),
|
|
||||||
('expires_at', models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True)),
|
|
||||||
('name', models.CharField(help_text='Descriptive name for this token', max_length=100)),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Whether this token is active')),
|
|
||||||
('user', models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'API Token',
|
|
||||||
'verbose_name_plural': 'API Tokens',
|
|
||||||
'db_table': 'epdb_api_token',
|
|
||||||
'ordering': ['-created'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Compound',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CompoundStructure',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('smiles', models.TextField(verbose_name='SMILES')),
|
|
||||||
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
|
||||||
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
|
||||||
('normalized_structure', models.BooleanField(default=False)),
|
|
||||||
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='default_structure',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PropertyPluginModel',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('plugin_identifier', models.CharField(max_length=255)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Group',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('name', models.TextField(verbose_name='Group name')),
|
|
||||||
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('group_member', models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
|
||||||
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_group',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='JobLog',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('task_id', models.UUIDField(unique=True)),
|
|
||||||
('job_name', models.TextField()),
|
|
||||||
('status', models.CharField(choices=[('INITIAL', 'Initial'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKED', 'Revoked'), ('IGNORED', 'Ignored')], default='INITIAL', max_length=20)),
|
|
||||||
('done_at', models.DateTimeField(blank=True, default=None, null=True)),
|
|
||||||
('task_result', models.TextField(blank=True, default=None, null=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Package',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
|
||||||
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'swappable': 'EPDB_PACKAGE_MODEL',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Node',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('depth', models.IntegerField(verbose_name='Node depth')),
|
|
||||||
('stereo_removed', models.BooleanField(default=False)),
|
|
||||||
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
|
||||||
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
|
||||||
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='end_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='start_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Pathway',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('predicted', models.BooleanField(default=False)),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Reaction',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
|
||||||
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
|
||||||
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
|
||||||
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EnzymeLink',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('ec_number', models.TextField(verbose_name='EC Number')),
|
|
||||||
('classification_level', models.IntegerField(verbose_name='Classification Level')),
|
|
||||||
('linking_method', models.TextField(verbose_name='Linking Method')),
|
|
||||||
('edge_evidence', models.ManyToManyField(to='epdb.edge')),
|
|
||||||
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.rule')),
|
|
||||||
('reaction_evidence', models.ManyToManyField(to='epdb.reaction')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='edge_label',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Scenario',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('scenario_date', models.CharField(default='No date', max_length=256)),
|
|
||||||
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reaction',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compoundstructure',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Setting',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('public', models.BooleanField(default=False)),
|
|
||||||
('global_default', models.BooleanField(default=False)),
|
|
||||||
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
|
||||||
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
|
||||||
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
|
||||||
('expansion_scheme', models.CharField(choices=[('BFS', 'Breadth First Search'), ('DFS', 'Depth First Search'), ('GREEDY', 'Greedy')], default='BFS', max_length=20)),
|
|
||||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Setting Rule Packages')),
|
|
||||||
('property_models', models.ManyToManyField(blank=True, related_name='settings', to='epdb.propertypluginmodel', verbose_name='Setting Property Models')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='setting',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_setting',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EnviFormer',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MLRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='model',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='app_domain',
|
|
||||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='data_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='eval_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='propertypluginmodel',
|
|
||||||
name='rule_packages',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RuleBasedRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('multigen_eval', models.BooleanField(default=False)),
|
|
||||||
('min_count', models.IntegerField(default=10)),
|
|
||||||
('max_count', models.IntegerField(default=0)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalIdentifier',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('object_id', models.IntegerField()),
|
|
||||||
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
|
||||||
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
|
||||||
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
|
||||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
||||||
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Identifier',
|
|
||||||
'verbose_name_plural': 'External Identifiers',
|
|
||||||
'db_table': 'epdb_external_identifier',
|
|
||||||
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
|
||||||
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleAmbitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('smirks', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
|
||||||
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRDKitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRuleOrdering',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('order_index', models.IntegerField()),
|
|
||||||
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
|
||||||
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sequentialrule',
|
|
||||||
name='simple_rules',
|
|
||||||
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ParallelRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='compound',
|
|
||||||
unique_together={('uuid', 'package')},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AdditionalInformation',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('type', models.TextField(verbose_name='Additional Information Type')),
|
|
||||||
('data', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
|
||||||
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
|
||||||
('scenario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scenario_additional_information', to='epdb.scenario')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [models.Index(fields=['type'], name='epdb_additi_type_394349_idx'), models.Index(fields=['scenario', 'type'], name='epdb_additi_scenari_a59edf_idx'), models.Index(fields=['content_type', 'object_id'], name='epdb_additi_content_44d4b4_idx'), models.Index(fields=['scenario', 'content_type', 'object_id'], name='epdb_additi_scenari_ef2bf5_idx')],
|
|
||||||
'constraints': [models.CheckConstraint(condition=models.Q(models.Q(('content_type__isnull', True), ('object_id__isnull', True)), models.Q(('content_type__isnull', False), ('object_id__isnull', False)), _connector='OR'), name='ck_addinfo_gfk_pair'), models.CheckConstraint(condition=models.Q(('scenario__isnull', False), ('content_type__isnull', False), _connector='OR'), name='ck_addinfo_not_both_null')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GroupPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'group')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserSettingPermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('setting', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -46,9 +46,4 @@ class Migration(migrations.Migration):
|
|||||||
name="molfile",
|
name="molfile",
|
||||||
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
|
||||||
model_name="group",
|
|
||||||
name="secret",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="Secret Group"),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -23,7 +23,6 @@ MAPPING = {
|
|||||||
"true": HalfLifeModel.SFO,
|
"true": HalfLifeModel.SFO,
|
||||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||||
"other": HalfLifeModel.OTHER,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
291
epdb/models.py
291
epdb/models.py
@ -31,7 +31,10 @@ from sklearn.model_selection import ShuffleSplit
|
|||||||
|
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from bridge.dto import RunResult, PropertyPrediction
|
from bridge.dto import RunResult, PropertyPrediction
|
||||||
from epdb.exceptions import InvalidSMILESException
|
from epdb.exceptions import (
|
||||||
|
InvalidMolfileException,
|
||||||
|
InvalidSMILESException,
|
||||||
|
)
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
ApplicabilityDomainPCA,
|
||||||
@ -205,7 +208,6 @@ class Group(TimeStampedModel):
|
|||||||
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
||||||
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
||||||
public = models.BooleanField(verbose_name="Public Group", default=False)
|
public = models.BooleanField(verbose_name="Public Group", default=False)
|
||||||
secret = models.BooleanField(verbose_name="Secret Group", default=False)
|
|
||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
||||||
)
|
)
|
||||||
@ -577,10 +579,6 @@ class ReactionIdentifierMixin(ExternalIdentifierMixin):
|
|||||||
def get_uniprot_identifiers(self):
|
def get_uniprot_identifiers(self):
|
||||||
return self.get_external_identifier("UniProt")
|
return self.get_external_identifier("UniProt")
|
||||||
|
|
||||||
def contains_pes(self):
|
|
||||||
from bayer.models import PESStructure
|
|
||||||
return any([isinstance(o, PESStructure) for o in self.educts.all()]) or any(
|
|
||||||
[isinstance(o, PESStructure) for o in self.products.all()])
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# EP Objects #
|
# EP Objects #
|
||||||
@ -794,6 +792,13 @@ class Compound(
|
|||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
|
def get_structure_by_smiles(self, smiles: str) -> "CompoundStructure":
|
||||||
|
for struct in self.structures.all():
|
||||||
|
if struct.smiles == smiles:
|
||||||
|
return struct
|
||||||
|
|
||||||
|
raise ValueError(f"No structure with SMILES {smiles} found for {self.get_name()}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def structures(self) -> QuerySet:
|
def structures(self) -> QuerySet:
|
||||||
return CompoundStructure.objects.filter(compound=self)
|
return CompoundStructure.objects.filter(compound=self)
|
||||||
@ -862,8 +867,24 @@ class Compound(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
package: "Package",
|
||||||
|
smiles: str,
|
||||||
|
molfile: str | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles.strip() == "":
|
if smiles is None or smiles.strip() == "":
|
||||||
raise InvalidSMILESException("SMILES is required")
|
raise InvalidSMILESException("SMILES is required")
|
||||||
|
|
||||||
@ -873,50 +894,34 @@ class Compound(
|
|||||||
if parsed is None:
|
if parsed is None:
|
||||||
raise InvalidSMILESException("Given SMILES is invalid")
|
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()
|
|
||||||
|
|
||||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
|
|
||||||
subclasses = CompoundStructure.__subclasses__()
|
|
||||||
|
|
||||||
qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package)
|
|
||||||
if subclasses:
|
|
||||||
qs = qs.not_instance_of(*subclasses)
|
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES
|
# Check if we find a direct match for a given SMILES
|
||||||
if qs.exists():
|
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
|
||||||
found_structure = qs.first()
|
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
|
||||||
found_compound = found_structure.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
|
# Check if we can find the standardized one
|
||||||
if qs.exists():
|
if CompoundStructure.objects.filter(
|
||||||
# TODO should we add a structure?
|
smiles=standardized_smiles, compound__package=package
|
||||||
found_structure = qs.first()
|
).exists():
|
||||||
found_compound = found_structure.compound
|
found_c = CompoundStructure.objects.get(
|
||||||
|
smiles=standardized_smiles, compound__package=package
|
||||||
|
).compound
|
||||||
|
|
||||||
if name:
|
# As a direct match wasn't found add a new structure
|
||||||
found_structure.add_alias(name)
|
# TODO return newly created structure
|
||||||
found_compound.add_alias(name)
|
_ = found_c.add_structure(smiles, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
return found_compound
|
return found_c
|
||||||
|
|
||||||
# Generate Compound
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
c.package = package
|
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 == "":
|
if name is None or name == "":
|
||||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
@ -940,7 +945,12 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
c, smiles, name=name, description=description, normalized_structure=is_standardized
|
c,
|
||||||
|
smiles,
|
||||||
|
molfile=molfile,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
c.default_structure = cs
|
c.default_structure = cs
|
||||||
@ -953,11 +963,22 @@ class Compound(
|
|||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
|
molfile: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
default_structure: bool = False,
|
default_structure: bool = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "CompoundStructure":
|
) -> "CompoundStructure":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles == "":
|
if smiles is None or smiles == "":
|
||||||
raise ValueError("SMILES is required")
|
raise ValueError("SMILES is required")
|
||||||
|
|
||||||
@ -977,16 +998,28 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_standardized:
|
if is_standardized:
|
||||||
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
||||||
if CompoundStructure.objects.filter(
|
if CompoundStructure.objects.filter(smiles=smiles, compound__package=self.package).exists():
|
||||||
smiles__in=smiles, compound__package=self.package
|
found_cs = CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
).exists():
|
|
||||||
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
|
logger.info(
|
||||||
|
f"Setting molfile for found CompoundStructure(uuid={found_cs.uuid}) as it was empty"
|
||||||
|
)
|
||||||
|
found_cs.molfile = molfile
|
||||||
|
found_cs.save()
|
||||||
|
|
||||||
|
return found_cs
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
self, smiles, name=name, description=description, normalized_structure=is_standardized
|
self,
|
||||||
|
smiles,
|
||||||
|
name=name,
|
||||||
|
molfile=molfile,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
if default_structure:
|
if default_structure:
|
||||||
@ -1167,17 +1200,33 @@ class CompoundStructure(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
compound: Compound, smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
|
compound: Compound,
|
||||||
|
smiles: str,
|
||||||
|
molfile: str = None,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
# Clean for potential XSS
|
# Molfile has precendence over SMILES
|
||||||
if name is not None:
|
if molfile is not None and molfile.strip() != "":
|
||||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
||||||
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
found_cs = CompoundStructure.objects.get(smiles=smiles, compound=compound)
|
||||||
|
|
||||||
if name:
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
found_cs.add_alias(name)
|
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 found_cs
|
||||||
|
|
||||||
@ -1185,18 +1234,18 @@ class CompoundStructure(
|
|||||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||||
|
|
||||||
cs = CompoundStructure()
|
cs = CompoundStructure()
|
||||||
|
# Clean for potential XSS
|
||||||
if name is not None:
|
if name is not None:
|
||||||
cs.name = name
|
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if description is not None:
|
if description is not None:
|
||||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
cs.smiles = smiles
|
|
||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
cs.smiles = smiles
|
||||||
|
|
||||||
# Check if molfile is present and valid
|
# Check if molfile is present and valid
|
||||||
if molfile is not None and FormatConverter.from_molfile(molfile) is not None:
|
if molfile is not None and molfile.strip() != "":
|
||||||
cs.molfile = molfile
|
cs.molfile = molfile
|
||||||
|
|
||||||
if "normalized_structure" in kwargs:
|
if "normalized_structure" in kwargs:
|
||||||
@ -1415,9 +1464,6 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
if not FormatConverter.is_valid_smirks(smirks):
|
if not FormatConverter.is_valid_smirks(smirks):
|
||||||
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
|
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)
|
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
|
||||||
|
|
||||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
||||||
@ -1429,17 +1475,14 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
if query.exists():
|
if query.exists():
|
||||||
if query.count() > 1:
|
if query.count() > 1:
|
||||||
logger.error(f"More than one rule matched this one! {query}")
|
logger.error(f"More than one rule matched this one! {query}")
|
||||||
|
return query.first()
|
||||||
found_rule = query.first()
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_rule.add_alias(name)
|
|
||||||
|
|
||||||
return found_rule
|
|
||||||
|
|
||||||
r = SimpleAmbitRule()
|
r = SimpleAmbitRule()
|
||||||
r.package = package
|
r.package = package
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if name is None or name == "":
|
if name is None or name == "":
|
||||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
@ -1687,11 +1730,6 @@ class Reaction(
|
|||||||
rules: Union[Rule | List[Rule]] = None,
|
rules: Union[Rule | List[Rule]] = None,
|
||||||
multi_step: bool = False,
|
multi_step: bool = False,
|
||||||
):
|
):
|
||||||
|
|
||||||
# Clean for potential XSS
|
|
||||||
if name is not None and name.strip() != "":
|
|
||||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
|
|
||||||
_educts = []
|
_educts = []
|
||||||
_products = []
|
_products = []
|
||||||
|
|
||||||
@ -1746,19 +1784,14 @@ class Reaction(
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
||||||
)
|
)
|
||||||
|
return existing_reaction_qs.first()
|
||||||
found_reaction = existing_reaction_qs.first()
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_reaction.add_alias(name)
|
|
||||||
|
|
||||||
return found_reaction
|
|
||||||
|
|
||||||
r = Reaction()
|
r = Reaction()
|
||||||
r.package = package
|
r.package = package
|
||||||
|
|
||||||
if r is not None:
|
# Clean for potential XSS
|
||||||
r.name = name
|
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() != "":
|
if description is not None and description.strip() != "":
|
||||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
@ -1994,6 +2027,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
# add links start -> pseudo
|
# add links start -> pseudo
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -2003,6 +2037,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"source": node_url_to_idx[link["start_node_urls"][0]],
|
"source": node_url_to_idx[link["start_node_urls"][0]],
|
||||||
"target": pseudo_idx,
|
"target": pseudo_idx,
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
|
"to_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2010,6 +2045,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
for target in link["end_node_urls"]:
|
for target in link["end_node_urls"]:
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -2020,6 +2056,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"target": node_url_to_idx[target],
|
"target": node_url_to_idx[target],
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
"multi_step": link["multi_step"],
|
"multi_step": link["multi_step"],
|
||||||
|
"from_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2229,11 +2266,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
def add_node(
|
def add_node(
|
||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
depth: Optional[int] = -1,
|
description: str | None = None,
|
||||||
|
depth: int = -1,
|
||||||
):
|
):
|
||||||
return Node.create(self, smiles, depth, name=name, description=description)
|
return Node.create(self, smiles, depth, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_edge(
|
def add_edge(
|
||||||
@ -2329,17 +2367,19 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name is None or self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.default_node_label.name} (taken from underlying structure)"
|
if include_suffix:
|
||||||
)
|
return f"{self.default_node_label.name} (taken from underlying structure)"
|
||||||
|
else:
|
||||||
|
return self.default_node_label.name
|
||||||
|
|
||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
app_domain_data = self.get_app_domain_assessment_data()
|
app_domain_data = self.get_app_domain_assessment_data()
|
||||||
@ -2369,6 +2409,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
),
|
),
|
||||||
"image_type": "svg",
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"smiles": self.default_node_label.smiles,
|
"smiles": self.default_node_label.smiles,
|
||||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
||||||
"app_domain": {
|
"app_domain": {
|
||||||
@ -2391,28 +2432,43 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
pathway: "Pathway",
|
pathway: "Pathway",
|
||||||
smiles: str,
|
smiles: str,
|
||||||
depth: int,
|
depth: int,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
):
|
):
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
stereo_removed = False
|
stereo_removed = False
|
||||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
stereo_removed = True
|
stereo_removed = True
|
||||||
|
|
||||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
c = Compound.create(
|
||||||
|
pathway.package, smiles, molfile=molfile, name=name, description=description
|
||||||
|
)
|
||||||
|
|
||||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
structure = c.get_structure_by_smiles(smiles)
|
||||||
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
|
||||||
|
if Node.objects.filter(pathway=pathway, default_node_label=structure).exists():
|
||||||
|
return Node.objects.get(pathway=pathway, default_node_label=structure)
|
||||||
|
|
||||||
n = Node()
|
n = Node()
|
||||||
n.stereo_removed = stereo_removed
|
n.stereo_removed = stereo_removed
|
||||||
n.pathway = pathway
|
n.pathway = pathway
|
||||||
n.depth = depth
|
n.depth = depth
|
||||||
|
|
||||||
n.default_node_label = c.default_structure
|
n.default_node_label = structure
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
n.node_labels.add(c.default_structure)
|
n.node_labels.add(structure)
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
return n
|
return n
|
||||||
@ -2460,6 +2516,28 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def is_proposed_intermediate(self):
|
||||||
|
collected = defaultdict(dict)
|
||||||
|
for ai in self.additional_information.filter(
|
||||||
|
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"],
|
||||||
|
scenario__isnull=False,
|
||||||
|
):
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||||
|
|
||||||
|
if ai.type == "ProposedIntermediate":
|
||||||
|
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||||
|
|
||||||
|
if ai.type == "Confidence":
|
||||||
|
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||||
|
|
||||||
|
if ai.type == "TransformationProductImportance":
|
||||||
|
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
||||||
|
ai.get().importance.value
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(collected.values())
|
||||||
|
|
||||||
|
|
||||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2481,6 +2559,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
edge_json = {
|
edge_json = {
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"id": self.url,
|
"id": self.url,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"image": self.url + "?image=svg",
|
"image": self.url + "?image=svg",
|
||||||
@ -2595,17 +2674,19 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.edge_label.name} (taken from underlying reaction)"
|
if include_suffix:
|
||||||
)
|
return f"{self.edge_label.name} (taken from underlying reaction)"
|
||||||
|
else:
|
||||||
|
return self.edge_label.name
|
||||||
|
|
||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
from .exceptions import InvalidSMILESException
|
from .exceptions import InvalidMolfileException, InvalidSMILESException
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
EPDBURLParser,
|
||||||
@ -389,9 +389,6 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
|||||||
"debug": s.DEBUG,
|
"debug": s.DEBUG,
|
||||||
"external_databases": ExternalDatabase.get_databases(),
|
"external_databases": ExternalDatabase.get_databases(),
|
||||||
"site_id": s.MATOMO_SITE_ID,
|
"site_id": s.MATOMO_SITE_ID,
|
||||||
# EDIT START
|
|
||||||
"secret_groups": Group.objects.filter(secret=True),
|
|
||||||
# EDIT END
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,38 +588,10 @@ def packages(request):
|
|||||||
"package-description", s.DEFAULT_VALUES["description"]
|
"package-description", s.DEFAULT_VALUES["description"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# EDIT START
|
|
||||||
data_pool = None
|
|
||||||
package_classification = request.POST.get("package-classification")
|
|
||||||
classification = Package.Classification(int(package_classification))
|
|
||||||
# For SECRET we'll need a data pool which will be an additional perm check later
|
|
||||||
if classification == Package.Classification.SECRET:
|
|
||||||
package_data_pool = request.POST.get("package-data-pool")
|
|
||||||
|
|
||||||
if package_data_pool is None:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool is required!")
|
|
||||||
|
|
||||||
data_pool = GroupManager.get_group_by_url(current_user, package_data_pool)
|
|
||||||
|
|
||||||
if data_pool is None:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool does not exist or no access!")
|
|
||||||
|
|
||||||
if not data_pool.secret:
|
|
||||||
return error(request, "Invalid data pool.", "Data Pool is not a secret group!")
|
|
||||||
|
|
||||||
created_package = PackageManager.create_package(
|
created_package = PackageManager.create_package(
|
||||||
current_user, package_name, package_description
|
current_user, package_name, package_description
|
||||||
)
|
)
|
||||||
|
|
||||||
created_package.classification_level = classification
|
|
||||||
|
|
||||||
# Set previously determined data pool
|
|
||||||
if classification == Package.Classification.SECRET:
|
|
||||||
created_package.data_pool = data_pool
|
|
||||||
|
|
||||||
created_package.save()
|
|
||||||
# EDIT END
|
|
||||||
|
|
||||||
return redirect(created_package.url)
|
return redirect(created_package.url)
|
||||||
|
|
||||||
elif request.method == "OPTIONS":
|
elif request.method == "OPTIONS":
|
||||||
@ -1415,12 +1384,18 @@ def package_compounds(request, package_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
compound_name = request.POST.get("compound-name")
|
compound_name = request.POST.get("compound-name")
|
||||||
compound_smiles = request.POST.get("compound-smiles")
|
compound_smiles = request.POST.get("compound-smiles")
|
||||||
|
compound_molfile = request.POST.get("compound-molfile")
|
||||||
compound_description = request.POST.get("compound-description")
|
compound_description = request.POST.get("compound-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
current_package, compound_smiles, compound_name, compound_description
|
current_package,
|
||||||
|
compound_smiles,
|
||||||
|
molfile=compound_molfile,
|
||||||
|
name=compound_name,
|
||||||
|
description=compound_description,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except (InvalidSMILESException, InvalidMolfileException) as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
@ -1546,11 +1521,15 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
structure_name = request.POST.get("structure-name")
|
structure_name = request.POST.get("structure-name")
|
||||||
structure_smiles = request.POST.get("structure-smiles")
|
structure_smiles = request.POST.get("structure-smiles")
|
||||||
|
structure_molfile = request.POST.get("structure-molfile")
|
||||||
structure_description = request.POST.get("structure-description")
|
structure_description = request.POST.get("structure-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cs = current_compound.add_structure(
|
cs = current_compound.add_structure(
|
||||||
structure_smiles, structure_name, structure_description
|
structure_smiles,
|
||||||
|
molfile=structure_molfile,
|
||||||
|
name=structure_name,
|
||||||
|
description=structure_description,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return error(
|
return error(
|
||||||
@ -2121,12 +2100,14 @@ def package_pathways(request, package_uuid):
|
|||||||
else:
|
else:
|
||||||
prediction_setting = current_user.prediction_settings()
|
prediction_setting = current_user.prediction_settings()
|
||||||
|
|
||||||
|
is_predict_mode = pw_mode in {"predict", "incremental"}
|
||||||
|
|
||||||
pw = Pathway.create(
|
pw = Pathway.create(
|
||||||
current_package,
|
current_package,
|
||||||
stand_smiles,
|
stand_smiles if is_predict_mode else smiles,
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
predicted=pw_mode in {"predict", "incremental"},
|
predicted=is_predict_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set mode
|
# set mode
|
||||||
@ -2368,9 +2349,12 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
|||||||
node_description = request.POST.get("node-description")
|
node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
node_smiles = request.POST.get("node-smiles").strip()
|
node_smiles = request.POST.get("node-smiles").strip()
|
||||||
|
node_molfile = request.POST.get("node-molfile").strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
current_pathway.add_node(
|
||||||
|
node_smiles, molfile=node_molfile, name=node_name, description=node_description
|
||||||
|
)
|
||||||
except InvalidSMILESException:
|
except InvalidSMILESException:
|
||||||
return error(
|
return error(
|
||||||
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
||||||
@ -2482,7 +2466,26 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
return JsonResponse({"success": current_node.url})
|
return JsonResponse({"success": current_node.url})
|
||||||
|
|
||||||
return HttpResponseBadRequest()
|
new_node_name = request.POST.get("node-name")
|
||||||
|
new_node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
|
if any([new_node_name, new_node_description]):
|
||||||
|
if new_node_name is not None and new_node_name.strip() != "":
|
||||||
|
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
current_node.name = new_node_name
|
||||||
|
|
||||||
|
if new_node_description is not None and new_node_description.strip() != "":
|
||||||
|
new_node_description = nh3.clean(
|
||||||
|
new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
|
||||||
|
).strip()
|
||||||
|
current_node.description = new_node_description
|
||||||
|
|
||||||
|
current_node.save()
|
||||||
|
|
||||||
|
return redirect(current_node.url)
|
||||||
|
|
||||||
|
return error(request, "Node update failed!", "No changes were made to the node")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
|
|
||||||
@ -3014,15 +3017,9 @@ def settings(request):
|
|||||||
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
||||||
|
|
||||||
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
|
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
|
||||||
temp_max_nodes = request.POST.get("prediction-setting-max-nodes")
|
|
||||||
if temp_max_nodes is None or temp_max_nodes == "" or int(temp_max_nodes) == -1:
|
|
||||||
temp_max_nodes = s.DEFAULT_MAX_NUMBER_OF_NODES
|
|
||||||
else:
|
|
||||||
temp_max_nodes = int(request.POST.get("prediction-setting-max-nodes", 1))
|
|
||||||
|
|
||||||
max_nodes = min(
|
max_nodes = min(
|
||||||
max(
|
max(
|
||||||
temp_max_nodes,
|
int(request.POST.get("prediction-setting-max-nodes", 1)),
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
||||||
@ -3043,7 +3040,6 @@ def settings(request):
|
|||||||
|
|
||||||
model_uuid = model_url.split("/")[-1]
|
model_uuid = model_url.split("/")[-1]
|
||||||
params["model"] = EPModel.objects.get(uuid=model_uuid)
|
params["model"] = EPModel.objects.get(uuid=model_uuid)
|
||||||
# TODO Check if removed if request contains "" or not at all
|
|
||||||
params["model_threshold"] = request.POST.get(
|
params["model_threshold"] = request.POST.get(
|
||||||
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -39,3 +39,7 @@ select.select[multiple] {
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,30 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@ -5,6 +5,126 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
|
||||||
|
const basePagination = (
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId
|
||||||
|
) => ({
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId,
|
||||||
|
|
||||||
|
get paginatedItems() {
|
||||||
|
return this.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingStart() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return (this.currentPage - 1) * this.perPage + 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingEnd() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||||
|
},
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.fetchPage(this.currentPage + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.fetchPage(this.currentPage - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
if (page >= 1 && page <= this.totalPages) {
|
||||||
|
this.fetchPage(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get pageNumbers() {
|
||||||
|
const pages = [];
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.currentPage;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||||
|
|
||||||
|
let rangeStart;
|
||||||
|
let rangeEnd;
|
||||||
|
|
||||||
|
if (current <= 4) {
|
||||||
|
rangeStart = 2;
|
||||||
|
rangeEnd = 5;
|
||||||
|
} else if (current >= total - 3) {
|
||||||
|
rangeStart = total - 4;
|
||||||
|
rangeEnd = total - 1;
|
||||||
|
} else {
|
||||||
|
rangeStart = current - 1;
|
||||||
|
rangeEnd = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart > 2) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd < total - 1) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Alpine.data('paginatedList',
|
||||||
|
(items, options = {}) => ({
|
||||||
|
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchPage(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPage(page) {
|
||||||
|
this.totalItems = this.items.length;
|
||||||
|
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||||
|
this.currentPage = page
|
||||||
|
|
||||||
|
const start = page * this.perPage - this.perPage;
|
||||||
|
const end = page * this.perPage;
|
||||||
|
return this.items.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||||
items: [],
|
items: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|||||||
121
static/js/pw.js
121
static/js/pw.js
@ -1,13 +1,19 @@
|
|||||||
console.log("loaded pw.js")
|
console.log("loaded pw.js")
|
||||||
|
|
||||||
function findPaths(source_idx, target_idx, links) {
|
function findPaths(source_idx, links) {
|
||||||
const resultLinks = new Set();
|
const resultLinks = new Set();
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
// Helper function for depth-first search
|
// Helper function for depth-first search
|
||||||
function dfs(current) {
|
function dfs(current) {
|
||||||
if (current === target_idx) {
|
|
||||||
|
if (visited.has(current)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (links.filter(link => link.target.id === current).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
visited.add(current);
|
visited.add(current);
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
if (
|
if (
|
||||||
@ -23,7 +29,7 @@ function findPaths(source_idx, target_idx, links) {
|
|||||||
}
|
}
|
||||||
// Start DFS
|
// Start DFS
|
||||||
dfs(source_idx);
|
dfs(source_idx);
|
||||||
return Array.from(resultLinks); // Convert Set to Array for result
|
return Array.from(resultLinks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function predictFromNode(url) {
|
function predictFromNode(url) {
|
||||||
@ -61,6 +67,9 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
const depthMap = new Map();
|
||||||
|
|
||||||
|
// Avoid leaving unconnected Nodes leaving the viewport
|
||||||
|
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||||
|
|
||||||
// Sort nodes by depth first to minimize crossings
|
// Sort nodes by depth first to minimize crossings
|
||||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||||
|
|
||||||
@ -148,6 +157,11 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
|
|
||||||
|
linkText
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
@ -250,7 +264,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait before showing popup (ms)
|
||||||
var popupWaitBeforeShow = 1000;
|
var popupWaitBeforeShow = 750;
|
||||||
|
|
||||||
// Custom popover element
|
// Custom popover element
|
||||||
let popoverTimeout = null;
|
let popoverTimeout = null;
|
||||||
@ -482,7 +496,7 @@ function draw(pathway, elem) {
|
|||||||
if (n.stereo_removed) {
|
if (n.stereo_removed) {
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||||
}
|
}
|
||||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
@ -522,6 +536,21 @@ function draw(pathway, elem) {
|
|||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||||
|
for (var prop in n.proposed) {
|
||||||
|
if (n.proposed[prop].scenarioId === s.url) {
|
||||||
|
if("proposed" in n.proposed[prop]){
|
||||||
|
popupContent += "This compound is a proposed intermediate" + "<br>";
|
||||||
|
}
|
||||||
|
if("Confidence" in n.proposed[prop]){
|
||||||
|
popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
|
||||||
|
}
|
||||||
|
if("Transformation product importance" in n.proposed[prop]){
|
||||||
|
popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContent += "<br>";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,7 +563,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edge_popup(e) {
|
function edge_popup(e) {
|
||||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";
|
||||||
|
|
||||||
if (e.reaction.rules) {
|
if (e.reaction.rules) {
|
||||||
for (var rule of e.reaction.rules) {
|
for (var rule of e.reaction.rules) {
|
||||||
@ -585,7 +614,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
// Add background rectangle FIRST to enable pan/zoom on empty space
|
||||||
// This must be inserted before zoomable group so it's behind everything
|
// This must be inserted before zoomable group so it's behind everything
|
||||||
svg.insert("rect", "#zoomable")
|
const rect = svg.insert("rect", "#zoomable")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
@ -594,6 +623,30 @@ function draw(pathway, elem) {
|
|||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
.style("cursor", "grab");
|
.style("cursor", "grab");
|
||||||
|
|
||||||
|
// Click on empty SVG space (not on a node or link)
|
||||||
|
rect.on("click", function(event) {
|
||||||
|
// Only treat it as a background click if the direct target is the rect itself
|
||||||
|
// (clicks on nodes/links bubble up but originate from different elements)
|
||||||
|
const target = event.target;
|
||||||
|
const isNode = target.closest && (
|
||||||
|
target.closest("circle") !== null ||
|
||||||
|
target.closest("image") !== null ||
|
||||||
|
target.closest(".node") !== null
|
||||||
|
);
|
||||||
|
const isLink = target.closest && (
|
||||||
|
target.closest("path.link") !== null ||
|
||||||
|
target.closest("path.link_no_arrow") !== null
|
||||||
|
);
|
||||||
|
if (!isNode && !isLink) {
|
||||||
|
// console.log("Clicked outside a node or link");
|
||||||
|
// Clear all highlights
|
||||||
|
d3.selectAll("circle").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom Funktion aktivieren
|
// Zoom Funktion aktivieren
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([0.5, 5])
|
.scaleExtent([0.5, 5])
|
||||||
@ -640,18 +693,28 @@ function draw(pathway, elem) {
|
|||||||
.force("center", d3.forceCenter(width / 2, height / 4))
|
.force("center", d3.forceCenter(width / 2, height / 4))
|
||||||
.on("tick", ticked);
|
.on("tick", ticked);
|
||||||
|
|
||||||
// Kanten zeichnen
|
// Draw Edges
|
||||||
const link = zoomable.append("g")
|
const linkGroup = zoomable.append("g")
|
||||||
.selectAll("path")
|
.selectAll("g")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter().append("path")
|
.enter()
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "link-group");
|
||||||
|
|
||||||
|
const link = linkGroup
|
||||||
|
.append("path")
|
||||||
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
|
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||||
|
.attr("fill", "none")
|
||||||
|
|
||||||
// Check if target is pseudo and draw marker only if not pseudo
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
.attr("marker-end", d => {
|
.attr("marker-end", d => {
|
||||||
if (d.target.pseudo) return '';
|
if (d.target.pseudo) return '';
|
||||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
.on("click", function(event, d) {
|
.on("click", function(event, d) {
|
||||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||||
@ -682,6 +745,32 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function reaction_name(d) {
|
||||||
|
const to_pseudo = d?.to_pseudo || false;
|
||||||
|
|
||||||
|
if (to_pseudo) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "";
|
||||||
|
if ("reaction_probability" in d && d["reaction_probability"] !== null) {
|
||||||
|
suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
|
||||||
|
}
|
||||||
|
return d.plain_name + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkText = linkGroup
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "link-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("dy", -6)
|
||||||
|
.style("font-size", "4px")
|
||||||
|
.style("pointer-events", "none")
|
||||||
|
.style("display", "none")
|
||||||
|
.text(d => reaction_name(d))
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
d.el = this; // attach the DOM element to the data object
|
d.el = this; // attach the DOM element to the data object
|
||||||
@ -689,7 +778,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
pop_add(link, "Reaction", edge_popup);
|
pop_add(link, "Reaction", edge_popup);
|
||||||
|
|
||||||
// Knoten zeichnen
|
// Draw Nodes
|
||||||
const node = zoomable.append("g")
|
const node = zoomable.append("g")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
@ -707,7 +796,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
if (!wasHighlighted) {
|
if (!wasHighlighted) {
|
||||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||||
const inEdges = findPaths(d.id, 3, links);
|
const inEdges = findPaths(d.id, links);
|
||||||
const outEdges = []
|
const outEdges = []
|
||||||
// Colorize out edges green
|
// Colorize out edges green
|
||||||
for (const l of links) {
|
for (const l of links) {
|
||||||
@ -805,14 +894,14 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
function serializeSVG(svgElement) {
|
function serializeSVG(svgElement) {
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link").forEach(line => {
|
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
line.setAttribute("fill", style.fill);
|
line.setAttribute("fill", style.fill);
|
||||||
});
|
});
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
|
|||||||
@ -35,7 +35,6 @@
|
|||||||
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
|
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<img src="{% static 'images/bayer-logo.svg' %}" width="40">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not public_mode %}
|
{% if not public_mode %}
|
||||||
@ -89,23 +88,12 @@
|
|||||||
>Scenario</a
|
>Scenario</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<hr/>
|
|
||||||
<li>
|
|
||||||
<a href="{{ meta.server_url }}/group" id="scenarioLink"
|
|
||||||
>Group</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
{% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}
|
|
||||||
<img src="{% static 'images/restricted_mid.png' %}" width="200">
|
|
||||||
{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}
|
|
||||||
<img src="{% static 'images/secret_mid.png' %}" width="120">
|
|
||||||
{% endif %}
|
|
||||||
{% if not public_mode %}
|
{% if not public_mode %}
|
||||||
<a id="search-trigger" role="button" class="cursor-pointer">
|
<a id="search-trigger" role="button" class="cursor-pointer">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
x-data="{
|
x-data="{
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
reactionImageUrl: '',
|
reactionImageUrl: '',
|
||||||
pes: false,
|
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
@ -16,30 +15,20 @@
|
|||||||
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
||||||
const productsSelect = document.getElementById('add_pathway_edge_products');
|
const productsSelect = document.getElementById('add_pathway_edge_products');
|
||||||
|
|
||||||
const pesLinks = [];
|
|
||||||
|
|
||||||
const substrates = [];
|
const substrates = [];
|
||||||
for (const option of substratesSelect.selectedOptions) {
|
for (const option of substratesSelect.selectedOptions) {
|
||||||
substrates.push(option.dataset.smiles);
|
substrates.push(option.dataset.smiles);
|
||||||
if (option.dataset.pes === 'true') {
|
|
||||||
pesLinks.push(option.dataset.pes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const products = [];
|
const products = [];
|
||||||
for (const option of productsSelect.selectedOptions) {
|
for (const option of productsSelect.selectedOptions) {
|
||||||
products.push(option.dataset.smiles);
|
products.push(option.dataset.smiles);
|
||||||
if (option.dataset.pes === 'true') {
|
|
||||||
pesLinks.push(option.dataset.pes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (substrates.length > 0 && products.length > 0) {
|
if (substrates.length > 0 && products.length > 0) {
|
||||||
const reaction = substrates.join('.') + '>>' + products.join('.');
|
const reaction = substrates.join('.') + '>>' + products.join('.');
|
||||||
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
||||||
this.pes = pesLinks.length > 0;
|
|
||||||
} else {
|
} else {
|
||||||
this.pes = false;
|
|
||||||
this.reactionImageUrl = '';
|
this.reactionImageUrl = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -117,7 +106,6 @@
|
|||||||
{% for n in pathway.nodes %}
|
{% for n in pathway.nodes %}
|
||||||
<option
|
<option
|
||||||
data-smiles="{{ n.default_node_label.smiles }}"
|
data-smiles="{{ n.default_node_label.smiles }}"
|
||||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
|
||||||
value="{{ n.url }}"
|
value="{{ n.url }}"
|
||||||
>
|
>
|
||||||
{{ n.default_node_label.name|safe }}
|
{{ n.default_node_label.name|safe }}
|
||||||
@ -144,7 +132,6 @@
|
|||||||
{% for n in pathway.nodes %}
|
{% for n in pathway.nodes %}
|
||||||
<option
|
<option
|
||||||
data-smiles="{{ n.default_node_label.smiles }}"
|
data-smiles="{{ n.default_node_label.smiles }}"
|
||||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
|
||||||
value="{{ n.url }}"
|
value="{{ n.url }}"
|
||||||
>
|
>
|
||||||
{{ n.default_node_label.name|safe }}
|
{{ n.default_node_label.name|safe }}
|
||||||
@ -157,9 +144,6 @@
|
|||||||
|
|
||||||
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
||||||
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
|
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
|
||||||
<div x-show="pes">
|
|
||||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -82,9 +82,6 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if edge.edge_label.contains_pes %}
|
|
||||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reaction Description -->
|
<!-- Reaction Description -->
|
||||||
|
|||||||
@ -55,6 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if node.description and node.description != "no description" %}
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
|
<div class="collapse-content">{{ node.description }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|
||||||
{% for tpl in viz_templates %}
|
{% for tpl in viz_templates %}
|
||||||
|
|||||||
@ -21,12 +21,13 @@
|
|||||||
.link {
|
.link {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
/* marker-end: url(#arrow); */
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link_no_arrow {
|
.link_no_arrow {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node image {
|
.node image {
|
||||||
@ -177,6 +178,52 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||||
>
|
>
|
||||||
|
<li>
|
||||||
|
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="compound-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Compound Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="reaction-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Reaction Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if pathway.setting.model.app_domain %}
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
@ -495,6 +542,10 @@
|
|||||||
{{ pathway.d3_json|json_script:"pathway" }}
|
{{ pathway.d3_json|json_script:"pathway" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Global switch for compound names view
|
||||||
|
var compoundNamesViewEnabled = false;
|
||||||
|
// Gloabl switch for reaction names view
|
||||||
|
var reactionNamesViewEnabled = false;
|
||||||
// Global switch for app domain view
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
// Global switch for timeseries view
|
// Global switch for timeseries view
|
||||||
@ -530,6 +581,58 @@
|
|||||||
descContent.innerHTML = newDesc;
|
descContent.innerHTML = newDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compoundNamesBtn = document.getElementById(
|
||||||
|
"compound-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (compoundNamesBtn) {
|
||||||
|
compoundNamesBtn.addEventListener("click", function () {
|
||||||
|
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("compound-names-icon");
|
||||||
|
if (compoundNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
if (x.name) {
|
||||||
|
d3.select(x.el)
|
||||||
|
.append("text")
|
||||||
|
.text((d) => d.plain_name)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("y", -20)
|
||||||
|
.style("font-size", "4px");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
d3.select(x.el).select("text").remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const reactionNamesBtn = document.getElementById(
|
||||||
|
"reaction-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (reactionNamesBtn) {
|
||||||
|
reactionNamesBtn.addEventListener("click", function () {
|
||||||
|
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("reaction-names-icon");
|
||||||
|
if (reactionNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", null);
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", "none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// App domain toggle
|
// App domain toggle
|
||||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||||
if (appDomainBtn) {
|
if (appDomainBtn) {
|
||||||
|
|||||||
@ -79,9 +79,6 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if reaction.contains_pes %}
|
|
||||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reaction Description -->
|
<!-- Reaction Description -->
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from epdb.exceptions import InvalidSMILESException
|
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, CompoundStructure
|
from epdb.models import Compound, User, CompoundStructure
|
||||||
|
|
||||||
@ -19,6 +19,10 @@ class CompoundTest(TestCase):
|
|||||||
cls.user = User.objects.get(username="anonymous")
|
cls.user = User.objects.get(username="anonymous")
|
||||||
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
cls.INVALID_MOLFILE = "this is not a valid molfile"
|
||||||
|
|
||||||
def test_smoke(self):
|
def test_smoke(self):
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
self.package,
|
self.package,
|
||||||
@ -190,3 +194,153 @@ class CompoundTest(TestCase):
|
|||||||
|
|
||||||
c1.set_default_structure(c2)
|
c1.set_default_structure(c2)
|
||||||
self.assertNotEqual(default_structure, c2)
|
self.assertNotEqual(default_structure, c2)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Compound",
|
||||||
|
description="Created from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="Structure from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
# The SMILES must have been derived from the molfile, not the placeholder
|
||||||
|
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
|
||||||
|
self.assertIsNotNone(cs.smiles)
|
||||||
|
self.assertTrue(len(cs.smiles) > 0)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile_stores_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
|
||||||
|
name="Molfile Store Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = c.default_structure
|
||||||
|
|
||||||
|
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
|
||||||
|
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
|
||||||
|
# on the existing structure
|
||||||
|
_ = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Structure with Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch fresh from DB to confirm persistence
|
||||||
|
cs_db = CompoundStructure.objects.get(pk=cs.pk)
|
||||||
|
self.assertIsNotNone(cs_db.molfile)
|
||||||
|
self.assertNotEqual(cs_db.molfile.strip(), "")
|
||||||
|
|
||||||
|
def test_create_structure_from_invalid_molfile_raises(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Invalid Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidMolfileException):
|
||||||
|
CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.INVALID_MOLFILE,
|
||||||
|
name="Bad Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and smiles are supplied, molfile must win."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Precedence Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="C", # intentionally wrong / different SMILES
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Wins",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# SMILES should be derived from molfile, not the supplied "C"
|
||||||
|
self.assertNotEqual(cs.smiles, "C")
|
||||||
|
|
||||||
|
def test_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Empty Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=" ", # whitespace only – should be ignored
|
||||||
|
name="Fallback to SMILES",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_none_molfile_falls_back_to_smiles(self):
|
||||||
|
"""None as molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="None Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=None,
|
||||||
|
name="Fallback None Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_molfile_deduplication(self):
|
||||||
|
"""Creating a structure twice from the same molfile should return the existing object."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Molfile Dedup Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs1 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs2 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs1.pk, cs2.pk)
|
||||||
|
|||||||
@ -11,6 +11,9 @@ from epdb.models import Compound, Scenario, ExternalDatabase
|
|||||||
class CompoundViewTest(TestCase):
|
class CompoundViewTest(TestCase):
|
||||||
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(CompoundViewTest, cls).setUpClass()
|
super(CompoundViewTest, cls).setUpClass()
|
||||||
@ -396,3 +399,108 @@ class CompoundViewTest(TestCase):
|
|||||||
|
|
||||||
c = Compound.objects.get(url=compound_url)
|
c = Compound.objects.get(url=compound_url)
|
||||||
self.assertEqual(len(c.aliases), 0)
|
self.assertEqual(len(c.aliases), 0)
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile(self):
|
||||||
|
"""POSTing a valid molfile without a SMILES should create a compound successfully."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Created from molfile",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
self.assertEqual(c.name, "4-Nitrobenzoic acid")
|
||||||
|
self.assertEqual(c.description, "Created from molfile")
|
||||||
|
# SMILES should have been extracted from molfile, so it must be non-empty
|
||||||
|
self.assertIsNotNone(c.default_structure.smiles)
|
||||||
|
self.assertNotEqual(c.default_structure.smiles.strip(), "")
|
||||||
|
|
||||||
|
def test_create_compound_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and SMILES are submitted, the molfile should win."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile precedence test",
|
||||||
|
"compound-smiles": "C", # intentionally wrong/different
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
# The resulting SMILES must NOT be the dummy "C" supplied via compound-smiles
|
||||||
|
self.assertNotEqual(c.default_structure.smiles, "C")
|
||||||
|
|
||||||
|
def test_create_compound_via_invalid_molfile_returns_error(self):
|
||||||
|
"""POSTing an invalid molfile should not create a compound and should return an error response."""
|
||||||
|
initial_count = self.user1_default_package.compounds.count()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Bad Molfile Compound",
|
||||||
|
"compound-description": "Should fail",
|
||||||
|
"compound-molfile": "this is not a molfile at all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# The view should signal an error (non-2xx or a redirect to an error page, not 302 to a new compound)
|
||||||
|
self.assertNotEqual(response.status_code, 302)
|
||||||
|
# No new compound should have been created
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), initial_count)
|
||||||
|
|
||||||
|
def test_create_compound_via_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""Submitting an empty molfile with a valid SMILES should fall back to SMILES."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Fallback SMILES Compound",
|
||||||
|
"compound-description": "Empty molfile fallback",
|
||||||
|
"compound-smiles": "C(CCl)Cl",
|
||||||
|
"compound-molfile": " ", # whitespace only
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile_deduplication(self):
|
||||||
|
"""Submitting the same molfile twice should return the existing compound."""
|
||||||
|
response1 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response1.status_code, 302)
|
||||||
|
compound_url_1 = response1.url
|
||||||
|
|
||||||
|
response2 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response2.status_code, 302)
|
||||||
|
self.assertEqual(response2.url, compound_url_1)
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), 1)
|
||||||
|
|||||||
@ -72,8 +72,10 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
def test_import_package(self):
|
def test_import_package(self):
|
||||||
file = SimpleUploadedFile(
|
file = SimpleUploadedFile(
|
||||||
"Fixture_Package.json",
|
"EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json",
|
||||||
open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(),
|
open(
|
||||||
|
s.FIXTURE_DIRS[0] / "EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json", "rb"
|
||||||
|
).read(),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,10 +88,11 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
p = Package.objects.get(url=package_url)
|
p = Package.objects.get(url=package_url)
|
||||||
|
|
||||||
self.assertEqual(p.pathways.count(), 22)
|
self.assertEqual(p.pathways.count(), 219)
|
||||||
self.assertEqual(p.rules.count(), 45)
|
self.assertEqual(p.rules.count(), 498)
|
||||||
self.assertEqual(p.compounds.count(), 223)
|
self.assertEqual(p.compounds.count(), 1399)
|
||||||
self.assertEqual(p.reactions.count(), 212)
|
self.assertEqual(p.reactions.count(), 1480)
|
||||||
|
self.assertEqual(p.scenarios.count(), 1914)
|
||||||
|
|
||||||
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
||||||
self.assertEqual(upp.permission, Permission.ALL[0])
|
self.assertEqual(upp.permission, Permission.ALL[0])
|
||||||
|
|||||||
@ -9,32 +9,24 @@ from datetime import datetime
|
|||||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.db import transaction
|
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import HttpUrl
|
from pydantic import HttpUrl, ValidationError
|
||||||
|
|
||||||
|
from epdb.exceptions import PackageImportException
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
EnviFormer,
|
|
||||||
EPModel,
|
|
||||||
ExternalDatabase,
|
|
||||||
ExternalIdentifier,
|
|
||||||
License,
|
License,
|
||||||
MLRelativeReasoning,
|
|
||||||
Node,
|
Node,
|
||||||
ParallelRule,
|
ParallelRule,
|
||||||
Pathway,
|
Pathway,
|
||||||
Reaction,
|
Reaction,
|
||||||
Rule,
|
Rule,
|
||||||
RuleBasedRelativeReasoning,
|
|
||||||
Scenario,
|
Scenario,
|
||||||
Setting,
|
Setting,
|
||||||
SimpleAmbitRule,
|
SimpleAmbitRule,
|
||||||
SimpleRDKitRule,
|
|
||||||
SimpleRule,
|
|
||||||
)
|
)
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
@ -88,6 +80,9 @@ class RefPathwayExportSchema(RefExportSchema): ...
|
|||||||
class RefScenarioExportSchema(RefExportSchema): ...
|
class RefScenarioExportSchema(RefExportSchema): ...
|
||||||
|
|
||||||
|
|
||||||
|
class RefEnzymeExportSchema(RefExportSchema): ...
|
||||||
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# Compound #
|
# Compound #
|
||||||
############
|
############
|
||||||
@ -128,7 +123,28 @@ class ReactionExportSchema(RefReactionExportSchema):
|
|||||||
#########
|
#########
|
||||||
# Rules #
|
# Rules #
|
||||||
#########
|
#########
|
||||||
class RuleExportSchema(RefRuleExportSchema):
|
class EnzymeExportSchema(RefEnzymeExportSchema):
|
||||||
|
ec_number: str
|
||||||
|
classification_level: int
|
||||||
|
linking_method: str
|
||||||
|
reaction_evidence: List[RefReactionExportSchema]
|
||||||
|
edge_evidence: List[RefEdgeExportSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
||||||
|
enzymes: List[EnzymeExportSchema] | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_enzymes(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
res = []
|
||||||
|
for e in obj.get("enzymes", []):
|
||||||
|
res.append(EnzymeExportSchema.model_validate(e))
|
||||||
|
return res
|
||||||
|
return obj.enzymelink_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
class RuleExportSchema(EnzymeRuleExportSchema):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
aliases: List[str]
|
aliases: List[str]
|
||||||
@ -138,7 +154,7 @@ class RuleExportSchema(RefRuleExportSchema):
|
|||||||
scenarios: List[RefScenarioExportSchema]
|
scenarios: List[RefScenarioExportSchema]
|
||||||
|
|
||||||
|
|
||||||
class ParallelRuleExportSchema(RefRuleExportSchema):
|
class ParallelRuleExportSchema(EnzymeRuleExportSchema):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
aliases: List[str]
|
aliases: List[str]
|
||||||
@ -210,7 +226,7 @@ class PackageExportSchema(Schema):
|
|||||||
description: str
|
description: str
|
||||||
uuid: str
|
uuid: str
|
||||||
reviewed: bool
|
reviewed: bool
|
||||||
license: LicenseExportSchema | None
|
license: LicenseExportSchema | None = None
|
||||||
compounds: List[CompoundExportSchema]
|
compounds: List[CompoundExportSchema]
|
||||||
reactions: List[ReactionExportSchema]
|
reactions: List[ReactionExportSchema]
|
||||||
simple_rules: List[RuleExportSchema]
|
simple_rules: List[RuleExportSchema]
|
||||||
@ -224,6 +240,15 @@ class PackageExportSchema(Schema):
|
|||||||
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_compounds(obj):
|
||||||
|
res = []
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for c in obj.get("compounds", []):
|
||||||
|
res.append(CompoundExportSchema.model_validate(c))
|
||||||
|
return res
|
||||||
|
return obj.compounds.all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_simple_rules(obj):
|
def resolve_simple_rules(obj):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
@ -277,51 +302,239 @@ class PackageExporter:
|
|||||||
|
|
||||||
|
|
||||||
class PackageImporter:
|
class PackageImporter:
|
||||||
"""
|
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
||||||
Imports package data from JSON export.
|
|
||||||
Handles object creation, relationship mapping, and dependency resolution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
package: Dict[str, Any],
|
|
||||||
preserve_uuids: bool = False,
|
|
||||||
add_import_timestamp=True,
|
|
||||||
trust_reviewed=False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initialize the importer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
preserve_uuids: If True, preserve original UUIDs. If False, generate new ones.
|
|
||||||
"""
|
|
||||||
self.preserve_uuids = preserve_uuids
|
self.preserve_uuids = preserve_uuids
|
||||||
self.add_import_timestamp = add_import_timestamp
|
|
||||||
self.trust_reviewed = trust_reviewed
|
|
||||||
self.uuid_mapping = {}
|
|
||||||
self.object_cache = {}
|
|
||||||
self._raw_package = package
|
self._raw_package = package
|
||||||
|
self._cache = {}
|
||||||
def _get_or_generate_uuid(self, original_uuid: str) -> str:
|
|
||||||
"""Get mapped UUID or generate new one if not preserving UUIDs."""
|
|
||||||
if self.preserve_uuids:
|
|
||||||
return original_uuid
|
|
||||||
|
|
||||||
if original_uuid not in self.uuid_mapping:
|
|
||||||
self.uuid_mapping[original_uuid] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
return self.uuid_mapping[original_uuid]
|
|
||||||
|
|
||||||
def _cache_object(self, model_name: str, uuid_str: str, obj):
|
|
||||||
"""Cache a created object for later reference."""
|
|
||||||
self.object_cache[(model_name, uuid_str)] = obj
|
|
||||||
|
|
||||||
def _get_cached_object(self, model_name: str, uuid_str: str):
|
|
||||||
"""Get a cached object by model name and UUID."""
|
|
||||||
return self.object_cache.get((model_name, uuid_str))
|
|
||||||
|
|
||||||
def do_import(self) -> Package:
|
def do_import(self) -> Package:
|
||||||
return self._import_package_from_json(self._raw_package)
|
return self._import_package_from_json()
|
||||||
|
|
||||||
|
def _import_compounds(
|
||||||
|
self,
|
||||||
|
package: Package,
|
||||||
|
compounds: List[CompoundExportSchema],
|
||||||
|
):
|
||||||
|
for c in compounds:
|
||||||
|
new_c = Compound()
|
||||||
|
new_c.uuid = str(uuid.uuid4()) if not self.preserve_uuids else c.uuid
|
||||||
|
new_c.package = package
|
||||||
|
new_c.name = c.name
|
||||||
|
new_c.description = c.description
|
||||||
|
new_c.aliases = c.aliases
|
||||||
|
new_c.save()
|
||||||
|
|
||||||
|
self._cache[c.uuid] = new_c
|
||||||
|
|
||||||
|
for cs in c.structures:
|
||||||
|
new_cs = CompoundStructure()
|
||||||
|
new_cs.uuid = str(uuid.uuid4()) if not self.preserve_uuids else cs.uuid
|
||||||
|
new_cs.compound = new_c
|
||||||
|
new_cs.name = cs.name
|
||||||
|
new_cs.description = cs.description
|
||||||
|
new_cs.aliases = cs.aliases
|
||||||
|
new_cs.smiles = cs.smiles
|
||||||
|
new_cs.molfile = cs.molfile
|
||||||
|
|
||||||
|
new_cs.normalized_structure = cs.normalized_structure
|
||||||
|
new_cs.save()
|
||||||
|
|
||||||
|
self._cache[cs.uuid] = new_cs
|
||||||
|
|
||||||
|
# Now we can assigne the default_structure to the compound
|
||||||
|
new_c.default_structure = self._cache[c.default_structure.uuid]
|
||||||
|
new_c.save()
|
||||||
|
|
||||||
|
def _import_simple_rules(self, package: Package, rules: List[RuleExportSchema]):
|
||||||
|
for r in rules:
|
||||||
|
new_r = SimpleAmbitRule()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.smirks = r.smirks
|
||||||
|
new_r.reactant_filter_smarts = r.reactant_filter_smarts
|
||||||
|
new_r.product_filter_smarts = r.product_filter_smarts
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_composite_rules(self, package: Package, rules: List[ParallelRuleExportSchema]):
|
||||||
|
for r in rules:
|
||||||
|
new_r = ParallelRule()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
for sr in r.simple_rules:
|
||||||
|
new_r.simple_rules.add(self._cache[sr.uuid])
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_reactions(
|
||||||
|
self,
|
||||||
|
package: Package,
|
||||||
|
reactions: List[ReactionExportSchema],
|
||||||
|
):
|
||||||
|
for r in reactions:
|
||||||
|
new_r = Reaction()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.multi_step = r.multi_step
|
||||||
|
new_r.medline_references = r.medline_references
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
for educt in r.educts:
|
||||||
|
new_r.educts.add(self._cache[educt.uuid])
|
||||||
|
|
||||||
|
for product in r.products:
|
||||||
|
new_r.products.add(self._cache[product.uuid])
|
||||||
|
|
||||||
|
for rule in r.rules:
|
||||||
|
new_r.rules.add(self._cache[rule.uuid])
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_pathways(self, package: Package, pathways: List[PathwayExportSchema]):
|
||||||
|
for pw in pathways:
|
||||||
|
new_pw = Pathway()
|
||||||
|
new_pw.uuid = str(uuid.uuid4()) if not self.preserve_uuids else pw.uuid
|
||||||
|
new_pw.package = package
|
||||||
|
new_pw.name = pw.name
|
||||||
|
new_pw.description = pw.description
|
||||||
|
new_pw.aliases = pw.aliases
|
||||||
|
new_pw.predicted = pw.predicted
|
||||||
|
new_pw.save()
|
||||||
|
|
||||||
|
self._cache[pw.uuid] = new_pw
|
||||||
|
|
||||||
|
for n in pw.nodes:
|
||||||
|
new_n = Node()
|
||||||
|
new_n.uuid = str(uuid.uuid4()) if not self.preserve_uuids else n.uuid
|
||||||
|
new_n.pathway = new_pw
|
||||||
|
new_n.name = n.name
|
||||||
|
new_n.description = n.description
|
||||||
|
new_n.aliases = n.aliases
|
||||||
|
new_n.default_node_label = self._cache[n.default_node_label.uuid]
|
||||||
|
new_n.depth = n.depth
|
||||||
|
new_n.stereo_removed = n.stereo_removed
|
||||||
|
new_n.save()
|
||||||
|
|
||||||
|
for nl in n.node_labels:
|
||||||
|
new_n.node_labels.add(self._cache[nl.uuid])
|
||||||
|
|
||||||
|
self._cache[n.uuid] = new_n
|
||||||
|
|
||||||
|
for e in pw.edges:
|
||||||
|
new_e = Edge()
|
||||||
|
new_e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else e.uuid
|
||||||
|
new_e.pathway = new_pw
|
||||||
|
new_e.name = e.name
|
||||||
|
new_e.description = e.description
|
||||||
|
new_e.aliases = e.aliases
|
||||||
|
new_e.edge_label = self._cache[e.edge_label.uuid]
|
||||||
|
new_e.save()
|
||||||
|
|
||||||
|
for sn in e.start_nodes:
|
||||||
|
new_e.start_nodes.add(self._cache[sn.uuid])
|
||||||
|
|
||||||
|
for en in e.end_nodes:
|
||||||
|
new_e.end_nodes.add(self._cache[en.uuid])
|
||||||
|
|
||||||
|
self._cache[e.uuid] = new_e
|
||||||
|
|
||||||
|
def _import_scenarios(self, package: Package, scenarios: List[ScenarioExportSchema]):
|
||||||
|
for scen in scenarios:
|
||||||
|
new_s = Scenario()
|
||||||
|
new_s.uuid = str(uuid.uuid4()) if not self.preserve_uuids else scen.uuid
|
||||||
|
new_s.package = package
|
||||||
|
new_s.name = scen.name
|
||||||
|
new_s.description = scen.description
|
||||||
|
new_s.scenario_date = scen.scenario_date
|
||||||
|
new_s.scenario_type = scen.scenario_type
|
||||||
|
new_s.save()
|
||||||
|
|
||||||
|
self._cache[scen.uuid] = new_s
|
||||||
|
|
||||||
|
def _import_additional_information(
|
||||||
|
self, package: Package, additional_information: List[AdditionalInformationExportSchema]
|
||||||
|
):
|
||||||
|
for ai in additional_information:
|
||||||
|
new_ai = AdditionalInformation()
|
||||||
|
new_ai.uuid = str(uuid.uuid4()) if not self.preserve_uuids else ai.uuid
|
||||||
|
new_ai.package = package
|
||||||
|
new_ai.type = ai.type
|
||||||
|
new_ai.data = ai.data
|
||||||
|
|
||||||
|
if ai.scenario:
|
||||||
|
new_ai.scenario = self._cache[ai.scenario.uuid]
|
||||||
|
|
||||||
|
if ai.attach_object:
|
||||||
|
new_ai.content_object = self._cache[ai.attach_object.uuid]
|
||||||
|
|
||||||
|
new_ai.save()
|
||||||
|
|
||||||
|
self._cache[ai.uuid] = new_ai
|
||||||
|
|
||||||
|
def _link_scenarios_after_import(self, data: PackageExportSchema):
|
||||||
|
collections = [
|
||||||
|
data.compounds,
|
||||||
|
data.simple_rules,
|
||||||
|
data.composite_rules,
|
||||||
|
data.reactions,
|
||||||
|
data.pathways,
|
||||||
|
[n for pw in data.pathways for n in pw.nodes],
|
||||||
|
[e for pw in data.pathways for e in pw.edges],
|
||||||
|
]
|
||||||
|
|
||||||
|
for coll in collections:
|
||||||
|
for elem in coll:
|
||||||
|
elem_obj = self._cache[elem.uuid]
|
||||||
|
for scen in elem.scenarios:
|
||||||
|
elem_obj.scenarios.add(self._cache[scen.uuid])
|
||||||
|
|
||||||
|
def _import_package_from_json(
|
||||||
|
self,
|
||||||
|
) -> Package:
|
||||||
|
try:
|
||||||
|
parsed = PackageExportSchema.model_validate(self._raw_package)
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(f"Error validating package data: {e}")
|
||||||
|
raise PackageImportException("Deserialization failed")
|
||||||
|
|
||||||
|
package_uuid = str(uuid.uuid4())
|
||||||
|
if self.preserve_uuids:
|
||||||
|
package_uuid = parsed.uuid
|
||||||
|
|
||||||
|
package_license = License.objects.get(link=parsed.license.link) if parsed.license else None
|
||||||
|
|
||||||
|
package = Package()
|
||||||
|
package.uuid = package_uuid
|
||||||
|
package.name = f"{parsed.name} - Imported at {datetime.now()}"
|
||||||
|
package.description = parsed.description
|
||||||
|
package.reviewed = False
|
||||||
|
package.license = package_license
|
||||||
|
|
||||||
|
package.save()
|
||||||
|
|
||||||
|
# Import Elements
|
||||||
|
self._import_compounds(package, parsed.compounds)
|
||||||
|
self._import_simple_rules(package, parsed.simple_rules)
|
||||||
|
self._import_composite_rules(package, parsed.composite_rules)
|
||||||
|
self._import_reactions(package, parsed.reactions)
|
||||||
|
self._import_pathways(package, parsed.pathways)
|
||||||
|
self._import_scenarios(package, parsed.scenarios)
|
||||||
|
self._import_additional_information(package, parsed.additional_information)
|
||||||
|
self._link_scenarios_after_import(parsed)
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
|
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
|
||||||
@ -339,510 +552,6 @@ class PackageImporter:
|
|||||||
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
||||||
return hmac.compare_digest(signature, expected)
|
return hmac.compare_digest(signature, expected)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def _import_package_from_json(self, package_data: Dict[str, Any]) -> Package:
|
|
||||||
"""
|
|
||||||
Import a complete package from JSON data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package_data: Dictionary containing the package export data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Package instance
|
|
||||||
"""
|
|
||||||
print(f"Starting import of package: {package_data['name']}")
|
|
||||||
|
|
||||||
# Create the main package
|
|
||||||
package = self._create_package(package_data)
|
|
||||||
|
|
||||||
# Import in dependency order
|
|
||||||
self._import_compounds(package, package_data.get("compounds", []))
|
|
||||||
self._import_structures(package, package_data.get("structures", []))
|
|
||||||
self._import_rules(package, package_data.get("rules", {}))
|
|
||||||
self._import_reactions(package, package_data.get("reactions", []))
|
|
||||||
self._import_pathways(package, package_data.get("pathways", []))
|
|
||||||
self._import_nodes(package, package_data.get("nodes", []))
|
|
||||||
self._import_edges(package, package_data.get("edges", []))
|
|
||||||
self._import_scenarios(package, package_data.get("scenarios", []))
|
|
||||||
|
|
||||||
if package_data.get("models"):
|
|
||||||
self._import_models(package, package_data["models"])
|
|
||||||
|
|
||||||
# Set default structures for compounds (after all structures are created)
|
|
||||||
self._set_default_structures(package_data.get("compounds", []))
|
|
||||||
|
|
||||||
print(f"Package import completed: {package.name}")
|
|
||||||
return package
|
|
||||||
|
|
||||||
def _create_package(self, package_data: Dict[str, Any]) -> Package:
|
|
||||||
"""Create the main package object."""
|
|
||||||
package_uuid = self._get_or_generate_uuid(package_data["uuid"])
|
|
||||||
|
|
||||||
# Handle license
|
|
||||||
license_obj = None
|
|
||||||
if package_data.get("license"):
|
|
||||||
license_data = package_data["license"]
|
|
||||||
license_obj, _ = License.objects.get_or_create(
|
|
||||||
name=license_data["name"],
|
|
||||||
defaults={
|
|
||||||
"cc_string": license_data.get("cc_string", ""),
|
|
||||||
"link": license_data.get("link", ""),
|
|
||||||
"image_link": license_data.get("image_link", ""),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
new_name = package_data.get("name")
|
|
||||||
if self.add_import_timestamp:
|
|
||||||
new_name = f"{new_name} - Imported at {datetime.now()}"
|
|
||||||
|
|
||||||
new_reviewed = False
|
|
||||||
if self.trust_reviewed:
|
|
||||||
new_reviewed = package_data.get("reviewed", False)
|
|
||||||
|
|
||||||
package = Package.objects.create(
|
|
||||||
uuid=package_uuid,
|
|
||||||
name=new_name,
|
|
||||||
description=package_data["description"],
|
|
||||||
kv=package_data.get("kv", {}),
|
|
||||||
reviewed=new_reviewed,
|
|
||||||
license=license_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cache_object("Package", package_data["uuid"], package)
|
|
||||||
print(f"Created package: {package.name}")
|
|
||||||
return package
|
|
||||||
|
|
||||||
def _import_compounds(self, package: Package, compounds_data: List[Dict[str, Any]]):
|
|
||||||
"""Import compounds."""
|
|
||||||
print(f"Importing {len(compounds_data)} compounds...")
|
|
||||||
|
|
||||||
for compound_data in compounds_data:
|
|
||||||
compound_uuid = self._get_or_generate_uuid(compound_data["uuid"])
|
|
||||||
|
|
||||||
compound = Compound.objects.create(
|
|
||||||
uuid=compound_uuid,
|
|
||||||
package=package,
|
|
||||||
name=compound_data["name"],
|
|
||||||
description=compound_data["description"],
|
|
||||||
kv=compound_data.get("kv", {}),
|
|
||||||
# default_structure will be set later
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if compound_data.get("aliases"):
|
|
||||||
compound.aliases = compound_data["aliases"]
|
|
||||||
compound.save()
|
|
||||||
|
|
||||||
self._cache_object("Compound", compound_data["uuid"], compound)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
compound, compound_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_structures(self, package: Package, structures_data: List[Dict[str, Any]]):
|
|
||||||
"""Import compound structures."""
|
|
||||||
print(f"Importing {len(structures_data)} compound structures...")
|
|
||||||
|
|
||||||
for structure_data in structures_data:
|
|
||||||
structure_uuid = self._get_or_generate_uuid(structure_data["uuid"])
|
|
||||||
compound_uuid = structure_data["compound"]["uuid"]
|
|
||||||
compound = self._get_cached_object("Compound", compound_uuid)
|
|
||||||
|
|
||||||
if not compound:
|
|
||||||
print(f"Warning: Compound with UUID {compound_uuid} not found for structure")
|
|
||||||
continue
|
|
||||||
|
|
||||||
structure = CompoundStructure.objects.create(
|
|
||||||
uuid=structure_uuid,
|
|
||||||
compound=compound,
|
|
||||||
name=structure_data["name"],
|
|
||||||
description=structure_data["description"],
|
|
||||||
kv=structure_data.get("kv", {}),
|
|
||||||
smiles=structure_data["smiles"],
|
|
||||||
canonical_smiles=structure_data["canonical_smiles"],
|
|
||||||
inchikey=structure_data["inchikey"],
|
|
||||||
normalized_structure=structure_data.get("normalized_structure", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if structure_data.get("aliases"):
|
|
||||||
structure.aliases = structure_data["aliases"]
|
|
||||||
structure.save()
|
|
||||||
|
|
||||||
self._cache_object("CompoundStructure", structure_data["uuid"], structure)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
structure, structure_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_rules(self, package: Package, rules_data: Dict[str, Any]):
|
|
||||||
"""Import all types of rules."""
|
|
||||||
print("Importing rules...")
|
|
||||||
|
|
||||||
# Import simple rules first
|
|
||||||
simple_rules_data = rules_data.get("simple_rules", [])
|
|
||||||
print(f"Importing {len(simple_rules_data)} simple rules...")
|
|
||||||
|
|
||||||
for rule_data in simple_rules_data:
|
|
||||||
self._create_simple_rule(package, rule_data)
|
|
||||||
|
|
||||||
# Import parallel rules
|
|
||||||
parallel_rules_data = rules_data.get("parallel_rules", [])
|
|
||||||
print(f"Importing {len(parallel_rules_data)} parallel rules...")
|
|
||||||
|
|
||||||
for rule_data in parallel_rules_data:
|
|
||||||
self._create_parallel_rule(package, rule_data)
|
|
||||||
|
|
||||||
def _create_simple_rule(self, package: Package, rule_data: Dict[str, Any]):
|
|
||||||
"""Create a simple rule (SimpleAmbitRule or SimpleRDKitRule)."""
|
|
||||||
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
|
|
||||||
rule_type = rule_data.get("rule_type", "SimpleRule")
|
|
||||||
|
|
||||||
common_fields = {
|
|
||||||
"uuid": rule_uuid,
|
|
||||||
"package": package,
|
|
||||||
"name": rule_data["name"],
|
|
||||||
"description": rule_data["description"],
|
|
||||||
"kv": rule_data.get("kv", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule_type == "SimpleAmbitRule":
|
|
||||||
rule = SimpleAmbitRule.objects.create(
|
|
||||||
**common_fields,
|
|
||||||
smirks=rule_data.get("smirks", ""),
|
|
||||||
reactant_filter_smarts=rule_data.get("reactant_filter_smarts", ""),
|
|
||||||
product_filter_smarts=rule_data.get("product_filter_smarts", ""),
|
|
||||||
)
|
|
||||||
elif rule_type == "SimpleRDKitRule":
|
|
||||||
rule = SimpleRDKitRule.objects.create(
|
|
||||||
**common_fields, reaction_smarts=rule_data.get("reaction_smarts", "")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
rule = SimpleRule.objects.create(**common_fields)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if rule_data.get("aliases"):
|
|
||||||
rule.aliases = rule_data["aliases"]
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
self._cache_object("SimpleRule", rule_data["uuid"], rule)
|
|
||||||
return rule
|
|
||||||
|
|
||||||
def _create_parallel_rule(self, package: Package, rule_data: Dict[str, Any]):
|
|
||||||
"""Create a parallel rule."""
|
|
||||||
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
|
|
||||||
|
|
||||||
rule = ParallelRule.objects.create(
|
|
||||||
uuid=rule_uuid,
|
|
||||||
package=package,
|
|
||||||
name=rule_data["name"],
|
|
||||||
description=rule_data["description"],
|
|
||||||
kv=rule_data.get("kv", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if rule_data.get("aliases"):
|
|
||||||
rule.aliases = rule_data["aliases"]
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
# Add simple rules
|
|
||||||
for simple_rule_ref in rule_data.get("simple_rules", []):
|
|
||||||
simple_rule = self._get_cached_object("SimpleRule", simple_rule_ref["uuid"])
|
|
||||||
if simple_rule:
|
|
||||||
rule.simple_rules.add(simple_rule)
|
|
||||||
|
|
||||||
self._cache_object("ParallelRule", rule_data["uuid"], rule)
|
|
||||||
return rule
|
|
||||||
|
|
||||||
def _import_reactions(self, package: Package, reactions_data: List[Dict[str, Any]]):
|
|
||||||
"""Import reactions."""
|
|
||||||
print(f"Importing {len(reactions_data)} reactions...")
|
|
||||||
|
|
||||||
for reaction_data in reactions_data:
|
|
||||||
reaction_uuid = self._get_or_generate_uuid(reaction_data["uuid"])
|
|
||||||
|
|
||||||
reaction = Reaction.objects.create(
|
|
||||||
uuid=reaction_uuid,
|
|
||||||
package=package,
|
|
||||||
name=reaction_data["name"],
|
|
||||||
description=reaction_data["description"],
|
|
||||||
kv=reaction_data.get("kv", {}),
|
|
||||||
multi_step=reaction_data.get("multi_step", False),
|
|
||||||
medline_references=reaction_data.get("medline_references", []),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if reaction_data.get("aliases"):
|
|
||||||
reaction.aliases = reaction_data["aliases"]
|
|
||||||
reaction.save()
|
|
||||||
|
|
||||||
# Add educts and products
|
|
||||||
for educt_ref in reaction_data.get("educts", []):
|
|
||||||
compound = self._get_cached_object("CompoundStructure", educt_ref["uuid"])
|
|
||||||
if compound:
|
|
||||||
reaction.educts.add(compound)
|
|
||||||
|
|
||||||
for product_ref in reaction_data.get("products", []):
|
|
||||||
compound = self._get_cached_object("CompoundStructure", product_ref["uuid"])
|
|
||||||
if compound:
|
|
||||||
reaction.products.add(compound)
|
|
||||||
|
|
||||||
# Add rules
|
|
||||||
for rule_ref in reaction_data.get("rules", []):
|
|
||||||
# Try to find rule in different caches
|
|
||||||
rule = self._get_cached_object(
|
|
||||||
"SimpleRule", rule_ref["uuid"]
|
|
||||||
) or self._get_cached_object("ParallelRule", rule_ref["uuid"])
|
|
||||||
if rule:
|
|
||||||
reaction.rules.add(rule)
|
|
||||||
|
|
||||||
self._cache_object("Reaction", reaction_data["uuid"], reaction)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
reaction, reaction_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_pathways(self, package: Package, pathways_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathways."""
|
|
||||||
print(f"Importing {len(pathways_data)} pathways...")
|
|
||||||
|
|
||||||
for pathway_data in pathways_data:
|
|
||||||
pathway_uuid = self._get_or_generate_uuid(pathway_data["uuid"])
|
|
||||||
|
|
||||||
pathway = Pathway.objects.create(
|
|
||||||
uuid=pathway_uuid,
|
|
||||||
package=package,
|
|
||||||
name=pathway_data["name"],
|
|
||||||
description=pathway_data["description"],
|
|
||||||
kv=pathway_data.get("kv", {}),
|
|
||||||
# setting will be handled separately if needed
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if pathway_data.get("aliases"):
|
|
||||||
pathway.aliases = pathway_data["aliases"]
|
|
||||||
pathway.save()
|
|
||||||
|
|
||||||
self._cache_object("Pathway", pathway_data["uuid"], pathway)
|
|
||||||
|
|
||||||
def _import_nodes(self, package: Package, nodes_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathway nodes."""
|
|
||||||
print(f"Importing {len(nodes_data)} nodes...")
|
|
||||||
|
|
||||||
for node_data in nodes_data:
|
|
||||||
node_uuid = self._get_or_generate_uuid(node_data["uuid"])
|
|
||||||
pathway_uuid = node_data["pathway"]["uuid"]
|
|
||||||
pathway = self._get_cached_object("Pathway", pathway_uuid)
|
|
||||||
|
|
||||||
if not pathway:
|
|
||||||
print(f"Warning: Pathway with UUID {pathway_uuid} not found for node")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# For now, we'll set default_node_label to None and handle it later
|
|
||||||
# as it requires compound structures to be fully imported
|
|
||||||
node = Node.objects.create(
|
|
||||||
uuid=node_uuid,
|
|
||||||
pathway=pathway,
|
|
||||||
name=node_data["name"],
|
|
||||||
description=node_data["description"],
|
|
||||||
kv=node_data.get("kv", {}),
|
|
||||||
depth=node_data.get("depth", 0),
|
|
||||||
default_node_label=self._get_cached_object(
|
|
||||||
"CompoundStructure", node_data["default_node_label"]["uuid"]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if node_data.get("aliases"):
|
|
||||||
node.aliases = node_data["aliases"]
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
self._cache_object("Node", node_data["uuid"], node)
|
|
||||||
# Store node_data for later processing of relationships
|
|
||||||
node._import_data = node_data
|
|
||||||
|
|
||||||
def _import_edges(self, package: Package, edges_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathway edges."""
|
|
||||||
print(f"Importing {len(edges_data)} edges...")
|
|
||||||
|
|
||||||
for edge_data in edges_data:
|
|
||||||
edge_uuid = self._get_or_generate_uuid(edge_data["uuid"])
|
|
||||||
pathway_uuid = edge_data["pathway"]["uuid"]
|
|
||||||
pathway = self._get_cached_object("Pathway", pathway_uuid)
|
|
||||||
|
|
||||||
if not pathway:
|
|
||||||
print(f"Warning: Pathway with UUID {pathway_uuid} not found for edge")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# For now, we'll set edge_label to None and handle it later
|
|
||||||
edge = Edge.objects.create(
|
|
||||||
uuid=edge_uuid,
|
|
||||||
pathway=pathway,
|
|
||||||
name=edge_data["name"],
|
|
||||||
description=edge_data["description"],
|
|
||||||
kv=edge_data.get("kv", {}),
|
|
||||||
edge_label=self._get_cached_object("Reaction", edge_data["edge_label"]["uuid"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if edge_data.get("aliases"):
|
|
||||||
edge.aliases = edge_data["aliases"]
|
|
||||||
edge.save()
|
|
||||||
|
|
||||||
# Add start and end nodes
|
|
||||||
for start_node_ref in edge_data.get("start_nodes", []):
|
|
||||||
node = self._get_cached_object("Node", start_node_ref["uuid"])
|
|
||||||
if node:
|
|
||||||
edge.start_nodes.add(node)
|
|
||||||
|
|
||||||
for end_node_ref in edge_data.get("end_nodes", []):
|
|
||||||
node = self._get_cached_object("Node", end_node_ref["uuid"])
|
|
||||||
if node:
|
|
||||||
edge.end_nodes.add(node)
|
|
||||||
|
|
||||||
self._cache_object("Edge", edge_data["uuid"], edge)
|
|
||||||
|
|
||||||
def _import_scenarios(self, package: Package, scenarios_data: List[Dict[str, Any]]):
|
|
||||||
"""Import scenarios."""
|
|
||||||
print(f"Importing {len(scenarios_data)} scenarios...")
|
|
||||||
|
|
||||||
# First pass: create scenarios without parent relationships
|
|
||||||
for scenario_data in scenarios_data:
|
|
||||||
scenario_uuid = self._get_or_generate_uuid(scenario_data["uuid"])
|
|
||||||
|
|
||||||
scenario_date = None
|
|
||||||
if scenario_data.get("scenario_date"):
|
|
||||||
scenario_date = scenario_data["scenario_date"]
|
|
||||||
|
|
||||||
scenario = Scenario.objects.create(
|
|
||||||
uuid=scenario_uuid,
|
|
||||||
package=package,
|
|
||||||
name=scenario_data["name"],
|
|
||||||
description=scenario_data["description"],
|
|
||||||
kv=scenario_data.get("kv", {}),
|
|
||||||
scenario_date=scenario_date,
|
|
||||||
scenario_type=scenario_data.get("scenario_type"),
|
|
||||||
additional_information=scenario_data.get("additional_information", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cache_object("Scenario", scenario_data["uuid"], scenario)
|
|
||||||
# Store scenario_data for later processing of parent relationships
|
|
||||||
scenario._import_data = scenario_data
|
|
||||||
|
|
||||||
# Second pass: set parent relationships
|
|
||||||
for scenario_data in scenarios_data:
|
|
||||||
if scenario_data.get("parent"):
|
|
||||||
scenario = self._get_cached_object("Scenario", scenario_data["uuid"])
|
|
||||||
parent = self._get_cached_object("Scenario", scenario_data["parent"]["uuid"])
|
|
||||||
if scenario and parent:
|
|
||||||
scenario.parent = parent
|
|
||||||
scenario.save()
|
|
||||||
|
|
||||||
def _import_models(self, package: Package, models_data: List[Dict[str, Any]]):
|
|
||||||
"""Import EPModels."""
|
|
||||||
print(f"Importing {len(models_data)} models...")
|
|
||||||
|
|
||||||
for model_data in models_data:
|
|
||||||
model_uuid = self._get_or_generate_uuid(model_data["uuid"])
|
|
||||||
model_type = model_data.get("model_type", "EPModel")
|
|
||||||
|
|
||||||
common_fields = {
|
|
||||||
"uuid": model_uuid,
|
|
||||||
"package": package,
|
|
||||||
"name": model_data["name"],
|
|
||||||
"description": model_data["description"],
|
|
||||||
"kv": model_data.get("kv", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add PackageBasedModel fields if present
|
|
||||||
if "threshold" in model_data:
|
|
||||||
common_fields.update(
|
|
||||||
{
|
|
||||||
"threshold": model_data.get("threshold"),
|
|
||||||
"eval_results": model_data.get("eval_results", {}),
|
|
||||||
"model_status": model_data.get("model_status", "INITIAL"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the appropriate model type
|
|
||||||
if model_type == "RuleBasedRelativeReasoning":
|
|
||||||
model = RuleBasedRelativeReasoning.objects.create(
|
|
||||||
**common_fields,
|
|
||||||
min_count=model_data.get("min_count", 1),
|
|
||||||
max_count=model_data.get("max_count", 10),
|
|
||||||
)
|
|
||||||
elif model_type == "MLRelativeReasoning":
|
|
||||||
model = MLRelativeReasoning.objects.create(**common_fields)
|
|
||||||
elif model_type == "EnviFormer":
|
|
||||||
model = EnviFormer.objects.create(**common_fields)
|
|
||||||
else:
|
|
||||||
model = EPModel.objects.create(**common_fields)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if model_data.get("aliases"):
|
|
||||||
model.aliases = model_data["aliases"]
|
|
||||||
model.save()
|
|
||||||
|
|
||||||
# Add package relationships for PackageBasedModel
|
|
||||||
if hasattr(model, "rule_packages"):
|
|
||||||
for pkg_ref in model_data.get("rule_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.rule_packages.add(pkg)
|
|
||||||
|
|
||||||
for pkg_ref in model_data.get("data_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.data_packages.add(pkg)
|
|
||||||
|
|
||||||
for pkg_ref in model_data.get("eval_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.eval_packages.add(pkg)
|
|
||||||
|
|
||||||
self._cache_object("EPModel", model_data["uuid"], model)
|
|
||||||
|
|
||||||
def _set_default_structures(self, compounds_data: List[Dict[str, Any]]):
|
|
||||||
"""Set default structures for compounds after all structures are created."""
|
|
||||||
print("Setting default structures for compounds...")
|
|
||||||
|
|
||||||
for compound_data in compounds_data:
|
|
||||||
if compound_data.get("default_structure"):
|
|
||||||
compound = self._get_cached_object("Compound", compound_data["uuid"])
|
|
||||||
structure = self._get_cached_object(
|
|
||||||
"CompoundStructure", compound_data["default_structure"]["uuid"]
|
|
||||||
)
|
|
||||||
if compound and structure:
|
|
||||||
compound.default_structure = structure
|
|
||||||
compound.save()
|
|
||||||
|
|
||||||
def _create_external_identifiers(self, obj, identifiers_data: List[Dict[str, Any]]):
|
|
||||||
"""Create external identifiers for an object."""
|
|
||||||
for identifier_data in identifiers_data:
|
|
||||||
# Get or create the external database
|
|
||||||
db_data = identifier_data["database"]
|
|
||||||
database, _ = ExternalDatabase.objects.get_or_create(
|
|
||||||
name=db_data["name"],
|
|
||||||
defaults={
|
|
||||||
"base_url": db_data.get("base_url", ""),
|
|
||||||
"full_name": db_data.get("name", ""),
|
|
||||||
"description": "",
|
|
||||||
"is_active": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the external identifier
|
|
||||||
ExternalIdentifier.objects.create(
|
|
||||||
content_object=obj,
|
|
||||||
database=database,
|
|
||||||
identifier_value=identifier_data["identifier_value"],
|
|
||||||
url=identifier_data.get("url", ""),
|
|
||||||
is_primary=identifier_data.get("is_primary", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PathwayUtils:
|
class PathwayUtils:
|
||||||
def __init__(self, pathway: "Pathway"):
|
def __init__(self, pathway: "Pathway"):
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#ad825570480bbe2f1a35c04923fede756c450751" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user