forked from enviPath/enviPy
Compare commits
28 Commits
d4295c9349
...
develop-ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b5d299128 | |||
| 38e901a51e | |||
| c92fccaf8e | |||
| 5eb3ebac89 | |||
| d9530ce755 | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e | |||
| 9d70db2ca2 | |||
| fec26d0233 | |||
| 689f7998eb | |||
| 8498e59fa1 | |||
| b508511cd6 | |||
| 877804c0ff | |||
| 964574c700 | |||
| 5029a8cda5 | |||
| d06bd0d4fd | |||
| f7c45b8015 | |||
| 68aea97013 | |||
| 3cc7fa9e8b | |||
| 21f3390a43 | |||
| 8cdf91c8fb | |||
| bafbf11322 | |||
| f1a9456d1d | |||
| e0764126e3 | |||
| ef0c45b203 | |||
| b737fc93eb |
93
.dockerignore
Normal file
93
.dockerignore
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# uv / virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# uv cache
|
||||||
|
.uv/
|
||||||
|
uv.lock.bak
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Test / coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache/
|
||||||
|
pytest_cache/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
|
||||||
|
# Type checkers
|
||||||
|
.mypy_cache/
|
||||||
|
.pyre/
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# IDEs / editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitea/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.yaml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Frontend stuff
|
||||||
|
node_modules/
|
||||||
@ -3,10 +3,20 @@ EP_DATA_DIR=
|
|||||||
ALLOWED_HOSTS=
|
ALLOWED_HOSTS=
|
||||||
DEBUG=
|
DEBUG=
|
||||||
LOG_LEVEL=
|
LOG_LEVEL=
|
||||||
|
MODEL_BUILDING_ENABLED=
|
||||||
|
APPLICABILITY_DOMAIN_ENABLED=
|
||||||
ENVIFORMER_PRESENT=
|
ENVIFORMER_PRESENT=
|
||||||
FLAG_CELERY_PRESENT=
|
ENVIFORMER_DEVICE=
|
||||||
SERVER_URL=
|
|
||||||
PLUGINS_ENABLED=
|
PLUGINS_ENABLED=
|
||||||
|
SERVER_URL=
|
||||||
|
SERVER_PATH=
|
||||||
|
ADMIN_APPROVAL_REQUIRED=
|
||||||
|
REGISTRATION_MANDATORY=
|
||||||
|
LOG_DIR=
|
||||||
|
# Celery
|
||||||
|
FLAG_CELERY_PRESENT=
|
||||||
|
CELERY_BROKER_URL=
|
||||||
|
CELERY_RESULT_BACKEND=
|
||||||
# DB
|
# DB
|
||||||
POSTGRES_SERVICE_NAME=
|
POSTGRES_SERVICE_NAME=
|
||||||
POSTGRES_DB=
|
POSTGRES_DB=
|
||||||
@ -16,5 +26,30 @@ POSTGRES_PORT=
|
|||||||
# MAIL
|
# MAIL
|
||||||
EMAIL_HOST_USER=
|
EMAIL_HOST_USER=
|
||||||
EMAIL_HOST_PASSWORD=
|
EMAIL_HOST_PASSWORD=
|
||||||
# MATOMO
|
DEFAULT_FROM_EMAIL=
|
||||||
MATOMO_SITE_ID
|
SERVER_EMAIL=
|
||||||
|
# SENTRY
|
||||||
|
SENTRY_ENABLED=
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENVIRONMENT=
|
||||||
|
# MS ENTRA
|
||||||
|
MS_ENTRA_ENABLED=
|
||||||
|
MS_CLIENT_ID=
|
||||||
|
MS_CLIENT_SECRET=
|
||||||
|
MS_TENANT_ID=
|
||||||
|
MS_REDIRECT_URI=
|
||||||
|
MS_SCOPES=
|
||||||
|
# Tenant
|
||||||
|
TENANT=
|
||||||
|
EPDB_PACKAGE_MODEL=
|
||||||
|
# Captcha
|
||||||
|
CAP_ENABLED=
|
||||||
|
CAP_API_BASE=
|
||||||
|
CAP_SITE_KEY=
|
||||||
|
CAP_SECRET_KEY=
|
||||||
|
# QUARKUS (JAVA)
|
||||||
|
ENVIRULE_ENABLED=
|
||||||
|
FINGERPRINT_URL=
|
||||||
|
# Biotransformer
|
||||||
|
BIOTRANSFORMER_ENABLED=
|
||||||
|
BIOTRANSFORMER_URL=
|
||||||
|
|||||||
@ -5,10 +5,12 @@ repos:
|
|||||||
rev: v3.2.0
|
rev: v3.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
exclude: epiuclid/schemas/
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
exclude: epiuclid/schemas/
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
exclude: ^static/images/|fixtures/
|
exclude: ^static/images/|^epiuclid/schemas/|^fixtures/
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.13.3
|
rev: v0.13.3
|
||||||
|
|||||||
100
Dockerfile
Normal file
100
Dockerfile
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
curl \
|
||||||
|
openssh-client \
|
||||||
|
git \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
ENV PATH="/root/.local/bin:${PATH}"
|
||||||
|
|
||||||
|
# Install dependencies first (cached layer — only invalidated when lockfile changes)
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
|
||||||
|
# Add key from git.envipath.com to known_hosts
|
||||||
|
RUN mkdir -p -m 0700 /root/.ssh \
|
||||||
|
&& ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts
|
||||||
|
|
||||||
|
# We'll need access to private repos, let docker make use of host ssh agent and use it like:
|
||||||
|
# docker build --ssh default -t envipath/envipy-bayer:1.0 .
|
||||||
|
RUN --mount=type=ssh \
|
||||||
|
uv sync --locked --extra ms-login --extra pepper-plugin
|
||||||
|
|
||||||
|
# Now copy source and do a final sync to install the project itself
|
||||||
|
# Ensure .dockerignore is reasonable
|
||||||
|
COPY bb4g bb4g
|
||||||
|
COPY biotransformer biotransformer
|
||||||
|
COPY bayer bayer
|
||||||
|
COPY bridge bridge
|
||||||
|
COPY envipath envipath
|
||||||
|
COPY epapi epapi
|
||||||
|
COPY epauth epauth
|
||||||
|
COPY epdb epdb
|
||||||
|
COPY epiuclid epiuclid
|
||||||
|
COPY fixtures fixtures
|
||||||
|
COPY migration migration
|
||||||
|
COPY pepper pepper
|
||||||
|
COPY scripts scripts
|
||||||
|
COPY static static
|
||||||
|
COPY templates templates
|
||||||
|
COPY tests tests
|
||||||
|
COPY utilities utilities
|
||||||
|
COPY manage.py .
|
||||||
|
|
||||||
|
# Install frontend deps
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Build frontend assets
|
||||||
|
RUN uv run python scripts/pnpm_wrapper.py install
|
||||||
|
RUN uv run python scripts/pnpm_wrapper.py run build
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS production
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq5 \
|
||||||
|
libxrender1 \
|
||||||
|
libxext6 \
|
||||||
|
libfontconfig1 \
|
||||||
|
nano \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd -ms /bin/bash django
|
||||||
|
|
||||||
|
# Create directories in /opt and set ownership
|
||||||
|
RUN mkdir -p /opt/enviPy \
|
||||||
|
&& mkdir -p /opt/enviPy/celery \
|
||||||
|
&& mkdir -p /opt/enviPy/log \
|
||||||
|
&& mkdir -p /opt/enviPy/models \
|
||||||
|
&& mkdir -p /opt/enviPy/plugins \
|
||||||
|
&& mkdir -p /opt/enviPy/static \
|
||||||
|
&& chown -R django:django /opt/enviPy
|
||||||
|
|
||||||
|
COPY --from=builder --chown=django:django /app /app
|
||||||
|
|
||||||
|
RUN touch /app/.env && chown -R django:django /app/.env
|
||||||
|
|
||||||
|
USER django
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "envipath.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
||||||
0
bayer/__init__.py
Normal file
0
bayer/__init__.py
Normal file
19
bayer/admin.py
Normal file
19
bayer/admin.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
from .models import (
|
||||||
|
PESCompound,
|
||||||
|
PESStructure
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PESCompoundAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PESStructureAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(PESCompound, PESCompoundAdmin)
|
||||||
|
admin.site.register(PESStructure, PESStructureAdmin)
|
||||||
6
bayer/apps.py
Normal file
6
bayer/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BayerConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'bayer'
|
||||||
39
bayer/epdb_hooks.py
Normal file
39
bayer/epdb_hooks.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from epdb.template_registry import register_template
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# PES Create
|
||||||
|
register_template(
|
||||||
|
"epdb.actions.collections.compound",
|
||||||
|
"actions/collections/new_pes.html",
|
||||||
|
)
|
||||||
|
register_template(
|
||||||
|
"modals.collections.compound",
|
||||||
|
"modals/collections/new_pes_modal.html",
|
||||||
|
)
|
||||||
|
register_template(
|
||||||
|
"epdb.actions.objects.pathway.add",
|
||||||
|
"actions/objects/pathway_add_pes.html",
|
||||||
|
)
|
||||||
|
register_template(
|
||||||
|
"epdb.modals.objects.pathway.add",
|
||||||
|
"modals/objects/add_pathway_pes_node_modal.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
# PES Viz
|
||||||
|
register_template(
|
||||||
|
"epdb.objects.compound.viz",
|
||||||
|
"objects/compound_viz.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
register_template(
|
||||||
|
"epdb.objects.compound_structure.viz",
|
||||||
|
"objects/compound_structure_viz.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
register_template(
|
||||||
|
"epdb.objects.node.viz",
|
||||||
|
"objects/node_viz.html",
|
||||||
|
)
|
||||||
35
bayer/migrations/0001_initial.py
Normal file
35
bayer/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Package',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
||||||
|
('classification_level', models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted'), (20, 'Secret')], default=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'epdb_package',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
22
bayer/migrations/0002_initial.py
Normal file
22
bayer/migrations/0002_initial.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bayer', '0001_initial'),
|
||||||
|
('epdb', '0019_remove_scenario_additional_information_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='license',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-17 21:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bayer', '0002_initial'),
|
||||||
|
('epdb', '0023_alter_compoundstructure_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PESCompound',
|
||||||
|
fields=[
|
||||||
|
('compound_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compound')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.compound',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PESStructure',
|
||||||
|
fields=[
|
||||||
|
('compoundstructure_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compoundstructure')),
|
||||||
|
('pes_link', models.URLField(verbose_name='PES Link')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.compoundstructure',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='data_pool',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
bayer/migrations/__init__.py
Normal file
0
bayer/migrations/__init__.py
Normal file
236
bayer/models.py
Normal file
236
bayer/models.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
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)}"
|
||||||
|
}
|
||||||
9
bayer/templates/actions/collections/new_pes.html
Normal file
9
bayer/templates/actions/collections/new_pes.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
onclick="document.getElementById('new_pes_modal').showModal(); return false;"
|
||||||
|
>
|
||||||
|
New PES
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
8
bayer/templates/actions/objects/pathway_add_pes.html
Normal file
8
bayer/templates/actions/objects/pathway_add_pes.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
onclick="document.getElementById('add_pathway_pes_node_modal').showModal(); return false;"
|
||||||
|
>
|
||||||
|
<i class="glyphicon glyphicon-plus"></i> Add PES</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
175
bayer/templates/modals/collections/new_package_modal.html
Normal file
175
bayer/templates/modals/collections/new_package_modal.html
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="new_package_modal"
|
||||||
|
class="modal"
|
||||||
|
x-data="{
|
||||||
|
isSubmitting: false,
|
||||||
|
packageClassification: null,
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
this.packageClassification = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setFormData(data) {
|
||||||
|
this.formData = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
get isSecret() {
|
||||||
|
return this.packageClassification === '20';
|
||||||
|
},
|
||||||
|
|
||||||
|
submit(formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
|
||||||
|
// Remove previously injected inputs
|
||||||
|
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Add values from dynamic form into the html form
|
||||||
|
if (this.formData) {
|
||||||
|
Object.entries(this.formData).forEach(([key, value]) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = key;
|
||||||
|
input.value = value;
|
||||||
|
input.classList.add('dynamic-param');
|
||||||
|
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form && form.checkValidity()) {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
form.submit();
|
||||||
|
} else if (form) {
|
||||||
|
form.reportValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
@close="reset()"
|
||||||
|
>
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<h3 class="text-lg font-bold">New Package</h3>
|
||||||
|
|
||||||
|
<!-- Close button (X) -->
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="py-4">
|
||||||
|
<form
|
||||||
|
id="new_package_form"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
action=""
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="package-name">
|
||||||
|
<span class="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="package-name"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="package-name"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="package-description">
|
||||||
|
<span class="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="package-description"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Description..."
|
||||||
|
name="package-description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Classification Level -->
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="package-classification">
|
||||||
|
<span class="label-text">Package Classification</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="package-classification"
|
||||||
|
name="package-classification"
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
x-model="packageClassification"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="null" disabled selected>Select Classification</option>
|
||||||
|
<option value="0">Internal</option>
|
||||||
|
<option value="10">Restricted</option>
|
||||||
|
<option value="20">Secret</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Secret Groups -->
|
||||||
|
<div class="form-control mb-3" x-show="isSecret" x-cloak>
|
||||||
|
<label class="label" for="package-data-pool">
|
||||||
|
<span class="label-text">Data Pool for SECRET Package</span>
|
||||||
|
</label>
|
||||||
|
<p>Only users with this role can be granted access to this package</p>
|
||||||
|
<select
|
||||||
|
id="package-data-pool"
|
||||||
|
name="package-data-pool"
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
>
|
||||||
|
<option value="" disabled selected>Select Data Pool</option>
|
||||||
|
{% for obj in meta.secret_groups %}
|
||||||
|
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-action">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick="this.closest('dialog').close()"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="submit('new_package_form')"
|
||||||
|
:disabled="isSubmitting || !selectedType || loadingSchemas"
|
||||||
|
>
|
||||||
|
<span x-show="!isSubmitting">Submit</span>
|
||||||
|
<span
|
||||||
|
x-show="isSubmitting"
|
||||||
|
class="loading loading-spinner loading-sm"
|
||||||
|
></span>
|
||||||
|
<span x-show="isSubmitting">Creating...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button :disabled="isSubmitting">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
174
bayer/templates/modals/collections/new_pes_modal.html
Normal file
174
bayer/templates/modals/collections/new_pes_modal.html
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="new_pes_modal"
|
||||||
|
class="modal"
|
||||||
|
x-data="{
|
||||||
|
isSubmitting: false,
|
||||||
|
pesLink: null,
|
||||||
|
pesVizHtml: '',
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
get isPESSet() {
|
||||||
|
console.log(this.pesLink);
|
||||||
|
return this.pesLink !== null;
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePesViz() {
|
||||||
|
if (!this.isPESSet) {
|
||||||
|
this.pesVizHtml = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.objectFit = 'cover';
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
this.pesVizHtml = img.outerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
this.pesVizHtml = `
|
||||||
|
<div class='alert alert-error' role='alert'>
|
||||||
|
<h4 class='alert-heading'>Could not render PES!</h4>
|
||||||
|
<p>Could not render PES - Do you have access?</p>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
submit(formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
|
||||||
|
// Remove previously injected inputs
|
||||||
|
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Add values from dynamic form into the html form
|
||||||
|
if (this.formData) {
|
||||||
|
Object.entries(this.formData).forEach(([key, value]) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = key;
|
||||||
|
input.value = value;
|
||||||
|
input.classList.add('dynamic-param');
|
||||||
|
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form && form.checkValidity()) {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
form.submit();
|
||||||
|
} else if (form) {
|
||||||
|
form.reportValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@close="reset()"
|
||||||
|
>
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<h3 class="text-lg font-bold">New PES</h3>
|
||||||
|
|
||||||
|
<!-- Close button (X) -->
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="py-4">
|
||||||
|
<form
|
||||||
|
id="new-pes-modal-form"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
action="{% url 'create pes' meta.current_package.uuid %}"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-name">
|
||||||
|
<span class="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-name"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-name"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-description">
|
||||||
|
<span class="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-description"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-description"
|
||||||
|
placeholder="Description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="pes-link">
|
||||||
|
<span class="label-text">Link to PES</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="pes-link"
|
||||||
|
name="pes-link"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
||||||
|
x-model="pesLink"
|
||||||
|
@input="updatePesViz()"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-action">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick="this.closest('dialog').close()"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="submit('new-pes-modal-form')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
<span x-show="!isSubmitting">Submit</span>
|
||||||
|
<span
|
||||||
|
x-show="isSubmitting"
|
||||||
|
class="loading loading-spinner loading-sm"
|
||||||
|
></span>
|
||||||
|
<span x-show="isSubmitting">Creating...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button :disabled="isSubmitting">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
174
bayer/templates/modals/objects/add_pathway_pes_node_modal.html
Normal file
174
bayer/templates/modals/objects/add_pathway_pes_node_modal.html
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="add_pathway_pes_node_modal"
|
||||||
|
class="modal"
|
||||||
|
x-data="{
|
||||||
|
isSubmitting: false,
|
||||||
|
pesLink: null,
|
||||||
|
pesVizHtml: '',
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
get isPESSet() {
|
||||||
|
console.log(this.pesLink);
|
||||||
|
return this.pesLink !== null;
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePesViz() {
|
||||||
|
if (!this.isPESSet) {
|
||||||
|
this.pesVizHtml = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = '{% url 'depict_pes' %}?pesLink=' + encodeURIComponent(this.pesLink);
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.objectFit = 'cover';
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
this.pesVizHtml = img.outerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
this.pesVizHtml = `
|
||||||
|
<div class='alert alert-error' role='alert'>
|
||||||
|
<h4 class='alert-heading'>Could not render PES!</h4>
|
||||||
|
<p>Could not render PES - Do you have access?</p>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
submit(formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
|
||||||
|
// Remove previously injected inputs
|
||||||
|
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Add values from dynamic form into the html form
|
||||||
|
if (this.formData) {
|
||||||
|
Object.entries(this.formData).forEach(([key, value]) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = key;
|
||||||
|
input.value = value;
|
||||||
|
input.classList.add('dynamic-param');
|
||||||
|
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form && form.checkValidity()) {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
form.submit();
|
||||||
|
} else if (form) {
|
||||||
|
form.reportValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@close="reset()"
|
||||||
|
>
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<h3 class="text-lg font-bold">New PES</h3>
|
||||||
|
|
||||||
|
<!-- Close button (X) -->
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="py-4">
|
||||||
|
<form
|
||||||
|
id="new-pes-node-modal-form"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
action="{% url 'create pes node' current_object.package.uuid current_object.uuid %}"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-name">
|
||||||
|
<span class="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-name"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-name"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="compound-description">
|
||||||
|
<span class="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="compound-description"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
name="compound-description"
|
||||||
|
placeholder="Description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-3">
|
||||||
|
<label class="label" for="pes-link">
|
||||||
|
<span class="label-text">Link to PES</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="pes-link"
|
||||||
|
name="pes-link"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Link to PES e.g. https://pesregapp-test.cropkey-np.ag/entities/PES-000126"
|
||||||
|
x-model="pesLink"
|
||||||
|
@input="updatePesViz()"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pes-viz" class="mb-3" x-html="pesVizHtml"></div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-action">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick="this.closest('dialog').close()"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="submit('new-pes-node-modal-form')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
<span x-show="!isSubmitting">Submit</span>
|
||||||
|
<span
|
||||||
|
x-show="isSubmitting"
|
||||||
|
class="loading loading-spinner loading-sm"
|
||||||
|
></span>
|
||||||
|
<span x-show="isSubmitting">Creating...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button :disabled="isSubmitting">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
19
bayer/templates/objects/compound_structure_viz.html
Normal file
19
bayer/templates/objects/compound_structure_viz.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% if compound_structure.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Representation -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img src='{% url 'depict_pes' %}?pesLink={{ compound_structure.pes_link|urlencode }}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
19
bayer/templates/objects/compound_viz.html
Normal file
19
bayer/templates/objects/compound_viz.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% if compound.default_structure.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Representation -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img src='{% url 'depict_pes' %}?pesLink={{ compound.default_structure.pes_link|urlencode }}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
19
bayer/templates/objects/node_viz.html
Normal file
19
bayer/templates/objects/node_viz.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% if node.default_node_label.pes_link %}
|
||||||
|
<!-- PES -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
|
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Representation -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">PES Image Representation</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img src='{% url 'depict_pes' %}?pesLink={{ node.default_node_label.pes_link|urlencode }}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
97
bayer/templates/objects/package.html
Normal file
97
bayer/templates/objects/package.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{% extends "framework_modern.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% block action_modals %}
|
||||||
|
{% include "modals/objects/edit_package_modal.html" %}
|
||||||
|
{% include "modals/objects/edit_package_permissions_modal.html" %}
|
||||||
|
{% include "modals/objects/publish_package_modal.html" %}
|
||||||
|
{% include "modals/objects/set_license_modal.html" %}
|
||||||
|
{% include "modals/objects/export_package_modal.html" %}
|
||||||
|
{% include "modals/objects/generic_delete_modal.html" %}
|
||||||
|
{% endblock action_modals %}
|
||||||
|
|
||||||
|
<div class="space-y-2 p-4">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="card bg-base-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="card-title text-2xl">{{ package.name }} {% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}<img src="{% static 'images/restricted_mid.png' %}" width="100">{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}<img src="{% static 'images/secret_mid.png' %}" width="60">{% endif %}</h2>
|
||||||
|
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-wrench"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="-1"
|
||||||
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||||
|
>
|
||||||
|
{% block actions %}
|
||||||
|
{% include "actions/objects/package.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">{{ package.description|safe }}</p>
|
||||||
|
<ul class="menu bg-base-200 rounded-box mt-4 w-full">
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/pathway" class="hover:bg-base-300"
|
||||||
|
>Pathways ({{ package.pathways.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/rule" class="hover:bg-base-300"
|
||||||
|
>Rules ({{ package.rules.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/compound" class="hover:bg-base-300"
|
||||||
|
>Compounds ({{ package.compounds.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/reaction" class="hover:bg-base-300"
|
||||||
|
>Reactions ({{ package.reactions.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/model" class="hover:bg-base-300"
|
||||||
|
>Models ({{ package.models.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ package.url }}/scenario" class="hover:bg-base-300"
|
||||||
|
>Scenarios ({{ package.scenarios.count }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Show actions button if there are actions
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const actionsButton = document.getElementById("actionsButton");
|
||||||
|
const actionsList = actionsButton?.querySelector("ul");
|
||||||
|
if (actionsList && actionsList.children.length > 0) {
|
||||||
|
actionsButton?.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock content %}
|
||||||
154
bayer/templates/static/login.html
Normal file
154
bayer/templates/static/login.html
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
{% extends "static/login_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}enviPath - Sign In{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<style>
|
||||||
|
/* Tab styling */
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input[type="radio"].tab-radio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-label {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.tab-label:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
input[type="radio"].tab-radio:checked + .tab-label {
|
||||||
|
border-bottom-color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div>
|
||||||
|
<img src="{% static 'images/bayer-logo.svg' %}">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-4 ...">
|
||||||
|
<div><p></p></div>
|
||||||
|
<div><p></p></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="border-base-300 mb-6 border-b" hidden>
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="auth-tab"
|
||||||
|
id="tab-sso"
|
||||||
|
class="tab-radio"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label for="tab-sso" class="tab-label">SSO</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="auth-tab"
|
||||||
|
id="tab-signin"
|
||||||
|
class="tab-radio"
|
||||||
|
/>
|
||||||
|
<label for="tab-signin" class="tab-label">Local User</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SSO Tab -->
|
||||||
|
<div id="content-sso" class="tab-content active">
|
||||||
|
<button role="link" onclick="window.location.href='/entra/login'" name="sso" class="btn btn-primary w-full">
|
||||||
|
Login with Microsoft
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign In Tab -->
|
||||||
|
<div id="content-signin" class="tab-content">
|
||||||
|
<form method="post" action="{% url 'login' %}" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="login" value="true" />
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="username">
|
||||||
|
<span class="label-text">Account</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
placeholder="Username or Email"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="passwordinput">
|
||||||
|
<span class="label-text">Password</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="passwordinput"
|
||||||
|
name="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<a href="{% url 'password_reset' %}" class="link link-primary text-sm"
|
||||||
|
>Forgot password?</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
|
||||||
|
<button type="submit" name="signin" class="btn btn-primary w-full">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
// Tab switching functionality
|
||||||
|
document.querySelectorAll('input[name="auth-tab"]').forEach((radio) => {
|
||||||
|
radio.addEventListener("change", function () {
|
||||||
|
// Hide all content
|
||||||
|
document.querySelectorAll(".tab-content").forEach((content) => {
|
||||||
|
content.classList.remove("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selected content
|
||||||
|
const contentId = "content-" + this.id.replace("tab-", "");
|
||||||
|
document.getElementById(contentId).classList.add("active");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for hash in URL to auto-select tab
|
||||||
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const hash = window.location.hash.substring(1); // Remove the # symbol
|
||||||
|
if (hash === "signup" || hash === "signin") {
|
||||||
|
const tabRadio = document.getElementById("tab-" + hash);
|
||||||
|
if (tabRadio) {
|
||||||
|
tabRadio.checked = true;
|
||||||
|
// Trigger change event to show correct content
|
||||||
|
tabRadio.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
3
bayer/tests.py
Normal file
3
bayer/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
0
bayer/tests/__init__.py
Normal file
0
bayer/tests/__init__.py
Normal file
0
bayer/tests/pes/__init__.py
Normal file
0
bayer/tests/pes/__init__.py
Normal file
174
bayer/tests/pes/test_pes.py
Normal file
174
bayer/tests/pes/test_pes.py
Normal file
File diff suppressed because one or more lines are too long
19
bayer/urls.py
Normal file
19
bayer/urls.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import views as v
|
||||||
|
|
||||||
|
UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"),
|
||||||
|
re_path(
|
||||||
|
rf"^package/(?P<package_uuid>{UUID})/pes$",
|
||||||
|
v.create_pes,
|
||||||
|
name="create pes",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/pes$",
|
||||||
|
v.create_pes_node,
|
||||||
|
name="create pes node",
|
||||||
|
),
|
||||||
|
]
|
||||||
155
bayer/views.py
Normal file
155
bayer/views.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
from bayer.models import PESCompound
|
||||||
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import Pathway, Node
|
||||||
|
from epdb.views import _anonymous_or_real
|
||||||
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
|
@package_permission_required()
|
||||||
|
def create_pes(request, package_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
|
compound_name = request.POST.get('compound-name')
|
||||||
|
compound_description = request.POST.get('compound-description')
|
||||||
|
pes_link = request.POST.get('pes-link')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
except ValueError as e:
|
||||||
|
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
|
return redirect(pes.url)
|
||||||
|
else:
|
||||||
|
return BadRequest("Please provide a PES link.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@package_permission_required()
|
||||||
|
def create_pes_node(request, package_uuid, pathway_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
|
compound_name = request.POST.get('compound-name')
|
||||||
|
compound_description = request.POST.get('compound-description')
|
||||||
|
pes_link = request.POST.get('pes-link')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
except ValueError as e:
|
||||||
|
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
|
n = Node()
|
||||||
|
n.stereo_removed = False
|
||||||
|
n.pathway = current_pathway
|
||||||
|
n.depth = 0
|
||||||
|
|
||||||
|
n.default_node_label = pes.default_structure
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
n.node_labels.add(pes.default_structure)
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return BadRequest("Please provide a PES link.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_pes(request, pes_url) -> dict:
|
||||||
|
from epauth.views import get_access_token_from_request
|
||||||
|
token = get_access_token_from_request(request)
|
||||||
|
|
||||||
|
if token:
|
||||||
|
for k, v in s.PES_API_MAPPING.items():
|
||||||
|
if pes_url.startswith(k):
|
||||||
|
pes_id = pes_url.split('/')[-1]
|
||||||
|
|
||||||
|
if pes_id == 'dummy':
|
||||||
|
import json
|
||||||
|
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
||||||
|
res_data["pes_url"] = pes_url
|
||||||
|
return res_data
|
||||||
|
else:
|
||||||
|
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
||||||
|
params = {"pes_reg_entity_corporate_id": pes_id}
|
||||||
|
|
||||||
|
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res.raise_for_status()
|
||||||
|
pes_data = res.json()
|
||||||
|
|
||||||
|
if len(pes_data) == 0:
|
||||||
|
raise ValueError(f"PES with id {pes_id} not found")
|
||||||
|
|
||||||
|
res_data = pes_data[0]
|
||||||
|
res_data["pes_url"] = pes_url
|
||||||
|
return res_data
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown URL {pes_url}")
|
||||||
|
else:
|
||||||
|
raise ValueError("Could not fetch access token from request.")
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_pes(request):
|
||||||
|
pes_link = request.GET.get('pesLink')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
|
||||||
|
representations = pes_data.get('representations')
|
||||||
|
|
||||||
|
for rep in representations:
|
||||||
|
if rep.get('type') == 'color':
|
||||||
|
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
|
||||||
|
return HttpResponse(image_data, content_type="image/png")
|
||||||
183
bb4g/__init__.py
Normal file
183
bb4g/__init__.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import json
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
import enum
|
||||||
|
import requests
|
||||||
|
from django.conf import settings as s
|
||||||
|
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
|
||||||
|
from envipy_additional_information import register
|
||||||
|
|
||||||
|
from bridge.contracts import Classifier # noqa: I001
|
||||||
|
from bridge.dto import (
|
||||||
|
BuildResult,
|
||||||
|
EnviPyDTO,
|
||||||
|
EvaluationResult,
|
||||||
|
RunResult,
|
||||||
|
TransformationProductPrediction,
|
||||||
|
) # noqa: I001
|
||||||
|
|
||||||
|
class SamplingAlgorithm(enum.Enum):
|
||||||
|
EXACT = "exact"
|
||||||
|
|
||||||
|
|
||||||
|
@register("bb4gconfig")
|
||||||
|
class BB4GConfig(EnviPyModel):
|
||||||
|
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
||||||
|
cutoff: int = -5
|
||||||
|
|
||||||
|
class UI:
|
||||||
|
title = "BB4G Configuration"
|
||||||
|
sampling_algorithm = UIConfig(
|
||||||
|
widget=WidgetType.SELECT,
|
||||||
|
label="BB4G Sampling Algorithm",
|
||||||
|
order=1,
|
||||||
|
placeholder="If unset defaults to 'exact'"
|
||||||
|
)
|
||||||
|
cutoff = UIConfig(
|
||||||
|
widget=WidgetType.NUMBER,
|
||||||
|
label="BB4G Cutoff",
|
||||||
|
order=2,
|
||||||
|
placeholder="If unset defaults to -5"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Once stable these will be exposed by enviPy-plugins lib
|
||||||
|
class BB4G(Classifier):
|
||||||
|
Config = BB4GConfig
|
||||||
|
|
||||||
|
def __init__(self, config: BB4GConfig | None = None):
|
||||||
|
super().__init__(config)
|
||||||
|
self.url = f"{s.BB4G_URL}"
|
||||||
|
|
||||||
|
self.token = self.acquire_token()
|
||||||
|
self.header = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def acquire_token(self):
|
||||||
|
BB4G_TENANT_ID = s.BB4G_TENANT_ID
|
||||||
|
BB4G_CLIENT_ID = s.BB4G_CLIENT_ID
|
||||||
|
BB4G_CLIENT_SECRET = s.BB4G_CLIENT_SECRET
|
||||||
|
BB4G_SCOPE = s.BB4G_SCOPE
|
||||||
|
|
||||||
|
BB4G_TOKEN_URL = f"https://login.microsoftonline.com/{BB4G_TENANT_ID}/oauth2/v2.0/token"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"client_id": BB4G_CLIENT_ID,
|
||||||
|
"client_secret": BB4G_CLIENT_SECRET,
|
||||||
|
"scope": BB4G_SCOPE,
|
||||||
|
"grant_type": "client_credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
# No Proxy required, URL is whitelisted
|
||||||
|
res = requests.post(BB4G_TOKEN_URL, data=payload)
|
||||||
|
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
return res.json()["access_token"]
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
header = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
started = False
|
||||||
|
retries = 0
|
||||||
|
while not started and retries < 5:
|
||||||
|
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
||||||
|
|
||||||
|
if res.status_code == 200:
|
||||||
|
started = True
|
||||||
|
elif res.status_code in [500, 502]:
|
||||||
|
retries += 1
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected status code: {res.status_code}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def requires_rule_packages(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def requires_data_packages(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def identifier(cls) -> str:
|
||||||
|
return "bb4g"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def name(cls) -> str:
|
||||||
|
return "BB4G Template Free Model"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def display(cls) -> str:
|
||||||
|
return "BB4G Template Free Model"
|
||||||
|
|
||||||
|
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||||
|
|
||||||
|
# Ensure Service is running
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
smiles = [c.smiles for c in eP.get_compounds()]
|
||||||
|
preds = self._post(smiles)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for substrate in preds.keys():
|
||||||
|
results.append(
|
||||||
|
TransformationProductPrediction(
|
||||||
|
substrate=substrate,
|
||||||
|
products=preds[substrate],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return RunResult(
|
||||||
|
producer=eP.get_context().url,
|
||||||
|
description=f"Generated at {datetime.now()}",
|
||||||
|
result=results,
|
||||||
|
)
|
||||||
|
|
||||||
|
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _post(self, smiles: List[str]) -> dict[str, dict[str, float]]:
|
||||||
|
header = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for smi in smiles:
|
||||||
|
data = {
|
||||||
|
"smiles": smi,
|
||||||
|
"sampling_alg": self.config.sampling_algorithm.value,
|
||||||
|
"cutoff": self.config.cutoff,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
|
||||||
|
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
for substrate, predictions in resp.json().items():
|
||||||
|
preds = {}
|
||||||
|
|
||||||
|
for pred in predictions:
|
||||||
|
prod = pred["prediction"]
|
||||||
|
prob = math.exp(pred["log_likelihood"])
|
||||||
|
preds[prod] = prob
|
||||||
|
|
||||||
|
result[substrate] = preds
|
||||||
|
|
||||||
|
return result
|
||||||
112
biotransformer/__init__.py
Normal file
112
biotransformer/__init__.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import enum
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings as s
|
||||||
|
|
||||||
|
# Once stable these will be exposed by enviPy-plugins lib
|
||||||
|
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType # noqa: I001
|
||||||
|
from envipy_additional_information import register # noqa: I001
|
||||||
|
|
||||||
|
from bridge.contracts import Classifier # noqa: I001
|
||||||
|
from bridge.dto import (
|
||||||
|
BuildResult,
|
||||||
|
EnviPyDTO,
|
||||||
|
EvaluationResult,
|
||||||
|
RunResult,
|
||||||
|
TransformationProductPrediction,
|
||||||
|
) # noqa: I001
|
||||||
|
|
||||||
|
|
||||||
|
class BiotransformerEnvType(enum.Enum):
|
||||||
|
CYP450 = "CYP450"
|
||||||
|
ALLHUMAN = "ALLHUMAN"
|
||||||
|
ECBASED = "ECBASED"
|
||||||
|
HGUT = "HGUT"
|
||||||
|
PHASEII = "PHASEII"
|
||||||
|
ENV = "ENV"
|
||||||
|
|
||||||
|
|
||||||
|
@register("biotransformerconfig")
|
||||||
|
class BiotransformerConfig(EnviPyModel):
|
||||||
|
env_type: BiotransformerEnvType
|
||||||
|
|
||||||
|
class UI:
|
||||||
|
title = "Biotransformer Type"
|
||||||
|
env_type = UIConfig(widget=WidgetType.SELECT, label="Biotransformer Type", order=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Biotransformer(Classifier):
|
||||||
|
Config = BiotransformerConfig
|
||||||
|
|
||||||
|
def __init__(self, config: BiotransformerConfig | None = None):
|
||||||
|
super().__init__(config)
|
||||||
|
self.url = f"{s.BIOTRANSFORMER_URL}/biotransformer"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def requires_rule_packages(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def requires_data_packages(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def identifier(cls) -> str:
|
||||||
|
return "biotransformer3"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def name(cls) -> str:
|
||||||
|
return "Biotransformer 3.0"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def display(cls) -> str:
|
||||||
|
return "Biotransformer 3.0"
|
||||||
|
|
||||||
|
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||||
|
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]]:
|
||||||
|
data = {"substrates": smiles, "mode": self.config.env_type.value}
|
||||||
|
res = requests.post(self.url, json=data)
|
||||||
|
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
# Example Response JSON:
|
||||||
|
# {
|
||||||
|
# 'products': {
|
||||||
|
# 'CN1C=NC2=C1C(=O)N(C(=O)N2C)C': {
|
||||||
|
# 'CN1C2=C(C(=O)N(C)C1=O)NC=N2': 0.5,
|
||||||
|
# 'CN1C=NC2=C1C(=O)N(C)C(=O)N2.CN1C=NC2=C1C(=O)NC(=O)N2C.CO': 0.5
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
return res.json()["products"]
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import enum
|
import enum
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from envipy_additional_information import EnviPyModel
|
||||||
|
|
||||||
from .dto import BuildResult, EnviPyDTO, EvaluationResult, RunResult
|
from .dto import BuildResult, EnviPyDTO, EvaluationResult, RunResult
|
||||||
|
|
||||||
|
|
||||||
@ -27,12 +29,14 @@ class Plugin(ABC):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def identifier(self) -> str:
|
def identifier(cls) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def name(self) -> str:
|
def name(cls) -> str:
|
||||||
"""
|
"""
|
||||||
Represents an abstract method that provides a contract for implementing a method
|
Represents an abstract method that provides a contract for implementing a method
|
||||||
to return a name as a string. Must be implemented in subclasses.
|
to return a name as a string. Must be implemented in subclasses.
|
||||||
@ -46,8 +50,9 @@ class Plugin(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def display(self) -> str:
|
def display(cls) -> str:
|
||||||
"""
|
"""
|
||||||
An abstract method that must be implemented by subclasses to display
|
An abstract method that must be implemented by subclasses to display
|
||||||
specific information or behavior. The method ensures that all subclasses
|
specific information or behavior. The method ensures that all subclasses
|
||||||
@ -64,8 +69,9 @@ class Plugin(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Property(Plugin):
|
class Property(Plugin):
|
||||||
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def requires_rule_packages(self) -> bool:
|
def requires_rule_packages(cls) -> bool:
|
||||||
"""
|
"""
|
||||||
Defines an abstract method to determine whether rule packages are required.
|
Defines an abstract method to determine whether rule packages are required.
|
||||||
|
|
||||||
@ -79,8 +85,9 @@ class Property(Plugin):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def requires_data_packages(self) -> bool:
|
def requires_data_packages(cls) -> bool:
|
||||||
"""
|
"""
|
||||||
Defines an abstract method to determine whether data packages are required.
|
Defines an abstract method to determine whether data packages are required.
|
||||||
|
|
||||||
@ -231,3 +238,171 @@ class Property(Plugin):
|
|||||||
NotImplementedError: If the method is not implemented by a subclass.
|
NotImplementedError: If the method is not implemented by a subclass.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Classifier(Plugin):
|
||||||
|
Config: type[EnviPyModel] | None = None
|
||||||
|
|
||||||
|
def __init__(self, config: EnviPyModel | None = None):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_config(cls) -> bool:
|
||||||
|
return cls.Config is not None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
||||||
|
if cls.Config is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# remove empty strings a.k.a unset params to not overwrite defaults
|
||||||
|
cpy = {}
|
||||||
|
if data is not None:
|
||||||
|
for k, v in data.items():
|
||||||
|
if v != "":
|
||||||
|
cpy[k] = v
|
||||||
|
|
||||||
|
return cls.Config(**cpy)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, data: dict | None = None):
|
||||||
|
return cls(cls.parse_config(data))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def requires_rule_packages(cls) -> bool:
|
||||||
|
"""
|
||||||
|
Defines an abstract method to determine whether rule packages are required.
|
||||||
|
|
||||||
|
This method should be implemented by subclasses to specify if they depend
|
||||||
|
on rule packages for their functioning.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If the subclass has not implemented this method.
|
||||||
|
|
||||||
|
@return: A boolean indicating if rule packages are required.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def requires_data_packages(cls) -> bool:
|
||||||
|
"""
|
||||||
|
Defines an abstract method to determine whether data packages are required.
|
||||||
|
|
||||||
|
This method should be implemented by subclasses to specify if they depend
|
||||||
|
on data packages for their functioning.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If the subclass has not implemented this method.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the service requires data packages, False otherwise.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||||
|
"""
|
||||||
|
Abstract method to prepare and construct a specific build process based on the provided
|
||||||
|
environment data transfer object (EnviPyDTO). This method should be implemented by
|
||||||
|
subclasses to handle the particular requirements of the environment.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
eP : EnviPyDTO
|
||||||
|
The data transfer object containing environment details for the build process.
|
||||||
|
|
||||||
|
*args :
|
||||||
|
Additional positional arguments required for the build.
|
||||||
|
|
||||||
|
**kwargs :
|
||||||
|
Additional keyword arguments to offer flexibility and customization for
|
||||||
|
the build process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BuildResult | None
|
||||||
|
Returns a BuildResult instance if the build operation succeeds, else returns None.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError
|
||||||
|
If the method is not implemented in a subclass.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||||
|
"""
|
||||||
|
Represents an abstract base class for executing a generic process with
|
||||||
|
provided parameters and returning a standardized result.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
None.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
run(eP, *args, **kwargs):
|
||||||
|
Executes a task with specified input parameters and optional
|
||||||
|
arguments, returning the outcome in the form of a RunResult object.
|
||||||
|
This is an abstract method and must be implemented in subclasses.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If the subclass does not implement the abstract
|
||||||
|
method.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
eP (EnviPyDTO): The primary object containing information or data required
|
||||||
|
for processing. Mandatory.
|
||||||
|
*args: Variable length argument list for additional positional arguments.
|
||||||
|
**kwargs: Arbitrary keyword arguments for additional options or settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RunResult: A result object encapsulating the status, output, or details
|
||||||
|
of the process execution.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult | None:
|
||||||
|
"""
|
||||||
|
Abstract method for evaluating data based on the given input and additional arguments.
|
||||||
|
|
||||||
|
This method is intended to be implemented by subclasses and provides
|
||||||
|
a mechanism to perform an evaluation procedure based on input encapsulated
|
||||||
|
in an EnviPyDTO object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
eP : EnviPyDTO
|
||||||
|
The data transfer object containing necessary input for evaluation.
|
||||||
|
*args : tuple
|
||||||
|
Additional positional arguments for the evaluation process.
|
||||||
|
**kwargs : dict
|
||||||
|
Additional keyword arguments for the evaluation process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EvaluationResult
|
||||||
|
The result of the evaluation performed by the method.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError
|
||||||
|
If the method is not implemented in the subclass.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult | None:
|
||||||
|
"""
|
||||||
|
An abstract method designed to build and evaluate a model or system using the provided
|
||||||
|
environmental parameters and additional optional arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
eP (EnviPyDTO): The environmental parameters required for building and evaluating.
|
||||||
|
*args: Additional positional arguments.
|
||||||
|
**kwargs: Additional keyword arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EvaluationResult: The result of the evaluation process.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If the method is not implemented by a subclass.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@ -59,10 +59,19 @@ class EnviPyDTO(Protocol):
|
|||||||
) -> List["ProductSet"]: ...
|
) -> List["ProductSet"]: ...
|
||||||
|
|
||||||
|
|
||||||
class PredictedProperty(EnviPyModel):
|
class EnviPyPrediction(EnviPyModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyPrediction(EnviPyPrediction):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TransformationProductPrediction(EnviPyPrediction):
|
||||||
|
substrate: str
|
||||||
|
products: dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
@register("buildresult")
|
@register("buildresult")
|
||||||
class BuildResult(EnviPyModel):
|
class BuildResult(EnviPyModel):
|
||||||
data: dict[str, Any] | List[dict[str, Any]] | None
|
data: dict[str, Any] | List[dict[str, Any]] | None
|
||||||
@ -72,7 +81,7 @@ class BuildResult(EnviPyModel):
|
|||||||
class RunResult(EnviPyModel):
|
class RunResult(EnviPyModel):
|
||||||
producer: HttpUrl
|
producer: HttpUrl
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
result: PredictedProperty | List[PredictedProperty]
|
result: EnviPyPrediction | List[EnviPyPrediction]
|
||||||
|
|
||||||
|
|
||||||
@register("evaluationresult")
|
@register("evaluationresult")
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:18
|
image: postgres:18
|
||||||
container_name: envipath-postgres
|
container_name: eppostgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: envipath
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql
|
- ep_bayer_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,9 +18,37 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: envipath-redis
|
container_name: epredis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_redis_data:/data
|
||||||
|
|
||||||
|
biotransformer3:
|
||||||
|
image: envipath/biotransformer3:1.0
|
||||||
|
container_name: epbiotransformer3
|
||||||
|
|
||||||
|
# web:
|
||||||
|
# image: envipath/envipy-bayer:1.0
|
||||||
|
# container_name: epdjango
|
||||||
|
# ports:
|
||||||
|
# - "127.0.0.1:8000:8000"
|
||||||
|
# env_file:
|
||||||
|
# - .env
|
||||||
|
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||||
|
# volumes:
|
||||||
|
# - ep_bayer_data:/opt/enviPy/
|
||||||
|
|
||||||
|
celery_worker:
|
||||||
|
image: envipath/envipy-bayer:1.0
|
||||||
|
container_name: epcelery
|
||||||
|
env_file:
|
||||||
|
- .env.dev
|
||||||
|
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_data:/opt/enviPy/
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
ep_bayer_postgres_data:
|
||||||
|
ep_bayer_redis_data:
|
||||||
|
ep_bayer_data:
|
||||||
|
|||||||
50
docker-compose.yml
Normal file
50
docker-compose.yml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:18
|
||||||
|
container_name: eppostgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_postgres_data:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: epredis
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_redis_data:/data
|
||||||
|
|
||||||
|
biotransformer3:
|
||||||
|
image: envipath/biotransformer3:1.0
|
||||||
|
container_name: epbiotransformer3
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: envipath/envipy-bayer:1.0
|
||||||
|
container_name: epdjango
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_data:/opt/enviPy/
|
||||||
|
|
||||||
|
celery_worker:
|
||||||
|
image: envipath/envipy-bayer:1.0
|
||||||
|
container_name: epcelery
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||||
|
volumes:
|
||||||
|
- ep_bayer_data:/opt/enviPy/
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ep_bayer_postgres_data:
|
||||||
|
ep_bayer_redis_data:
|
||||||
|
ep_bayer_data:
|
||||||
@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
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")
|
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev")
|
||||||
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)
|
||||||
|
|
||||||
@ -37,7 +37,6 @@ ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
|
|||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
@ -45,6 +44,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.postgres",
|
||||||
# 3rd party
|
# 3rd party
|
||||||
"django_extensions",
|
"django_extensions",
|
||||||
"oauth2_provider",
|
"oauth2_provider",
|
||||||
@ -74,6 +74,7 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
@ -92,10 +93,19 @@ if os.environ.get("REGISTRATION_MANDATORY", False) == "True":
|
|||||||
|
|
||||||
ROOT_URLCONF = "envipath.urls"
|
ROOT_URLCONF = "envipath.urls"
|
||||||
|
|
||||||
|
TEMPLATE_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "templates"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# If we have a non-public tenant, we might need to overwrite some templates
|
||||||
|
# search TENANT folder first...
|
||||||
|
if TENANT != "public":
|
||||||
|
TEMPLATE_DIRS.insert(0, os.path.join(BASE_DIR, TENANT, "templates"))
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": (os.path.join(BASE_DIR, "templates"),),
|
"DIRS": TEMPLATE_DIRS,
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
@ -133,6 +143,12 @@ 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
|
||||||
@ -181,11 +197,21 @@ ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") ==
|
|||||||
# SESAME_MAX_AGE = 300
|
# SESAME_MAX_AGE = 300
|
||||||
# # TODO set to "home"
|
# # TODO set to "home"
|
||||||
# LOGIN_REDIRECT_URL = "/"
|
# LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_HOST = os.environ.get("SERVER_URL", "http://localhost:8000")
|
||||||
|
SERVER_PATH = os.environ.get("SERVER_PATH", "")
|
||||||
|
|
||||||
|
SERVER_URL = SERVER_HOST
|
||||||
|
if SERVER_PATH:
|
||||||
|
SERVER_URL = os.path.join(SERVER_HOST, SERVER_PATH)
|
||||||
|
|
||||||
|
|
||||||
LOGIN_URL = "/login/"
|
LOGIN_URL = "/login/"
|
||||||
|
if SERVER_PATH:
|
||||||
|
LOGIN_URL = f"/{SERVER_PATH}/login/"
|
||||||
|
|
||||||
SERVER_URL = os.environ.get("SERVER_URL", "http://localhost:8000")
|
CSRF_TRUSTED_ORIGINS = [SERVER_HOST]
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [SERVER_URL]
|
|
||||||
|
|
||||||
AMBIT_URL = "http://localhost:9001"
|
AMBIT_URL = "http://localhost:9001"
|
||||||
DEFAULT_VALUES = {"description": "no description"}
|
DEFAULT_VALUES = {"description": "no description"}
|
||||||
@ -207,19 +233,20 @@ if not os.path.exists(LOG_DIR):
|
|||||||
os.mkdir(LOG_DIR)
|
os.mkdir(LOG_DIR)
|
||||||
|
|
||||||
PLUGIN_DIR = os.path.join(EP_DATA_DIR, "plugins")
|
PLUGIN_DIR = os.path.join(EP_DATA_DIR, "plugins")
|
||||||
|
if not os.path.exists(PLUGIN_DIR):
|
||||||
|
os.mkdir(PLUGIN_DIR)
|
||||||
|
|
||||||
API_PAGINATION_DEFAULT_PAGE_SIZE = int(os.environ.get("API_PAGINATION_DEFAULT_PAGE_SIZE", 50))
|
API_PAGINATION_DEFAULT_PAGE_SIZE = int(os.environ.get("API_PAGINATION_DEFAULT_PAGE_SIZE", 50))
|
||||||
PAGINATION_MAX_PER_PAGE_SIZE = int(
|
PAGINATION_MAX_PER_PAGE_SIZE = int(
|
||||||
os.environ.get("API_PAGINATION_MAX_PAGE_SIZE", 100)
|
os.environ.get("API_PAGINATION_MAX_PAGE_SIZE", 100)
|
||||||
) # Ninja override
|
) # Ninja override
|
||||||
|
|
||||||
if not os.path.exists(PLUGIN_DIR):
|
|
||||||
os.mkdir(PLUGIN_DIR)
|
|
||||||
|
|
||||||
# Set this as our static root dir
|
# Set this as our static root dir
|
||||||
STATIC_ROOT = STATIC_DIR
|
STATIC_ROOT = STATIC_DIR
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
if SERVER_PATH:
|
||||||
|
STATIC_URL = f"/{SERVER_PATH}/static/"
|
||||||
|
|
||||||
# Where the sources are stored...
|
# Where the sources are stored...
|
||||||
STATICFILES_DIRS = (BASE_DIR / "static",)
|
STATICFILES_DIRS = (BASE_DIR / "static",)
|
||||||
@ -283,9 +310,8 @@ if not FLAG_CELERY_PRESENT:
|
|||||||
|
|
||||||
# Celery Configuration Options
|
# Celery Configuration Options
|
||||||
CELERY_TIMEZONE = "Europe/Berlin"
|
CELERY_TIMEZONE = "Europe/Berlin"
|
||||||
# Celery Configuration
|
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||||
CELERY_BROKER_URL = "redis://localhost:6379/0" # Use Redis as message broker
|
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")
|
||||||
CELERY_RESULT_BACKEND = "redis://localhost:6379/1"
|
|
||||||
CELERY_ACCEPT_CONTENT = ["json"]
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
CELERY_TASK_SERIALIZER = "json"
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
|
|
||||||
@ -323,9 +349,11 @@ DEFAULT_MODEL_THRESHOLD = 0.25
|
|||||||
|
|
||||||
# Loading Plugins
|
# Loading Plugins
|
||||||
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
|
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
|
||||||
BASE_PLUGINS = [
|
BASE_PLUGINS = os.environ.get("BASE_PLUGINS", None)
|
||||||
"pepper.PEPPER",
|
if BASE_PLUGINS:
|
||||||
]
|
BASE_PLUGINS = BASE_PLUGINS.split(",")
|
||||||
|
else:
|
||||||
|
BASE_PLUGINS = []
|
||||||
|
|
||||||
CLASSIFIER_PLUGINS = {}
|
CLASSIFIER_PLUGINS = {}
|
||||||
PROPERTY_PLUGINS = {}
|
PROPERTY_PLUGINS = {}
|
||||||
@ -353,6 +381,10 @@ if SENTRY_ENABLED:
|
|||||||
before_send=before_send,
|
before_send=before_send,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IUCLID_EXPORT_ENABLED = os.environ.get("IUCLID_EXPORT_ENABLED", "False") == "True"
|
||||||
|
if IUCLID_EXPORT_ENABLED:
|
||||||
|
INSTALLED_APPS.append("epiuclid")
|
||||||
|
|
||||||
# compile into digestible flags
|
# compile into digestible flags
|
||||||
FLAGS = {
|
FLAGS = {
|
||||||
"MODEL_BUILDING": MODEL_BUILDING_ENABLED,
|
"MODEL_BUILDING": MODEL_BUILDING_ENABLED,
|
||||||
@ -361,6 +393,7 @@ FLAGS = {
|
|||||||
"SENTRY": SENTRY_ENABLED,
|
"SENTRY": SENTRY_ENABLED,
|
||||||
"ENVIFORMER": ENVIFORMER_PRESENT,
|
"ENVIFORMER": ENVIFORMER_PRESENT,
|
||||||
"APPLICABILITY_DOMAIN": APPLICABILITY_DOMAIN_ENABLED,
|
"APPLICABILITY_DOMAIN": APPLICABILITY_DOMAIN_ENABLED,
|
||||||
|
"IUCLID_EXPORT": IUCLID_EXPORT_ENABLED,
|
||||||
}
|
}
|
||||||
|
|
||||||
# path of the URL are checked via "startswith"
|
# path of the URL are checked via "startswith"
|
||||||
@ -373,7 +406,6 @@ LOGIN_EXEMPT_URLS = [
|
|||||||
"/o/userinfo/",
|
"/o/userinfo/",
|
||||||
"/password_reset/",
|
"/password_reset/",
|
||||||
"/reset/",
|
"/reset/",
|
||||||
"/microsoft/",
|
|
||||||
"/terms",
|
"/terms",
|
||||||
"/privacy",
|
"/privacy",
|
||||||
"/cookie-policy",
|
"/cookie-policy",
|
||||||
@ -382,8 +414,13 @@ LOGIN_EXEMPT_URLS = [
|
|||||||
"/careers",
|
"/careers",
|
||||||
"/cite",
|
"/cite",
|
||||||
"/legal",
|
"/legal",
|
||||||
|
"/entra/",
|
||||||
|
"/auth/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if SERVER_PATH:
|
||||||
|
LOGIN_EXEMPT_URLS = [f"/{SERVER_PATH}{x}" for x in LOGIN_EXEMPT_URLS]
|
||||||
|
|
||||||
# MS AD/Entra
|
# MS AD/Entra
|
||||||
MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True"
|
MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True"
|
||||||
if MS_ENTRA_ENABLED:
|
if MS_ENTRA_ENABLED:
|
||||||
@ -399,3 +436,58 @@ if MS_ENTRA_ENABLED:
|
|||||||
|
|
||||||
# Site ID 10 -> beta.envipath.org
|
# Site ID 10 -> beta.envipath.org
|
||||||
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
||||||
|
|
||||||
|
# CAP
|
||||||
|
CAP_ENABLED = os.environ.get("CAP_ENABLED", "False") == "True"
|
||||||
|
CAP_API_BASE = os.environ.get("CAP_API_BASE", None)
|
||||||
|
CAP_SITE_KEY = os.environ.get("CAP_SITE_KEY", None)
|
||||||
|
CAP_SECRET_KEY = os.environ.get("CAP_SECRET_KEY", None)
|
||||||
|
|
||||||
|
# Biotransformer
|
||||||
|
BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "True"
|
||||||
|
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
||||||
|
if BIOTRANSFORMER_ENABLED:
|
||||||
|
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
||||||
|
|
||||||
|
# PES
|
||||||
|
PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None)
|
||||||
|
if PES_API_MAPPING:
|
||||||
|
import json
|
||||||
|
PES_API_MAPPING = json.loads(PES_API_MAPPING)
|
||||||
|
else:
|
||||||
|
PES_API_MAPPING = {}
|
||||||
|
|
||||||
|
# Entra Groups
|
||||||
|
ENTRA_GROUPS = os.environ.get("ENTRA_GROUPS", None)
|
||||||
|
if ENTRA_GROUPS:
|
||||||
|
import json
|
||||||
|
ENTRA_GROUPS = json.loads(ENTRA_GROUPS)
|
||||||
|
else:
|
||||||
|
ENTRA_GROUPS = {}
|
||||||
|
|
||||||
|
ENTRA_SECRET_GROUPS = os.environ.get("ENTRA_SECRET_GROUPS", None)
|
||||||
|
if ENTRA_SECRET_GROUPS:
|
||||||
|
import json
|
||||||
|
ENTRA_SECRET_GROUPS = json.loads(ENTRA_SECRET_GROUPS)
|
||||||
|
else:
|
||||||
|
ENTRA_SECRET_GROUPS = {}
|
||||||
|
|
||||||
|
# PES Data Pools vs Entra Mapping
|
||||||
|
DATA_POOL_MAPPING = os.environ.get("DATA_POOL_MAPPING", None)
|
||||||
|
if DATA_POOL_MAPPING:
|
||||||
|
import json
|
||||||
|
DATA_POOL_MAPPING = json.loads(DATA_POOL_MAPPING)
|
||||||
|
else:
|
||||||
|
DATA_POOL_MAPPING = {}
|
||||||
|
|
||||||
|
PROXIES = {}
|
||||||
|
if os.environ.get("HTTP_PROXY"):
|
||||||
|
PROXIES["http"] = os.environ.get("HTTP_PROXY")
|
||||||
|
PROXIES["https"] = os.environ.get("HTTPS_PROXY")
|
||||||
|
|
||||||
|
# BB4g
|
||||||
|
BB4G_URL = os.environ.get("BB4G_URL")
|
||||||
|
BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
|
||||||
|
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
|
||||||
|
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
||||||
|
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")
|
||||||
|
|||||||
@ -21,19 +21,27 @@ from django.urls import include, path
|
|||||||
|
|
||||||
from .api import api_v1, api_legacy
|
from .api import api_v1, api_legacy
|
||||||
|
|
||||||
|
PATH_PREFIX = s.SERVER_PATH
|
||||||
|
if PATH_PREFIX and not PATH_PREFIX.endswith("/"):
|
||||||
|
PATH_PREFIX += "/"
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include("epdb.urls")),
|
path(f"{PATH_PREFIX}", include("epdb.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path(f"{PATH_PREFIX}admin/", admin.site.urls),
|
||||||
path("api/v1/", api_v1.urls),
|
path(f"{PATH_PREFIX}api/v1/", api_v1.urls),
|
||||||
path("api/legacy/", api_legacy.urls),
|
path(f"{PATH_PREFIX}api/legacy/", api_legacy.urls),
|
||||||
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
path(f"{PATH_PREFIX}o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if "migration" in s.INSTALLED_APPS:
|
if "migration" in s.INSTALLED_APPS:
|
||||||
urlpatterns.append(path("", include("migration.urls")))
|
urlpatterns.append(path(f"{PATH_PREFIX}", include("migration.urls")))
|
||||||
|
|
||||||
if s.MS_ENTRA_ENABLED:
|
if s.MS_ENTRA_ENABLED:
|
||||||
urlpatterns.append(path("", include("epauth.urls")))
|
urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
|
||||||
|
|
||||||
|
if s.TENANT != "public":
|
||||||
|
urlpatterns.append(path(f"{PATH_PREFIX}", include(f"{s.TENANT}.urls")))
|
||||||
|
|
||||||
# Custom error handlers
|
# Custom error handlers
|
||||||
handler400 = "epdb.views.handler400"
|
handler400 = "epdb.views.handler400"
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class ScenarioCreationAPITests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
self.assertIn("Package not found", response.json()["detail"])
|
self.assertIn(f"Package with UUID {fake_uuid} not found", response.json()["detail"])
|
||||||
|
|
||||||
def test_create_scenario_insufficient_permissions(self):
|
def test_create_scenario_insufficient_permissions(self):
|
||||||
"""Test that unauthorized access returns 403."""
|
"""Test that unauthorized access returns 403."""
|
||||||
|
|||||||
@ -74,7 +74,6 @@ class TestSchemaGeneration:
|
|||||||
assert all(isinstance(g, str) for g in groups), (
|
assert all(isinstance(g, str) for g in groups), (
|
||||||
f"{model_name}: all groups should be strings, got {groups}"
|
f"{model_name}: all groups should be strings, got {groups}"
|
||||||
)
|
)
|
||||||
assert len(groups) > 0, f"{model_name}: should have at least one group"
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
|
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
|
||||||
def test_form_data_matches_schema(self, model_name: str, model_cls: Type[BaseModel]):
|
def test_form_data_matches_schema(self, model_name: str, model_cls: Type[BaseModel]):
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
from django.db.models import Model
|
|
||||||
from epdb.logic import PackageManager
|
|
||||||
from epdb.models import CompoundStructure, User, Package, Compound, Scenario
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.db.models import Model
|
||||||
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import CompoundStructure, User, Compound, Scenario
|
||||||
|
|
||||||
from .errors import EPAPINotFoundError, EPAPIPermissionDeniedError
|
from .errors import EPAPINotFoundError, EPAPIPermissionDeniedError
|
||||||
|
|
||||||
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
def get_compound_for_read(user, compound_uuid: UUID):
|
def get_compound_for_read(user, compound_uuid: UUID):
|
||||||
"""
|
"""
|
||||||
@ -41,6 +45,24 @@ def get_package_for_read(user, package_uuid: UUID):
|
|||||||
return package
|
return package
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_for_write(user, package_uuid: UUID):
|
||||||
|
"""
|
||||||
|
Get package by UUID with permission check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# FIXME: update package manager with custom exceptions to avoid manual checks here
|
||||||
|
try:
|
||||||
|
package = Package.objects.get(uuid=package_uuid)
|
||||||
|
except Package.DoesNotExist:
|
||||||
|
raise EPAPINotFoundError(f"Package with UUID {package_uuid} not found")
|
||||||
|
|
||||||
|
# FIXME: optimize package manager to exclusively work with UUIDs
|
||||||
|
if not user or user.is_anonymous or not PackageManager.writable(user, package):
|
||||||
|
raise EPAPIPermissionDeniedError("Insufficient permissions to access this package.")
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
|
|
||||||
def get_scenario_for_read(user, scenario_uuid: UUID):
|
def get_scenario_for_read(user, scenario_uuid: UUID):
|
||||||
"""Get scenario by UUID with read permission check."""
|
"""Get scenario by UUID with read permission check."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
23
epapi/v1/endpoints/groups.py
Normal file
23
epapi/v1/endpoints/groups.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.conf import settings as s
|
||||||
|
from ninja import Router
|
||||||
|
from ninja_extra.pagination import paginate
|
||||||
|
|
||||||
|
from epdb.logic import GroupManager
|
||||||
|
|
||||||
|
from ..pagination import EnhancedPageNumberPagination
|
||||||
|
from ..schemas import GroupOutSchema
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/groups/", response=EnhancedPageNumberPagination.Output[GroupOutSchema])
|
||||||
|
@paginate(
|
||||||
|
EnhancedPageNumberPagination,
|
||||||
|
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||||
|
)
|
||||||
|
def list_all_groups(request):
|
||||||
|
"""
|
||||||
|
List all groups the user has access to.
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
return GroupManager.get_groups(user)
|
||||||
@ -9,7 +9,6 @@ import logging
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from epdb.models import Scenario
|
from epdb.models import Scenario
|
||||||
from epdb.logic import PackageManager
|
|
||||||
from epdb.views import _anonymous_or_real
|
from epdb.views import _anonymous_or_real
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
from ..pagination import EnhancedPageNumberPagination
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
@ -17,7 +16,7 @@ from ..schemas import (
|
|||||||
ScenarioOutSchema,
|
ScenarioOutSchema,
|
||||||
ScenarioCreateSchema,
|
ScenarioCreateSchema,
|
||||||
)
|
)
|
||||||
from ..dal import get_user_entities_for_read, get_package_entities_for_read
|
from ..dal import get_user_entities_for_read, get_package_entities_for_read, get_package_for_write
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -58,7 +57,7 @@ def create_scenario(request, package_uuid: UUID, payload: ScenarioCreateSchema =
|
|||||||
user = _anonymous_or_real(request)
|
user = _anonymous_or_real(request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_package = PackageManager.get_package_by_id(user, package_uuid)
|
current_package = get_package_for_write(user, package_uuid)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
if "does not exist" in error_msg:
|
if "does not exist" in error_msg:
|
||||||
|
|||||||
@ -15,9 +15,9 @@ router = Router()
|
|||||||
EnhancedPageNumberPagination,
|
EnhancedPageNumberPagination,
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||||
)
|
)
|
||||||
def list_all_pathways(request):
|
def list_all_settings(request):
|
||||||
"""
|
"""
|
||||||
List all pathways from reviewed packages.
|
List all settings the user has access to.
|
||||||
"""
|
"""
|
||||||
user = request.user
|
user = request.user
|
||||||
return SettingManager.get_all_settings(user)
|
return SettingManager.get_all_settings(user)
|
||||||
|
|||||||
3
epapi/v1/interfaces/__init__.py
Normal file
3
epapi/v1/interfaces/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Service interfaces: each subdirectory defines the full boundary contract between enviPy and feature-flagged apps. DTOs and projections are shared concerns to avoid direct ORM access.
|
||||||
|
"""
|
||||||
0
epapi/v1/interfaces/iuclid/__init__.py
Normal file
0
epapi/v1/interfaces/iuclid/__init__.py
Normal file
58
epapi/v1/interfaces/iuclid/dto.py
Normal file
58
epapi/v1/interfaces/iuclid/dto.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayCompoundDTO:
|
||||||
|
pk: int
|
||||||
|
name: str
|
||||||
|
smiles: str | None = None
|
||||||
|
cas_number: str | None = None
|
||||||
|
ec_number: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayScenarioDTO:
|
||||||
|
scenario_uuid: UUID
|
||||||
|
name: str
|
||||||
|
additional_info: list = field(default_factory=list) # EnviPyModel instances
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayNodeDTO:
|
||||||
|
node_uuid: UUID
|
||||||
|
compound_pk: int
|
||||||
|
name: str
|
||||||
|
depth: int
|
||||||
|
smiles: str | None = None
|
||||||
|
cas_number: str | None = None
|
||||||
|
ec_number: str | None = None
|
||||||
|
additional_info: list = field(default_factory=list) # EnviPyModel instances
|
||||||
|
scenarios: list[PathwayScenarioDTO] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayEdgeDTO:
|
||||||
|
edge_uuid: UUID
|
||||||
|
start_compound_pks: list[int] = field(default_factory=list)
|
||||||
|
end_compound_pks: list[int] = field(default_factory=list)
|
||||||
|
probability: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayModelInfoDTO:
|
||||||
|
model_name: str | None = None
|
||||||
|
model_uuid: UUID | None = None
|
||||||
|
software_name: str | None = None
|
||||||
|
software_version: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PathwayExportDTO:
|
||||||
|
pathway_uuid: UUID
|
||||||
|
pathway_name: str
|
||||||
|
compounds: list[PathwayCompoundDTO] = field(default_factory=list)
|
||||||
|
nodes: list[PathwayNodeDTO] = field(default_factory=list)
|
||||||
|
edges: list[PathwayEdgeDTO] = field(default_factory=list)
|
||||||
|
root_compound_pks: list[int] = field(default_factory=list)
|
||||||
|
model_info: PathwayModelInfoDTO | None = None
|
||||||
142
epapi/v1/interfaces/iuclid/projections.py
Normal file
142
epapi/v1/interfaces/iuclid/projections.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import Pathway
|
||||||
|
|
||||||
|
from epapi.v1.errors import EPAPINotFoundError, EPAPIPermissionDeniedError
|
||||||
|
|
||||||
|
from .dto import (
|
||||||
|
PathwayCompoundDTO,
|
||||||
|
PathwayEdgeDTO,
|
||||||
|
PathwayExportDTO,
|
||||||
|
PathwayModelInfoDTO,
|
||||||
|
PathwayNodeDTO,
|
||||||
|
PathwayScenarioDTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pathway_for_iuclid_export(user, pathway_uuid: UUID) -> PathwayExportDTO:
|
||||||
|
"""Return pathway data projected into DTOs for the IUCLID export consumer."""
|
||||||
|
try:
|
||||||
|
pathway = (
|
||||||
|
Pathway.objects.select_related("package", "setting", "setting__model")
|
||||||
|
.prefetch_related(
|
||||||
|
"node_set__default_node_label__compound__external_identifiers__database",
|
||||||
|
"node_set__scenarios",
|
||||||
|
"edge_set__start_nodes__default_node_label__compound",
|
||||||
|
"edge_set__end_nodes__default_node_label__compound",
|
||||||
|
)
|
||||||
|
.get(uuid=pathway_uuid)
|
||||||
|
)
|
||||||
|
except Pathway.DoesNotExist:
|
||||||
|
raise EPAPINotFoundError(f"Pathway with UUID {pathway_uuid} not found")
|
||||||
|
|
||||||
|
if not user or user.is_anonymous or not PackageManager.readable(user, pathway.package):
|
||||||
|
raise EPAPIPermissionDeniedError("Insufficient permissions to access this pathway.")
|
||||||
|
|
||||||
|
nodes: list[PathwayNodeDTO] = []
|
||||||
|
edges: list[PathwayEdgeDTO] = []
|
||||||
|
compounds_by_pk: dict[int, PathwayCompoundDTO] = {}
|
||||||
|
root_compound_pks: list[int] = []
|
||||||
|
|
||||||
|
for node in pathway.node_set.all().order_by("depth", "pk"):
|
||||||
|
cs = node.default_node_label
|
||||||
|
if cs is None:
|
||||||
|
continue
|
||||||
|
compound = cs.compound
|
||||||
|
|
||||||
|
cas_number = None
|
||||||
|
ec_number = None
|
||||||
|
for ext_id in compound.external_identifiers.all():
|
||||||
|
db_name = ext_id.database.name if ext_id.database else None
|
||||||
|
if db_name == "CAS" and cas_number is None:
|
||||||
|
cas_number = ext_id.identifier_value
|
||||||
|
elif db_name == "EC" and ec_number is None:
|
||||||
|
ec_number = ext_id.identifier_value
|
||||||
|
|
||||||
|
ai_for_node = []
|
||||||
|
scenario_entries: list[PathwayScenarioDTO] = []
|
||||||
|
for scenario in sorted(node.scenarios.all(), key=lambda item: item.pk):
|
||||||
|
ai_for_scenario = list(scenario.get_additional_information(direct_only=True))
|
||||||
|
ai_for_node.extend(ai_for_scenario)
|
||||||
|
scenario_entries.append(
|
||||||
|
PathwayScenarioDTO(
|
||||||
|
scenario_uuid=scenario.uuid,
|
||||||
|
name=scenario.name,
|
||||||
|
additional_info=ai_for_scenario,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes.append(
|
||||||
|
PathwayNodeDTO(
|
||||||
|
node_uuid=node.uuid,
|
||||||
|
compound_pk=compound.pk,
|
||||||
|
name=compound.name,
|
||||||
|
depth=node.depth,
|
||||||
|
smiles=cs.smiles,
|
||||||
|
cas_number=cas_number,
|
||||||
|
ec_number=ec_number,
|
||||||
|
additional_info=ai_for_node,
|
||||||
|
scenarios=scenario_entries,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if node.depth == 0 and compound.pk not in root_compound_pks:
|
||||||
|
root_compound_pks.append(compound.pk)
|
||||||
|
|
||||||
|
if compound.pk not in compounds_by_pk:
|
||||||
|
compounds_by_pk[compound.pk] = PathwayCompoundDTO(
|
||||||
|
pk=compound.pk,
|
||||||
|
name=compound.name,
|
||||||
|
smiles=cs.smiles,
|
||||||
|
cas_number=cas_number,
|
||||||
|
ec_number=ec_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
for edge in pathway.edge_set.all():
|
||||||
|
start_compounds = {
|
||||||
|
n.default_node_label.compound.pk
|
||||||
|
for n in edge.start_nodes.all()
|
||||||
|
if n.default_node_label is not None
|
||||||
|
}
|
||||||
|
end_compounds = {
|
||||||
|
n.default_node_label.compound.pk
|
||||||
|
for n in edge.end_nodes.all()
|
||||||
|
if n.default_node_label is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
probability = None
|
||||||
|
if edge.kv and edge.kv.get("probability") is not None:
|
||||||
|
try:
|
||||||
|
probability = float(edge.kv.get("probability"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
probability = None
|
||||||
|
|
||||||
|
edges.append(
|
||||||
|
PathwayEdgeDTO(
|
||||||
|
edge_uuid=edge.uuid,
|
||||||
|
start_compound_pks=sorted(start_compounds),
|
||||||
|
end_compound_pks=sorted(end_compounds),
|
||||||
|
probability=probability,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
model_info = None
|
||||||
|
if pathway.setting and pathway.setting.model:
|
||||||
|
model = pathway.setting.model
|
||||||
|
model_info = PathwayModelInfoDTO(
|
||||||
|
model_name=model.get_name(),
|
||||||
|
model_uuid=model.uuid,
|
||||||
|
software_name="enviPath",
|
||||||
|
software_version=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return PathwayExportDTO(
|
||||||
|
pathway_uuid=pathway.uuid,
|
||||||
|
pathway_name=pathway.get_name(),
|
||||||
|
compounds=list(compounds_by_pk.values()),
|
||||||
|
nodes=nodes,
|
||||||
|
edges=edges,
|
||||||
|
root_compound_pks=root_compound_pks,
|
||||||
|
model_info=model_info,
|
||||||
|
)
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from ninja import Router
|
from ninja import Router
|
||||||
from ninja.security import SessionAuth
|
from ninja.security import SessionAuth
|
||||||
|
|
||||||
|
from envipath import settings as s
|
||||||
from .auth import BearerTokenAuth
|
from .auth import BearerTokenAuth
|
||||||
from .endpoints import (
|
from .endpoints import (
|
||||||
packages,
|
packages,
|
||||||
@ -13,6 +14,7 @@ from .endpoints import (
|
|||||||
structure,
|
structure,
|
||||||
additional_information,
|
additional_information,
|
||||||
settings,
|
settings,
|
||||||
|
groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main router with authentication
|
# Main router with authentication
|
||||||
@ -34,3 +36,9 @@ router.add_router("", models.router)
|
|||||||
router.add_router("", structure.router)
|
router.add_router("", structure.router)
|
||||||
router.add_router("", additional_information.router)
|
router.add_router("", additional_information.router)
|
||||||
router.add_router("", settings.router)
|
router.add_router("", settings.router)
|
||||||
|
router.add_router("", groups.router)
|
||||||
|
|
||||||
|
if s.IUCLID_EXPORT_ENABLED:
|
||||||
|
from epiuclid.api import router as iuclid_router
|
||||||
|
|
||||||
|
router.add_router("", iuclid_router)
|
||||||
|
|||||||
@ -126,3 +126,10 @@ class SettingOutSchema(Schema):
|
|||||||
url: str = ""
|
url: str = ""
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class GroupOutSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
url: str = ""
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("microsoft/login/", views.microsoft_login, name="microsoft_login"),
|
path("entra/login/", views.entra_login, name="entra_login"),
|
||||||
path("microsoft/callback/", views.microsoft_callback, name="microsoft_callback"),
|
path("auth/redirect/", views.entra_callback, name="entra_callback"),
|
||||||
|
path("auth/token/", views.get_token, name="get_token"),
|
||||||
]
|
]
|
||||||
|
|||||||
160
epauth/views.py
160
epauth/views.py
@ -1,34 +1,51 @@
|
|||||||
import msal
|
import msal
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from epdb.logic import UserManager
|
from epdb.logic import UserManager, GroupManager
|
||||||
|
from epdb.models import Group
|
||||||
|
|
||||||
|
|
||||||
def microsoft_login(request):
|
def get_msal_app_with_cache(request):
|
||||||
|
"""
|
||||||
|
Create MSAL app with session-based token cache.
|
||||||
|
"""
|
||||||
|
cache = msal.SerializableTokenCache()
|
||||||
|
|
||||||
|
# Load cache from session if it exists
|
||||||
|
if request.session.get("msal_token_cache"):
|
||||||
|
cache.deserialize(request.session["msal_token_cache"])
|
||||||
|
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
client_id=s.MS_ENTRA_CLIENT_ID,
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
||||||
authority=s.MS_ENTRA_AUTHORITY
|
authority=s.MS_ENTRA_AUTHORITY,
|
||||||
|
token_cache=cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
return msal_app, cache
|
||||||
|
|
||||||
|
|
||||||
|
def entra_login(request):
|
||||||
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
|
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
||||||
|
authority=s.MS_ENTRA_AUTHORITY,
|
||||||
)
|
)
|
||||||
|
|
||||||
flow = msal_app.initiate_auth_code_flow(
|
flow = msal_app.initiate_auth_code_flow(
|
||||||
scopes=s.MS_ENTRA_SCOPES,
|
scopes=s.MS_ENTRA_SCOPES, redirect_uri=s.MS_ENTRA_REDIRECT_URI
|
||||||
redirect_uri=s.MS_ENTRA_REDIRECT_URI
|
|
||||||
)
|
)
|
||||||
|
|
||||||
request.session["msal_auth_flow"] = flow
|
request.session["msal_auth_flow"] = flow
|
||||||
return redirect(flow["auth_uri"])
|
return redirect(flow["auth_uri"])
|
||||||
|
|
||||||
|
|
||||||
def microsoft_callback(request):
|
def entra_callback(request):
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app, cache = get_msal_app_with_cache(request)
|
||||||
client_id=s.MS_ENTRA_CLIENT_ID,
|
|
||||||
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
|
||||||
authority=s.MS_ENTRA_AUTHORITY
|
|
||||||
)
|
|
||||||
|
|
||||||
flow = request.session.pop("msal_auth_flow", None)
|
flow = request.session.pop("msal_auth_flow", None)
|
||||||
if not flow:
|
if not flow:
|
||||||
@ -37,30 +54,117 @@ def microsoft_callback(request):
|
|||||||
# Acquire token using the flow and callback request
|
# Acquire token using the flow and callback request
|
||||||
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
|
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
|
||||||
|
|
||||||
if "access_token" in result:
|
# Save the token cache to session
|
||||||
# Optional: Fetch user info from Microsoft Graph
|
if cache.has_state_changed:
|
||||||
import requests
|
request.session["msal_token_cache"] = cache.serialize()
|
||||||
resp = requests.get(
|
|
||||||
"https://graph.microsoft.com/v1.0/me",
|
|
||||||
headers={"Authorization": f"Bearer {result['access_token']}"}
|
|
||||||
)
|
|
||||||
user_info = resp.json()
|
|
||||||
|
|
||||||
user_name = user_info["displayName"]
|
claims = result["id_token_claims"]
|
||||||
user_email = user_info["mail"]
|
|
||||||
user_oid = user_info["id"]
|
user_name = claims.get("name")
|
||||||
|
user_email = claims.get("emailaddress", claims.get("email"))
|
||||||
|
user_oid = claims.get("oid")
|
||||||
|
|
||||||
|
if not all([user_name, user_email, user_oid]):
|
||||||
|
raise ValueError("Missing required claims in ID token")
|
||||||
|
|
||||||
# Get implementing class
|
# Get implementing class
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
if User.objects.filter(uuid=user_oid).exists():
|
if User.objects.filter(uuid=user_oid).exists():
|
||||||
login(request, User.objects.get(uuid=user_oid))
|
u = User.objects.get(uuid=user_oid)
|
||||||
|
|
||||||
|
if u.username != user_name:
|
||||||
|
u.username = user_name
|
||||||
|
u.save()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
|
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
|
||||||
|
|
||||||
login(request, u)
|
login(request, u)
|
||||||
|
|
||||||
# TODO Group Sync
|
# EDIT START
|
||||||
|
|
||||||
return redirect("/")
|
# 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()
|
||||||
|
|
||||||
return redirect("/") # Handle errors
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def get_access_token_from_request(request, scopes=None):
|
||||||
|
"""
|
||||||
|
Get an access token from the request using MSAL token cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if auth via Access Token
|
||||||
|
if request.headers.get("Authorization"):
|
||||||
|
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
|
||||||
|
|
||||||
|
if scopes is None:
|
||||||
|
scopes = s.MS_ENTRA_SCOPES
|
||||||
|
|
||||||
|
# Get user from request (must be authenticated)
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create MSAL app with persistent cache
|
||||||
|
msal_app, cache = get_msal_app_with_cache(request)
|
||||||
|
|
||||||
|
# Try to get accounts from cache
|
||||||
|
accounts = msal_app.get_accounts()
|
||||||
|
|
||||||
|
if not accounts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the account that matches the current user
|
||||||
|
user_account = None
|
||||||
|
for account in accounts:
|
||||||
|
if account.get("local_account_id") == str(request.user.uuid):
|
||||||
|
user_account = account
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no matching account found, use the first available account
|
||||||
|
if not user_account and accounts:
|
||||||
|
user_account = accounts[0]
|
||||||
|
|
||||||
|
if not user_account:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try to acquire token silently from cache
|
||||||
|
result = msal_app.acquire_token_silent(scopes=scopes, account=user_account)
|
||||||
|
|
||||||
|
# Save cache changes back to session
|
||||||
|
if cache.has_state_changed:
|
||||||
|
request.session["msal_token_cache"] = cache.serialize()
|
||||||
|
|
||||||
|
if result and "access_token" in result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(request):
|
||||||
|
token = get_access_token_from_request(request)
|
||||||
|
msg = f"{token}"
|
||||||
|
return HttpResponse(msg, content_type='text/plain')
|
||||||
|
|||||||
118
epdb/admin.py
118
epdb/admin.py
@ -1,8 +1,12 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
|
ClassifierPluginModel,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
@ -28,6 +32,8 @@ from .models import (
|
|||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
@ -44,6 +50,113 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
"date_joined",
|
"date_joined",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
actions = ["send_welcome_mail", "send_affiliation_mail"]
|
||||||
|
|
||||||
|
@admin.action(description="Send welcome mail")
|
||||||
|
def send_welcome_mail(self, request, queryset):
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
tpl = """Hello {username},
|
||||||
|
|
||||||
|
Your account has been successfully activated.
|
||||||
|
|
||||||
|
To log in, please visit
|
||||||
|
https://envipath.org/password_reset/
|
||||||
|
and request a new password.
|
||||||
|
|
||||||
|
If you have any questions or feedback, feel free to visit our community forum at
|
||||||
|
https://community.envipath.org/.
|
||||||
|
You do not need to register again for the forum - you can log in using your enviPath account by clicking "Log In" and then "Log in with enviPath."
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The enviPath Team"""
|
||||||
|
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for user in queryset:
|
||||||
|
if user.is_active:
|
||||||
|
logger.info(f"{user.username} already active - not sending mail again")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
"Your enviPath Account Is Now Active",
|
||||||
|
tpl.format(username=user.username),
|
||||||
|
"admin@envipath.org",
|
||||||
|
[user.email],
|
||||||
|
bcc=["admin@envipath.org"],
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
user.password = "ASDF"
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
users.append(user)
|
||||||
|
logger.info(f"{user.username} -> {user.email} mail sent")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request, f"Sent welcome mail to {[u.email for u in users]}", messages.SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.action(description="Send affiliation mail")
|
||||||
|
def send_affiliation_mail(self, request, queryset):
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
tpl = """Dear {username},
|
||||||
|
|
||||||
|
Thank you for your interest in enviPath!
|
||||||
|
|
||||||
|
Please note that the public enviPath system is intended for non-commercial use only.
|
||||||
|
We see that you registered using the email address {email}.
|
||||||
|
If possible, we kindly ask you to register using an official email address that reflects your affiliation (e.g., a university, NGO, or research organization).
|
||||||
|
|
||||||
|
If you would like us to update your account, simply reply to this email and let us know which address we should use.
|
||||||
|
We will then change it in our system, and you will receive a password reset email at the new address.
|
||||||
|
|
||||||
|
If you are registering with a company email address and are interested in commercial use, you are very welcome to book a meeting with us so we can discuss how we can best support you.
|
||||||
|
To book a meeting, please visit https://envipath.com/book
|
||||||
|
|
||||||
|
If changing to an affiliation email address is not possible, please contact us at registration@envipath.org
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
enviPath team"""
|
||||||
|
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for user in queryset:
|
||||||
|
if user.is_active or user.contacted:
|
||||||
|
logger.info(
|
||||||
|
f"{user.username} already active or already contacted - not sending mail again"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
"Regarding your enviPath registration",
|
||||||
|
tpl.format(username=user.username, email=user.email),
|
||||||
|
"admin@envipath.org",
|
||||||
|
[user.email],
|
||||||
|
bcc=["admin@envipath.org"],
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
user.contacted = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
users.append(user)
|
||||||
|
logger.info(f"{user.username} -> {user.email} affiliation mail sent")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request, f"Sent affiliation mail to {[u.email for u in users]}", messages.SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
@ -83,6 +196,10 @@ class PropertyPluginModelAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClassifierPluginModelAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LicenseAdmin(admin.ModelAdmin):
|
class LicenseAdmin(admin.ModelAdmin):
|
||||||
list_display = ["cc_string", "link", "image_link"]
|
list_display = ["cc_string", "link", "image_link"]
|
||||||
|
|
||||||
@ -146,6 +263,7 @@ admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
|
|||||||
admin.site.register(EnviFormer, EnviFormerAdmin)
|
admin.site.register(EnviFormer, EnviFormerAdmin)
|
||||||
admin.site.register(PropertyPluginModel, PropertyPluginModelAdmin)
|
admin.site.register(PropertyPluginModel, PropertyPluginModelAdmin)
|
||||||
admin.site.register(License, LicenseAdmin)
|
admin.site.register(License, LicenseAdmin)
|
||||||
|
admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin)
|
||||||
admin.site.register(Compound, CompoundAdmin)
|
admin.site.register(Compound, CompoundAdmin)
|
||||||
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
||||||
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
|
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
|
||||||
|
|||||||
@ -16,8 +16,13 @@ class EPDBConfig(AppConfig):
|
|||||||
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
||||||
logger.info(f"Using Package model: {model_name}")
|
logger.info(f"Using Package model: {model_name}")
|
||||||
|
|
||||||
|
from .autodiscovery import autodiscover
|
||||||
|
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
if settings.PLUGINS_ENABLED:
|
if settings.PLUGINS_ENABLED:
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property, Classifier
|
||||||
from utilities.plugin import discover_plugins
|
from utilities.plugin import discover_plugins
|
||||||
|
|
||||||
settings.PROPERTY_PLUGINS.update(**discover_plugins(_cls=Property))
|
settings.PROPERTY_PLUGINS.update(**discover_plugins(_cls=Property))
|
||||||
|
settings.CLASSIFIER_PLUGINS.update(**discover_plugins(_cls=Classifier))
|
||||||
|
|||||||
5
epdb/autodiscovery.py
Normal file
5
epdb/autodiscovery.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.utils.module_loading import autodiscover_modules
|
||||||
|
|
||||||
|
|
||||||
|
def autodiscover():
|
||||||
|
autodiscover_modules("epdb_hooks")
|
||||||
@ -1,18 +1,30 @@
|
|||||||
|
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 SessionAuth
|
from ninja.security import HttpBearer
|
||||||
|
|
||||||
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 GroupManager, PackageManager, SearchManager, SettingManager, UserManager
|
EPDBURLParser,
|
||||||
|
GroupManager,
|
||||||
|
PackageManager,
|
||||||
|
SearchManager,
|
||||||
|
SettingManager,
|
||||||
|
UserManager,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
|
AdditionalInformation,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
@ -37,6 +49,26 @@ 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):
|
||||||
@ -50,7 +82,52 @@ def _anonymous_or_real(request):
|
|||||||
return get_user_model().objects.get(username="anonymous")
|
return get_user_model().objects.get(username="anonymous")
|
||||||
|
|
||||||
|
|
||||||
router = Router(auth=SessionAuth(csrf=False))
|
def validate_token(token: str) -> dict:
|
||||||
|
TENANT_ID = s.MS_ENTRA_TENANT_ID
|
||||||
|
CLIENT_ID = s.MS_ENTRA_CLIENT_ID
|
||||||
|
|
||||||
|
jwks = get_cached_jwks(TENANT_ID)
|
||||||
|
|
||||||
|
header = jwt.get_unverified_header(token)
|
||||||
|
|
||||||
|
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(
|
||||||
|
next(k for k in jwks["keys"] if k["kid"] == header["kid"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle V1 and V2 tokens
|
||||||
|
try:
|
||||||
|
claims = jwt.decode(
|
||||||
|
token,
|
||||||
|
public_key,
|
||||||
|
algorithms=["RS256"],
|
||||||
|
audience=[CLIENT_ID, f"api://{CLIENT_ID}"],
|
||||||
|
issuer=[
|
||||||
|
f"https://sts.windows.net/{TENANT_ID}/",
|
||||||
|
f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Token verification failed! - {e}")
|
||||||
|
|
||||||
|
return claims
|
||||||
|
|
||||||
|
|
||||||
|
class MSBearerTokenAuth(HttpBearer):
|
||||||
|
|
||||||
|
def authenticate(self, request, token):
|
||||||
|
if token is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
claims = validate_token(token)
|
||||||
|
|
||||||
|
if not User.objects.filter(uuid=claims['oid']).exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
request.user = User.objects.get(uuid=claims['oid'])
|
||||||
|
return request.user
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(auth=MSBearerTokenAuth())
|
||||||
|
|
||||||
|
|
||||||
class Error(Schema):
|
class Error(Schema):
|
||||||
@ -94,6 +171,8 @@ class SimpleObject(Schema):
|
|||||||
return "reviewed" if obj.compound.package.reviewed else "unreviewed"
|
return "reviewed" if obj.compound.package.reviewed else "unreviewed"
|
||||||
elif isinstance(obj, Node) or isinstance(obj, Edge):
|
elif isinstance(obj, Node) or isinstance(obj, Edge):
|
||||||
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
||||||
|
elif isinstance(obj, dict) and "review_status" in obj:
|
||||||
|
return "reviewed" if obj.get("review_status") else "unreviewed"
|
||||||
else:
|
else:
|
||||||
raise ValueError("Object has no package")
|
raise ValueError("Object has no package")
|
||||||
|
|
||||||
@ -142,21 +221,6 @@ 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 #
|
||||||
@ -756,6 +820,7 @@ class CreateCompound(Schema):
|
|||||||
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")
|
||||||
@ -767,6 +832,25 @@ 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
|
# inchi is not used atm
|
||||||
|
|
||||||
|
if c.pesLink is not None:
|
||||||
|
from bayer.views import fetch_pes
|
||||||
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, c.pesLink)
|
||||||
|
except ValueError as e:
|
||||||
|
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||||
|
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
||||||
|
|
||||||
|
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||||
|
else:
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
||||||
)
|
)
|
||||||
@ -1327,7 +1411,14 @@ class ScenarioSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_collection(obj: Scenario):
|
def resolve_collection(obj: Scenario):
|
||||||
return obj.additional_information
|
res = defaultdict(list)
|
||||||
|
|
||||||
|
for ai in obj.get_additional_information(direct_only=False):
|
||||||
|
data = ai.data
|
||||||
|
data["related"] = ai.content_object.simple_json() if ai.content_object else None
|
||||||
|
res[ai.type].append(data)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: Rule):
|
def resolve_review_status(obj: Rule):
|
||||||
@ -1392,7 +1483,11 @@ def create_package_scenario(request, package_uuid):
|
|||||||
study_type = request.POST.get("type")
|
study_type = request.POST.get("type")
|
||||||
|
|
||||||
ais = []
|
ais = []
|
||||||
types = request.POST.getlist("adInfoTypes[]")
|
types = request.POST.get("adInfoTypes[]", [])
|
||||||
|
|
||||||
|
if types:
|
||||||
|
types = types.split(",")
|
||||||
|
|
||||||
for t in types:
|
for t in types:
|
||||||
ais.append(build_additional_information_from_request(request, t))
|
ais.append(build_additional_information_from_request(request, t))
|
||||||
|
|
||||||
@ -1434,6 +1529,49 @@ def delete_scenario(request, package_uuid, scenario_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/package/{uuid:package_uuid}/additional-information", response={200: str | Any, 403: Error}
|
||||||
|
)
|
||||||
|
def create_package_additional_information(request, package_uuid):
|
||||||
|
from utilities.legacy import build_additional_information_from_request
|
||||||
|
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
|
||||||
|
scen = request.POST.get("scenario")
|
||||||
|
scenario = Scenario.objects.get(package=p, url=scen)
|
||||||
|
|
||||||
|
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||||
|
attach_obj = url_parser.get_object()
|
||||||
|
|
||||||
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
|
if not attach_obj.url.startswith(p.url):
|
||||||
|
raise ValueError(
|
||||||
|
"Additional Information can only be set to objects stored in the same package!"
|
||||||
|
)
|
||||||
|
|
||||||
|
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||||
|
|
||||||
|
for t in types:
|
||||||
|
ai = build_additional_information_from_request(request, t)
|
||||||
|
|
||||||
|
AdditionalInformation.create(
|
||||||
|
p,
|
||||||
|
ai,
|
||||||
|
scenario=scenario,
|
||||||
|
content_object=attach_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO implement additional information endpoint ?
|
||||||
|
return redirect(f"{scenario.url}")
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Pathway #
|
# Pathway #
|
||||||
###########
|
###########
|
||||||
@ -1464,7 +1602,7 @@ class PathwayEdge(Schema):
|
|||||||
|
|
||||||
class PathwayNode(Schema):
|
class PathwayNode(Schema):
|
||||||
atomCount: int = Field(None, alias="atom_count")
|
atomCount: int = Field(None, alias="atom_count")
|
||||||
depth: int = Field(None, alias="depth")
|
depth: float = Field(None, alias="depth")
|
||||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
@ -1736,6 +1874,7 @@ class CreateNode(Schema):
|
|||||||
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(
|
||||||
@ -1747,14 +1886,43 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
|||||||
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)
|
||||||
|
|
||||||
|
if n.pesLink:
|
||||||
|
from bayer.views import fetch_pes
|
||||||
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, c.pesLink)
|
||||||
|
except ValueError as e:
|
||||||
|
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||||
|
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
||||||
|
|
||||||
|
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||||
|
|
||||||
|
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(n.nodeDepth)
|
||||||
else:
|
else:
|
||||||
node_depth = -1
|
node_depth = -1
|
||||||
|
|
||||||
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
||||||
|
|
||||||
return redirect(n.url)
|
return redirect(node.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding node failed!"}
|
||||||
|
|
||||||
@ -1805,7 +1973,7 @@ class EdgeSchema(Schema):
|
|||||||
startNodes: List["EdgeNode"] = Field([], alias="start_nodes")
|
startNodes: List["EdgeNode"] = Field([], alias="start_nodes")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: Node):
|
def resolve_review_status(obj: Edge):
|
||||||
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
||||||
|
|
||||||
|
|
||||||
@ -1864,13 +2032,16 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
educts = []
|
educts = []
|
||||||
products = []
|
products = []
|
||||||
|
|
||||||
|
subclasses = CompoundStructure.__subclasses__()
|
||||||
|
|
||||||
if e.edgeAsSmirks:
|
if e.edgeAsSmirks:
|
||||||
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
||||||
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
|
||||||
educts.append(
|
educts.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.get(
|
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||||
|
get(
|
||||||
compound__package=p, smiles=stand_ed
|
compound__package=p, smiles=stand_ed
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
@ -1881,7 +2052,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
products.append(
|
products.append(
|
||||||
Node.objects.get(
|
Node.objects.get(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
default_node_label=CompoundStructure.objects.get(
|
default_node_label=CompoundStructure.objects.not_instance_of(*subclasses).
|
||||||
|
get(
|
||||||
compound__package=p, smiles=stand_pr
|
compound__package=p, smiles=stand_pr
|
||||||
).compound.default_structure,
|
).compound.default_structure,
|
||||||
)
|
)
|
||||||
@ -1902,9 +2074,12 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
description=e.edgeReason,
|
description=e.edgeReason,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update depths as sideeffect of above operation
|
||||||
|
pw.update_depths()
|
||||||
|
|
||||||
return redirect(new_e.url)
|
return redirect(new_e.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding Edge failed!"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
||||||
|
|||||||
184
epdb/logic.py
184
epdb/logic.py
@ -7,6 +7,7 @@ import nh3
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import QuerySet
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
@ -264,8 +265,12 @@ class GroupManager(object):
|
|||||||
return bool(re.findall(GroupManager.group_pattern, url))
|
return bool(re.findall(GroupManager.group_pattern, url))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_group(current_user, name, description):
|
def create_group(current_user, name, description, *args, **kwargs):
|
||||||
g = Group()
|
g = Group()
|
||||||
|
|
||||||
|
if "uuid" in kwargs:
|
||||||
|
g.uuid = kwargs["uuid"]
|
||||||
|
|
||||||
# Clean for potential XSS
|
# Clean for potential XSS
|
||||||
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
@ -341,52 +346,17 @@ class PackageManager(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def readable(user, package):
|
def readable(user, package):
|
||||||
if (
|
return (
|
||||||
UserPackagePermission.objects.filter(package=package, user=user).exists()
|
PackageManager.has_package_permission(user, package, "read") | package.reviewed is True
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package, group__in=GroupManager.get_groups(user)
|
|
||||||
)
|
)
|
||||||
or package.reviewed is True
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def writable(user, package):
|
def writable(user, package):
|
||||||
if (
|
return PackageManager.has_package_permission(user, package, "write")
|
||||||
UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.WRITE[0]
|
|
||||||
).exists()
|
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package,
|
|
||||||
group__in=GroupManager.get_groups(user),
|
|
||||||
permission=Permission.WRITE[0],
|
|
||||||
).exists()
|
|
||||||
or UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.ALL[0]
|
|
||||||
).exists()
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def administrable(user, package):
|
def administrable(user, package):
|
||||||
if (
|
return PackageManager.has_package_permission(user, package, "all")
|
||||||
UserPackagePermission.objects.filter(
|
|
||||||
package=package, user=user, permission=Permission.ALL[0]
|
|
||||||
).exists()
|
|
||||||
or GroupPackagePermission.objects.filter(
|
|
||||||
package=package,
|
|
||||||
group__in=GroupManager.get_groups(user),
|
|
||||||
permission=Permission.ALL[0],
|
|
||||||
).exists()
|
|
||||||
or user.is_superuser
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
|
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
|
||||||
@ -395,6 +365,14 @@ class PackageManager(object):
|
|||||||
|
|
||||||
groups = GroupManager.get_groups(user)
|
groups = GroupManager.get_groups(user)
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
|
||||||
|
if package.classification_level == Package.Classification.SECRET:
|
||||||
|
if package.data_pool not in groups:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]}
|
||||||
|
|
||||||
valid_perms = perms.get(permission)
|
valid_perms = perms.get(permission)
|
||||||
@ -437,6 +415,7 @@ class PackageManager(object):
|
|||||||
try:
|
try:
|
||||||
p = Package.objects.get(uuid=package_id)
|
p = Package.objects.get(uuid=package_id)
|
||||||
if PackageManager.readable(user, p):
|
if PackageManager.readable(user, p):
|
||||||
|
p = PackageManager.check_package_classification(user, p)
|
||||||
return p
|
return p
|
||||||
else:
|
else:
|
||||||
# FIXME: use custom exception to be translatable to 403 in API
|
# FIXME: use custom exception to be translatable to 403 in API
|
||||||
@ -446,6 +425,37 @@ class PackageManager(object):
|
|||||||
except Package.DoesNotExist:
|
except Package.DoesNotExist:
|
||||||
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
raise ValueError("Package with ID {} does not exist!".format(package_id))
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_package_classification(user, pack: Package):
|
||||||
|
if pack.classification_level == Package.Classification.SECRET:
|
||||||
|
if pack.data_pool.user_member.filter(id=user.id).exists():
|
||||||
|
return pack
|
||||||
|
|
||||||
|
raise ValueError("Package is secret and not accessible to user!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return pack
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_package_classifications(user, package_qs: QuerySet[Package]):
|
||||||
|
non_secret = package_qs.exclude(classification_level=Package.Classification.SECRET)
|
||||||
|
secret = package_qs.filter(classification_level=Package.Classification.SECRET)
|
||||||
|
|
||||||
|
# TODO we should be able to do via the db
|
||||||
|
accessible_secret = []
|
||||||
|
|
||||||
|
for s_package in secret:
|
||||||
|
if s_package.data_pool.user_member.filter(id=user.id).exists():
|
||||||
|
accessible_secret.append(s_package.pk)
|
||||||
|
|
||||||
|
# Cannot combine a unique query with a non-unique query -> we have to call distinct
|
||||||
|
return Package.objects.filter(pk__in=accessible_secret).distinct() | non_secret.distinct()
|
||||||
|
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_readable_packages(user, include_reviewed=False):
|
def get_all_readable_packages(user, include_reviewed=False):
|
||||||
# UserPermission only exists if at least read is granted...
|
# UserPermission only exists if at least read is granted...
|
||||||
@ -470,7 +480,13 @@ class PackageManager(object):
|
|||||||
# remove package if user is owner and package is reviewed e.g. admin
|
# remove package if user is owner and package is reviewed e.g. admin
|
||||||
qs = qs.filter(reviewed=False)
|
qs = qs.filter(reviewed=False)
|
||||||
|
|
||||||
return qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
|
# EDIT START
|
||||||
|
qs = PackageManager.check_package_classifications(user, qs)
|
||||||
|
# EDIT END
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_writeable_packages(user):
|
def get_all_writeable_packages(user):
|
||||||
@ -514,11 +530,13 @@ class PackageManager(object):
|
|||||||
|
|
||||||
qs = qs.filter(reviewed=False)
|
qs = qs.filter(reviewed=False)
|
||||||
|
|
||||||
return qs.distinct()
|
qs = qs.distinct()
|
||||||
|
|
||||||
@staticmethod
|
# EDIT START
|
||||||
def get_packages():
|
qs = PackageManager.check_package_classifications(user, qs)
|
||||||
return Package.objects.all()
|
# EDIT END
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -623,6 +641,25 @@ 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()
|
||||||
|
|
||||||
@ -708,7 +745,13 @@ 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()
|
||||||
@ -716,6 +759,10 @@ class PackageManager(object):
|
|||||||
struc.description = structure["description"]
|
struc.description = structure["description"]
|
||||||
struc.aliases = structure.get("aliases", [])
|
struc.aliases = structure.get("aliases", [])
|
||||||
struc.smiles = structure["smiles"]
|
struc.smiles = structure["smiles"]
|
||||||
|
|
||||||
|
if structure.get("molfile"):
|
||||||
|
struc.molfile = structure["molfile"]
|
||||||
|
|
||||||
struc.save()
|
struc.save()
|
||||||
|
|
||||||
for scen in structure["scenarios"]:
|
for scen in structure["scenarios"]:
|
||||||
@ -1018,52 +1065,9 @@ class PackageManager(object):
|
|||||||
|
|
||||||
print("Fixing Node depths...")
|
print("Fixing Node depths...")
|
||||||
total_pws = Pathway.objects.filter(package=pack).count()
|
total_pws = Pathway.objects.filter(package=pack).count()
|
||||||
|
|
||||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||||
in_count = defaultdict(lambda: 0)
|
pw.update_depths()
|
||||||
out_count = defaultdict(lambda: 0)
|
|
||||||
|
|
||||||
for e in pw.edges:
|
|
||||||
# TODO check if this will remain
|
|
||||||
for react in e.start_nodes.all():
|
|
||||||
out_count[str(react.uuid)] += 1
|
|
||||||
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
in_count[str(prod.uuid)] += 1
|
|
||||||
|
|
||||||
root_nodes = []
|
|
||||||
for n in pw.nodes:
|
|
||||||
num_parents = in_count[str(n.uuid)]
|
|
||||||
if num_parents == 0:
|
|
||||||
# must be a root node or unconnected node
|
|
||||||
if n.depth != 0:
|
|
||||||
n.depth = 0
|
|
||||||
n.save()
|
|
||||||
|
|
||||||
# Only root node may have children
|
|
||||||
if out_count[str(n.uuid)] > 0:
|
|
||||||
root_nodes.append(n)
|
|
||||||
|
|
||||||
levels = [root_nodes]
|
|
||||||
seen = set()
|
|
||||||
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
|
|
||||||
for i, level_nodes in enumerate(levels):
|
|
||||||
new_level = []
|
|
||||||
for n in level_nodes:
|
|
||||||
for e in n.out_edges.all():
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
if str(prod.uuid) not in seen:
|
|
||||||
old_depth = prod.depth
|
|
||||||
if old_depth != i + 1:
|
|
||||||
prod.depth = i + 1
|
|
||||||
prod.save()
|
|
||||||
|
|
||||||
new_level.append(prod)
|
|
||||||
|
|
||||||
seen.add(str(n.uuid))
|
|
||||||
|
|
||||||
if new_level:
|
|
||||||
levels.append(new_level)
|
|
||||||
|
|
||||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||||
|
|
||||||
return pack
|
return pack
|
||||||
|
|||||||
@ -1,594 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-07-22 20:58
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.contrib.postgres.fields
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Compound',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EPModel',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Permission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='License',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('link', models.URLField(verbose_name='link')),
|
|
||||||
('image_link', models.URLField(verbose_name='Image link')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Rule',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('email', models.EmailField(max_length=254, unique=True)),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'user',
|
|
||||||
'verbose_name_plural': 'users',
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='APIToken',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('hashed_key', models.CharField(max_length=128, unique=True)),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
|
|
||||||
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CompoundStructure',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('smiles', models.TextField(verbose_name='SMILES')),
|
|
||||||
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
|
||||||
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
|
||||||
('normalized_structure', models.BooleanField(default=False)),
|
|
||||||
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='default_structure',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Edge',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EnviFormer',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PluginModel',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RuleBaseRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Group',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(verbose_name='Group name')),
|
|
||||||
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
|
||||||
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_group',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Node',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('depth', models.IntegerField(verbose_name='Node depth')),
|
|
||||||
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
|
||||||
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
|
||||||
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='end_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='start_nodes',
|
|
||||||
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Package',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
|
||||||
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='epmodel',
|
|
||||||
name='package',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='package',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_package',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='package',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='polymorphic_ctype',
|
|
||||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Pathway',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='pathway',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Reaction',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
|
||||||
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
|
||||||
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
|
||||||
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
|
||||||
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
|
||||||
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='edge_label',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Scenario',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('scenario_date', models.CharField(default='No date', max_length=256)),
|
|
||||||
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
|
||||||
('additional_information', models.JSONField(verbose_name='Additional Information')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
|
|
||||||
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reaction',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compoundstructure',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='scenarios',
|
|
||||||
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Setting',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('public', models.BooleanField(default=False)),
|
|
||||||
('global_default', models.BooleanField(default=False)),
|
|
||||||
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
|
||||||
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
|
||||||
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
|
||||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
|
||||||
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='setting',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='default_setting',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MLRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicabilityDomain',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
|
||||||
('name', models.TextField(default='no name', verbose_name='Name')),
|
|
||||||
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
|
||||||
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('num_neighbours', models.FloatField(default=5)),
|
|
||||||
('reliability_threshold', models.FloatField(default=0.5)),
|
|
||||||
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
|
||||||
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleAmbitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('smirks', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
|
||||||
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SimpleRDKitRule',
|
|
||||||
fields=[
|
|
||||||
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
|
||||||
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.simplerule',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SequentialRuleOrdering',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('order_index', models.IntegerField()),
|
|
||||||
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
|
||||||
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sequentialrule',
|
|
||||||
name='simple_rules',
|
|
||||||
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ParallelRule',
|
|
||||||
fields=[
|
|
||||||
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
|
||||||
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'base_manager_name': 'objects',
|
|
||||||
},
|
|
||||||
bases=('epdb.rule',),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='compound',
|
|
||||||
unique_together={('uuid', 'package')},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GroupPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'group')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserPackagePermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('package', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserSettingPermission',
|
|
||||||
fields=[
|
|
||||||
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
|
||||||
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('setting', 'user')},
|
|
||||||
},
|
|
||||||
bases=('epdb.permission',),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,128 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-08-25 18:07
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
('epdb', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalDatabase',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
|
||||||
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
|
||||||
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
|
||||||
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Database',
|
|
||||||
'verbose_name_plural': 'External Databases',
|
|
||||||
'db_table': 'epdb_external_database',
|
|
||||||
'ordering': ['name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='apitoken',
|
|
||||||
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='edge',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='edge',
|
|
||||||
name='polymorphic_ctype',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='is_active',
|
|
||||||
field=models.BooleanField(default=True, help_text='Whether this token is active'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='modified',
|
|
||||||
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='functional_groups',
|
|
||||||
field=models.JSONField(blank=True, default=dict, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='mlrelativereasoning',
|
|
||||||
name='app_domain',
|
|
||||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='created',
|
|
||||||
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='expires_at',
|
|
||||||
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='hashed_key',
|
|
||||||
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='apitoken',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='num_neighbours',
|
|
||||||
field=models.IntegerField(default=5),
|
|
||||||
),
|
|
||||||
migrations.AlterModelTable(
|
|
||||||
name='apitoken',
|
|
||||||
table='epdb_api_token',
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExternalIdentifier',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
||||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
||||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
('object_id', models.IntegerField()),
|
|
||||||
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
|
||||||
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
|
||||||
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
|
||||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
||||||
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'External Identifier',
|
|
||||||
'verbose_name_plural': 'External Identifiers',
|
|
||||||
'db_table': 'epdb_external_identifier',
|
|
||||||
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
|
||||||
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,228 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-08-26 17:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def populate_url(apps, schema_editor):
|
|
||||||
MODELS = [
|
|
||||||
'User',
|
|
||||||
'Group',
|
|
||||||
'Package',
|
|
||||||
'Compound',
|
|
||||||
'CompoundStructure',
|
|
||||||
'Pathway',
|
|
||||||
'Edge',
|
|
||||||
'Node',
|
|
||||||
'Reaction',
|
|
||||||
'SimpleAmbitRule',
|
|
||||||
'SimpleRDKitRule',
|
|
||||||
'ParallelRule',
|
|
||||||
'SequentialRule',
|
|
||||||
'Scenario',
|
|
||||||
'Setting',
|
|
||||||
'MLRelativeReasoning',
|
|
||||||
'EnviFormer',
|
|
||||||
'ApplicabilityDomain',
|
|
||||||
]
|
|
||||||
for model in MODELS:
|
|
||||||
obj_cls = apps.get_model("epdb", model)
|
|
||||||
for obj in obj_cls.objects.all():
|
|
||||||
obj.url = assemble_url(obj)
|
|
||||||
if obj.url is None:
|
|
||||||
raise ValueError(f"Could not assemble url for {obj}")
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
|
|
||||||
def assemble_url(obj):
|
|
||||||
from django.conf import settings as s
|
|
||||||
match obj.__class__.__name__:
|
|
||||||
case 'User':
|
|
||||||
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
|
||||||
case 'Group':
|
|
||||||
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
|
|
||||||
case 'Package':
|
|
||||||
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
|
|
||||||
case 'Compound':
|
|
||||||
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'CompoundStructure':
|
|
||||||
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
|
|
||||||
case 'SimpleAmbitRule':
|
|
||||||
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'SimpleRDKitRule':
|
|
||||||
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'ParallelRule':
|
|
||||||
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'SequentialRule':
|
|
||||||
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
|
|
||||||
case 'Reaction':
|
|
||||||
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'Pathway':
|
|
||||||
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'Node':
|
|
||||||
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
|
|
||||||
case 'Edge':
|
|
||||||
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
|
|
||||||
case 'MLRelativeReasoning':
|
|
||||||
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'EnviFormer':
|
|
||||||
return '{}/model/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'ApplicabilityDomain':
|
|
||||||
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
|
|
||||||
case 'Scenario':
|
|
||||||
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
|
|
||||||
case 'Setting':
|
|
||||||
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
|
|
||||||
case _:
|
|
||||||
raise ValueError(f"Unknown model {obj.__class__.__name__}")
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compound',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='compoundstructure',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='edge',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='epmodel',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='group',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='node',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='package',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reaction',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rule',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='scenario',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='setting',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=False, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
|
|
||||||
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
|
|
||||||
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='applicabilitydomain',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='compound',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='compoundstructure',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='edge',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='epmodel',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='group',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='node',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='package',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='pathway',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='reaction',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='rule',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='scenario',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='setting',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='user',
|
|
||||||
name='url',
|
|
||||||
field=models.TextField(null=True, unique=True, verbose_name='URL'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-09-09 09:21
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='mlrelativereasoning',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='mlrelativereasoning',
|
|
||||||
name='data_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='mlrelativereasoning',
|
|
||||||
name='eval_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='mlrelativereasoning',
|
|
||||||
name='rule_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RuleBasedRelativeReasoning',
|
|
||||||
fields=[
|
|
||||||
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
|
||||||
('threshold', models.FloatField(default=0.5)),
|
|
||||||
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
|
||||||
('min_count', models.IntegerField(default=10)),
|
|
||||||
('max_count', models.IntegerField(default=0)),
|
|
||||||
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
|
||||||
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')),
|
|
||||||
('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
|
|
||||||
('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=('epdb.epmodel',),
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='RuleBaseRelativeReasoning',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-09-11 06:21
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('epdb', '0004_alter_mlrelativereasoning_options_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='group',
|
|
||||||
name='group_member',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-09-18 06:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('epdb', '0005_alter_group_group_member'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='mlrelativereasoning',
|
|
||||||
name='multigen_eval',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rulebasedrelativereasoning',
|
|
||||||
name='multigen_eval',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-10-07 08:19
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='enviformer',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='app_domain',
|
|
||||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='data_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='eval_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='eval_results',
|
|
||||||
field=models.JSONField(blank=True, default=dict, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='model_status',
|
|
||||||
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='multigen_eval',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='enviformer',
|
|
||||||
name='rule_packages',
|
|
||||||
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-10-10 06:58
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
import uuid
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="EnzymeLink",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
model_utils.fields.AutoCreatedField(
|
|
||||||
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
model_utils.fields.AutoLastModifiedField(
|
|
||||||
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"uuid",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.TextField(default="no name", verbose_name="Name")),
|
|
||||||
(
|
|
||||||
"description",
|
|
||||||
models.TextField(default="no description", verbose_name="Descriptions"),
|
|
||||||
),
|
|
||||||
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
|
||||||
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
("ec_number", models.TextField(verbose_name="EC Number")),
|
|
||||||
("classification_level", models.IntegerField(verbose_name="Classification Level")),
|
|
||||||
("linking_method", models.TextField(verbose_name="Linking Method")),
|
|
||||||
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
|
|
||||||
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
|
|
||||||
(
|
|
||||||
"rule",
|
|
||||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-10-27 09:39
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import model_utils.fields
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0008_enzymelink"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="JobLog",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
model_utils.fields.AutoCreatedField(
|
|
||||||
default=django.utils.timezone.now, editable=False, verbose_name="created"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
model_utils.fields.AutoLastModifiedField(
|
|
||||||
default=django.utils.timezone.now, editable=False, verbose_name="modified"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("task_id", models.UUIDField(unique=True)),
|
|
||||||
("job_name", models.TextField()),
|
|
||||||
(
|
|
||||||
"status",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("INITIAL", "Initial"),
|
|
||||||
("SUCCESS", "Success"),
|
|
||||||
("FAILURE", "Failure"),
|
|
||||||
("REVOKED", "Revoked"),
|
|
||||||
("IGNORED", "Ignored"),
|
|
||||||
],
|
|
||||||
default="INITIAL",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
|
|
||||||
("task_result", models.TextField(blank=True, default=None, null=True)),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-11-11 14:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0009_joblog"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="license",
|
|
||||||
name="cc_string",
|
|
||||||
field=models.TextField(default="by-nc-sa", verbose_name="CC string"),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-11-11 14:13
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db.models import Min
|
|
||||||
|
|
||||||
|
|
||||||
def set_cc(apps, schema_editor):
|
|
||||||
License = apps.get_model("epdb", "License")
|
|
||||||
|
|
||||||
# For all existing licenses extract cc_string from link
|
|
||||||
for license in License.objects.all():
|
|
||||||
pattern = r"/licenses/([^/]+)/4\.0"
|
|
||||||
match = re.search(pattern, license.link)
|
|
||||||
if match:
|
|
||||||
license.cc_string = match.group(1)
|
|
||||||
license.save()
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Could not find license for {license.link}")
|
|
||||||
|
|
||||||
# Ensure we have all licenses
|
|
||||||
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
|
|
||||||
for cc_string in cc_strings:
|
|
||||||
if not License.objects.filter(cc_string=cc_string).exists():
|
|
||||||
new_license = License()
|
|
||||||
new_license.cc_string = cc_string
|
|
||||||
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
|
|
||||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
|
||||||
new_license.save()
|
|
||||||
|
|
||||||
# As we might have existing Licenses representing the same License,
|
|
||||||
# get min pk and all pks as a list
|
|
||||||
license_lookup_qs = License.objects.values("cc_string").annotate(
|
|
||||||
lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",))
|
|
||||||
)
|
|
||||||
|
|
||||||
license_lookup = {
|
|
||||||
row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs
|
|
||||||
}
|
|
||||||
|
|
||||||
Packages = apps.get_model("epdb", "Package")
|
|
||||||
|
|
||||||
for k, v in license_lookup.items():
|
|
||||||
# Set min pk to all packages pointing to any of the duplicates
|
|
||||||
Packages.objects.filter(pk__in=v[1]).update(license_id=v[0])
|
|
||||||
# remove the min pk from "other" pks as we use them for deletion
|
|
||||||
v[1].remove(v[0])
|
|
||||||
# Delete redundant License objects
|
|
||||||
License.objects.filter(pk__in=v[1]).delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0010_license_cc_string"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [migrations.RunPython(set_cc)]
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-02 13:09
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0011_auto_20251111_1413"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="node",
|
|
||||||
name="stereo_removed",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="pathway",
|
|
||||||
name="predicted",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-14 11:30
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0012_node_stereo_removed_pathway_predicted"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="setting",
|
|
||||||
name="expansion_schema",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("BFS", "Breadth First Search"),
|
|
||||||
("DFS", "Depth First Search"),
|
|
||||||
("GREEDY", "Greedy"),
|
|
||||||
],
|
|
||||||
default="BFS",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-14 16:02
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0013_setting_expansion_schema"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="setting",
|
|
||||||
old_name="expansion_schema",
|
|
||||||
new_name="expansion_scheme",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-01-19 19:26
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0014_rename_expansion_schema_setting_expansion_scheme"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="is_reviewer",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,179 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-02-12 09:38
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0015_user_is_reviewer"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="enviformer",
|
|
||||||
name="model_status",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="mlrelativereasoning",
|
|
||||||
name="model_status",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="rulebasedrelativereasoning",
|
|
||||||
name="model_status",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="epmodel",
|
|
||||||
name="model_status",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("INITIAL", "Initial"),
|
|
||||||
("INITIALIZING", "Model is initializing."),
|
|
||||||
("BUILDING", "Model is building."),
|
|
||||||
(
|
|
||||||
"BUILT_NOT_EVALUATED",
|
|
||||||
"Model is built and can be used for predictions, Model is not evaluated yet.",
|
|
||||||
),
|
|
||||||
("EVALUATING", "Model is evaluating"),
|
|
||||||
("FINISHED", "Model has finished building and evaluation."),
|
|
||||||
("ERROR", "Model has failed."),
|
|
||||||
],
|
|
||||||
default="INITIAL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="enviformer",
|
|
||||||
name="eval_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Evaluation Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="enviformer",
|
|
||||||
name="rule_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Rule Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="mlrelativereasoning",
|
|
||||||
name="eval_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Evaluation Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="mlrelativereasoning",
|
|
||||||
name="rule_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Rule Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="rulebasedrelativereasoning",
|
|
||||||
name="eval_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Evaluation Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="rulebasedrelativereasoning",
|
|
||||||
name="rule_packages",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Rule Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="PropertyPluginModel",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"epmodel_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="epdb.epmodel",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("threshold", models.FloatField(default=0.5)),
|
|
||||||
("eval_results", models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
("multigen_eval", models.BooleanField(default=False)),
|
|
||||||
("plugin_identifier", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"app_domain",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="epdb.applicabilitydomain",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"data_packages",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_data_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Data Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"eval_packages",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Evaluation Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"rule_packages",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Rule Packages",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
bases=("epdb.epmodel",),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="setting",
|
|
||||||
name="property_models",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
related_name="settings",
|
|
||||||
to="epdb.propertypluginmodel",
|
|
||||||
verbose_name="Setting Property Models",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name="PluginModel",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-02-20 12:02
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("contenttypes", "0002_remove_content_type_name"),
|
|
||||||
("epdb", "0016_remove_enviformer_model_status_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="AdditionalInformation",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
|
||||||
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
|
|
||||||
("kv", models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
("type", models.TextField(verbose_name="Additional Information Type")),
|
|
||||||
("data", models.JSONField(blank=True, default=dict, null=True)),
|
|
||||||
("object_id", models.PositiveBigIntegerField(blank=True, null=True)),
|
|
||||||
(
|
|
||||||
"content_type",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="contenttypes.contenttype",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"package",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to=settings.EPDB_PACKAGE_MODEL,
|
|
||||||
verbose_name="Package",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"scenario",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="scenario_additional_information",
|
|
||||||
to="epdb.scenario",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"indexes": [
|
|
||||||
models.Index(fields=["type"], name="epdb_additi_type_394349_idx"),
|
|
||||||
models.Index(
|
|
||||||
fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx"
|
|
||||||
),
|
|
||||||
models.Index(
|
|
||||||
fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx"
|
|
||||||
),
|
|
||||||
models.Index(
|
|
||||||
fields=["scenario", "content_type", "object_id"],
|
|
||||||
name="epdb_additi_scenari_ef2bf5_idx",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
"constraints": [
|
|
||||||
models.CheckConstraint(
|
|
||||||
condition=models.Q(
|
|
||||||
models.Q(("content_type__isnull", True), ("object_id__isnull", True)),
|
|
||||||
models.Q(("content_type__isnull", False), ("object_id__isnull", False)),
|
|
||||||
_connector="OR",
|
|
||||||
),
|
|
||||||
name="ck_addinfo_gfk_pair",
|
|
||||||
),
|
|
||||||
models.CheckConstraint(
|
|
||||||
condition=models.Q(
|
|
||||||
("scenario__isnull", False),
|
|
||||||
("content_type__isnull", False),
|
|
||||||
_connector="OR",
|
|
||||||
),
|
|
||||||
name="ck_addinfo_not_both_null",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-02-20 12:03
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def get_additional_information(scenario):
|
|
||||||
from envipy_additional_information import registry
|
|
||||||
from envipy_additional_information.parsers import TypeOfAerationParser
|
|
||||||
|
|
||||||
for k, vals in scenario.additional_information.items():
|
|
||||||
if k == "enzyme":
|
|
||||||
continue
|
|
||||||
|
|
||||||
if k == "SpikeConentration":
|
|
||||||
k = "SpikeConcentration"
|
|
||||||
|
|
||||||
if k == "AerationType":
|
|
||||||
k = "TypeOfAeration"
|
|
||||||
|
|
||||||
for v in vals:
|
|
||||||
# Per default additional fields are ignored
|
|
||||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
|
||||||
try:
|
|
||||||
inst = MAPPING[k](**v)
|
|
||||||
except Exception:
|
|
||||||
if k == "TypeOfAeration":
|
|
||||||
toa = TypeOfAerationParser()
|
|
||||||
inst = toa.from_string(v["type"])
|
|
||||||
|
|
||||||
# Add uuid to uniquely identify objects for manipulation
|
|
||||||
if "uuid" in v:
|
|
||||||
inst.__dict__["uuid"] = v["uuid"]
|
|
||||||
|
|
||||||
yield inst
|
|
||||||
|
|
||||||
|
|
||||||
def forward_func(apps, schema_editor):
|
|
||||||
Scenario = apps.get_model("epdb", "Scenario")
|
|
||||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
|
||||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
|
||||||
|
|
||||||
bulk = []
|
|
||||||
related = []
|
|
||||||
ctype = {o.model: o for o in ContentType.objects.all()}
|
|
||||||
parents = Scenario.objects.prefetch_related(
|
|
||||||
"compound_set",
|
|
||||||
"compoundstructure_set",
|
|
||||||
"reaction_set",
|
|
||||||
"rule_set",
|
|
||||||
"pathway_set",
|
|
||||||
"node_set",
|
|
||||||
"edge_set",
|
|
||||||
).filter(parent__isnull=True)
|
|
||||||
|
|
||||||
for i, scenario in enumerate(parents):
|
|
||||||
print(f"{i + 1}/{len(parents)}", end="\r")
|
|
||||||
if scenario.parent is not None:
|
|
||||||
related.append(scenario.parent)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for ai in get_additional_information(scenario):
|
|
||||||
bulk.append(
|
|
||||||
AdditionalInformation(
|
|
||||||
package=scenario.package,
|
|
||||||
scenario=scenario,
|
|
||||||
type=ai.__class__.__name__,
|
|
||||||
data=ai.model_dump(mode="json"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\n", len(bulk))
|
|
||||||
|
|
||||||
related = Scenario.objects.prefetch_related(
|
|
||||||
"compound_set",
|
|
||||||
"compoundstructure_set",
|
|
||||||
"reaction_set",
|
|
||||||
"rule_set",
|
|
||||||
"pathway_set",
|
|
||||||
"node_set",
|
|
||||||
"edge_set",
|
|
||||||
).filter(parent__isnull=False)
|
|
||||||
|
|
||||||
for i, scenario in enumerate(related):
|
|
||||||
print(f"{i + 1}/{len(related)}", end="\r")
|
|
||||||
parent = scenario.parent
|
|
||||||
# Check to which objects this scenario is attached to
|
|
||||||
for ai in get_additional_information(scenario):
|
|
||||||
rel_objs = [
|
|
||||||
"compound",
|
|
||||||
"compoundstructure",
|
|
||||||
"reaction",
|
|
||||||
"rule",
|
|
||||||
"pathway",
|
|
||||||
"node",
|
|
||||||
"edge",
|
|
||||||
]
|
|
||||||
for rel_obj in rel_objs:
|
|
||||||
for o in getattr(scenario, f"{rel_obj}_set").all():
|
|
||||||
bulk.append(
|
|
||||||
AdditionalInformation(
|
|
||||||
package=scenario.package,
|
|
||||||
scenario=parent,
|
|
||||||
type=ai.__class__.__name__,
|
|
||||||
data=ai.model_dump(mode="json"),
|
|
||||||
content_type=ctype[rel_obj],
|
|
||||||
object_id=o.pk,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Start creating additional information objects...")
|
|
||||||
AdditionalInformation.objects.bulk_create(bulk)
|
|
||||||
print("Done!")
|
|
||||||
print(len(bulk))
|
|
||||||
|
|
||||||
Scenario.objects.filter(parent__isnull=False).delete()
|
|
||||||
# Call ai save to fix urls
|
|
||||||
ais = AdditionalInformation.objects.all()
|
|
||||||
total = ais.count()
|
|
||||||
|
|
||||||
for i, ai in enumerate(ais):
|
|
||||||
print(f"{i + 1}/{total}", end="\r")
|
|
||||||
ai.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0017_additionalinformation"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
||||||
@ -1,20 +1,741 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
# Generated by Django 5.2.7 on 2026-03-06 10:51
|
||||||
|
|
||||||
from django.db import migrations
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("epdb", "0018_auto_20260220_1203"),
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.CreateModel(
|
||||||
model_name="scenario",
|
name='ApplicabilityDomain',
|
||||||
name="additional_information",
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('num_neighbours', models.IntegerField(default=5)),
|
||||||
|
('reliability_threshold', models.FloatField(default=0.5)),
|
||||||
|
('local_compatibilty_threshold', models.FloatField(default=0.5)),
|
||||||
|
('functional_groups', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.CreateModel(
|
||||||
model_name="scenario",
|
name='Edge',
|
||||||
name="parent",
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EPModel',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalDatabase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
|
||||||
|
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
|
||||||
|
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Database',
|
||||||
|
'verbose_name_plural': 'External Databases',
|
||||||
|
'db_table': 'epdb_external_database',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Permission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='License',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('cc_string', models.TextField(verbose_name='CC string')),
|
||||||
|
('link', models.URLField(verbose_name='link')),
|
||||||
|
('image_link', models.URLField(verbose_name='Image link')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Rule',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('is_reviewer', models.BooleanField(default=False)),
|
||||||
|
('default_package', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Default Package')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('hashed_key', models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)),
|
||||||
|
('expires_at', models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True)),
|
||||||
|
('name', models.CharField(help_text='Descriptive name for this token', max_length=100)),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Whether this token is active')),
|
||||||
|
('user', models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'API Token',
|
||||||
|
'verbose_name_plural': 'API Tokens',
|
||||||
|
'db_table': 'epdb_api_token',
|
||||||
|
'ordering': ['-created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Compound',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompoundStructure',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('smiles', models.TextField(verbose_name='SMILES')),
|
||||||
|
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
|
||||||
|
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
|
||||||
|
('normalized_structure', models.BooleanField(default=False)),
|
||||||
|
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='default_structure',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PropertyPluginModel',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('multigen_eval', models.BooleanField(default=False)),
|
||||||
|
('plugin_identifier', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('name', models.TextField(verbose_name='Group name')),
|
||||||
|
('public', models.BooleanField(default=False, verbose_name='Public Group')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('group_member', models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
|
||||||
|
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_group',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='JobLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('task_id', models.UUIDField(unique=True)),
|
||||||
|
('job_name', models.TextField()),
|
||||||
|
('status', models.CharField(choices=[('INITIAL', 'Initial'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKED', 'Revoked'), ('IGNORED', 'Ignored')], default='INITIAL', max_length=20)),
|
||||||
|
('done_at', models.DateTimeField(blank=True, default=None, null=True)),
|
||||||
|
('task_result', models.TextField(blank=True, default=None, null=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Package',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
|
||||||
|
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'swappable': 'EPDB_PACKAGE_MODEL',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Node',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('depth', models.IntegerField(verbose_name='Node depth')),
|
||||||
|
('stereo_removed', models.BooleanField(default=False)),
|
||||||
|
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
|
||||||
|
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
|
||||||
|
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='end_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='start_nodes',
|
||||||
|
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Pathway',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('predicted', models.BooleanField(default=False)),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='pathway',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Reaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
|
||||||
|
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
|
||||||
|
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
|
||||||
|
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
|
||||||
|
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EnzymeLink',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('ec_number', models.TextField(verbose_name='EC Number')),
|
||||||
|
('classification_level', models.IntegerField(verbose_name='Classification Level')),
|
||||||
|
('linking_method', models.TextField(verbose_name='Linking Method')),
|
||||||
|
('edge_evidence', models.ManyToManyField(to='epdb.edge')),
|
||||||
|
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.rule')),
|
||||||
|
('reaction_evidence', models.ManyToManyField(to='epdb.reaction')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='edge_label',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Scenario',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('scenario_date', models.CharField(default='No date', max_length=256)),
|
||||||
|
('scenario_type', models.CharField(default='Not specified', max_length=256)),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rule',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reaction',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='node',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='edge',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compoundstructure',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compound',
|
||||||
|
name='scenarios',
|
||||||
|
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Setting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('name', models.TextField(default='no name', verbose_name='Name')),
|
||||||
|
('description', models.TextField(default='no description', verbose_name='Descriptions')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('public', models.BooleanField(default=False)),
|
||||||
|
('global_default', models.BooleanField(default=False)),
|
||||||
|
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
|
||||||
|
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
|
||||||
|
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
|
||||||
|
('expansion_scheme', models.CharField(choices=[('BFS', 'Breadth First Search'), ('DFS', 'Depth First Search'), ('GREEDY', 'Greedy')], default='BFS', max_length=20)),
|
||||||
|
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Setting Rule Packages')),
|
||||||
|
('property_models', models.ManyToManyField(blank=True, related_name='settings', to='epdb.propertypluginmodel', verbose_name='Setting Property Models')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pathway',
|
||||||
|
name='setting',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='default_setting',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EnviFormer',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('multigen_eval', models.BooleanField(default=False)),
|
||||||
|
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MLRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('multigen_eval', models.BooleanField(default=False)),
|
||||||
|
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='applicabilitydomain',
|
||||||
|
name='model',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='propertypluginmodel',
|
||||||
|
name='app_domain',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='propertypluginmodel',
|
||||||
|
name='data_packages',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='propertypluginmodel',
|
||||||
|
name='eval_packages',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='propertypluginmodel',
|
||||||
|
name='rule_packages',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleBasedRelativeReasoning',
|
||||||
|
fields=[
|
||||||
|
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
|
||||||
|
('threshold', models.FloatField(default=0.5)),
|
||||||
|
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('multigen_eval', models.BooleanField(default=False)),
|
||||||
|
('min_count', models.IntegerField(default=10)),
|
||||||
|
('max_count', models.IntegerField(default=0)),
|
||||||
|
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
|
||||||
|
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
|
||||||
|
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
|
||||||
|
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('epdb.epmodel',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExternalIdentifier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('object_id', models.IntegerField()),
|
||||||
|
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
|
||||||
|
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
|
||||||
|
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'External Identifier',
|
||||||
|
'verbose_name_plural': 'External Identifiers',
|
||||||
|
'db_table': 'epdb_external_identifier',
|
||||||
|
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
|
||||||
|
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleAmbitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('smirks', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
|
||||||
|
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SimpleRDKitRule',
|
||||||
|
fields=[
|
||||||
|
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
|
||||||
|
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.simplerule',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SequentialRuleOrdering',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order_index', models.IntegerField()),
|
||||||
|
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
|
||||||
|
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sequentialrule',
|
||||||
|
name='simple_rules',
|
||||||
|
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ParallelRule',
|
||||||
|
fields=[
|
||||||
|
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
|
||||||
|
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'base_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
bases=('epdb.rule',),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='compound',
|
||||||
|
unique_together={('uuid', 'package')},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AdditionalInformation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('kv', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('type', models.TextField(verbose_name='Additional Information Type')),
|
||||||
|
('data', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||||
|
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
|
||||||
|
('scenario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scenario_additional_information', to='epdb.scenario')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'indexes': [models.Index(fields=['type'], name='epdb_additi_type_394349_idx'), models.Index(fields=['scenario', 'type'], name='epdb_additi_scenari_a59edf_idx'), models.Index(fields=['content_type', 'object_id'], name='epdb_additi_content_44d4b4_idx'), models.Index(fields=['scenario', 'content_type', 'object_id'], name='epdb_additi_scenari_ef2bf5_idx')],
|
||||||
|
'constraints': [models.CheckConstraint(condition=models.Q(models.Q(('content_type__isnull', True), ('object_id__isnull', True)), models.Q(('content_type__isnull', False), ('object_id__isnull', False)), _connector='OR'), name='ck_addinfo_gfk_pair'), models.CheckConstraint(condition=models.Q(('scenario__isnull', False), ('content_type__isnull', False), _connector='OR'), name='ck_addinfo_not_both_null')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GroupPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'group')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPackagePermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('package', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSettingPermission',
|
||||||
|
fields=[
|
||||||
|
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
|
||||||
|
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('setting', 'user')},
|
||||||
|
},
|
||||||
|
bases=('epdb.permission',),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-09 10:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_polymorphic_ctype(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
Compound = apps.get_model("epdb", "Compound")
|
||||||
|
CompoundStructure = apps.get_model("epdb", "CompoundStructure")
|
||||||
|
|
||||||
|
# Update Compound records
|
||||||
|
compound_ct = ContentType.objects.get_for_model(Compound)
|
||||||
|
Compound.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=compound_ct)
|
||||||
|
|
||||||
|
# Update CompoundStructure records
|
||||||
|
compound_structure_ct = ContentType.objects.get_for_model(CompoundStructure)
|
||||||
|
CompoundStructure.objects.filter(polymorphic_ctype__isnull=True).update(
|
||||||
|
polymorphic_ctype=compound_structure_ct
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_populate_polymorphic_ctype(apps, schema_editor):
|
||||||
|
Compound = apps.get_model("epdb", "Compound")
|
||||||
|
CompoundStructure = apps.get_model("epdb", "CompoundStructure")
|
||||||
|
|
||||||
|
Compound.objects.all().update(polymorphic_ctype=None)
|
||||||
|
CompoundStructure.objects.all().update(polymorphic_ctype=None)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("epdb", "0019_remove_scenario_additional_information_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="compoundstructure",
|
||||||
|
options={"base_manager_name": "objects"},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="compound",
|
||||||
|
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.AddField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
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.RunPython(populate_polymorphic_ctype, reverse_populate_polymorphic_ctype),
|
||||||
|
]
|
||||||
75
epdb/migrations/0021_classifierpluginmodel.py
Normal file
75
epdb/migrations/0021_classifierpluginmodel.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-25 11:44
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0020_alter_compoundstructure_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ClassifierPluginModel",
|
||||||
|
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)),
|
||||||
|
("plugin_config", models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
(
|
||||||
|
"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",),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-25 11:56
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0021_classifierpluginmodel"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="classifierpluginmodel",
|
||||||
|
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.AlterField(
|
||||||
|
model_name="enviformer",
|
||||||
|
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.AlterField(
|
||||||
|
model_name="mlrelativereasoning",
|
||||||
|
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.AlterField(
|
||||||
|
model_name="rulebasedrelativereasoning",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-21 11:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0022_alter_classifierpluginmodel_data_packages_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="compoundstructure",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="epmodel",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="parallelrule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="rule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="sequentialrule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="simpleambitrule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="simplerdkitrule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="simplerule",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
epdb/migrations/0024_user_contacted.py
Normal file
17
epdb/migrations/0024_user_contacted.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-21 19:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0023_alter_compoundstructure_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="contacted",
|
||||||
|
field=models.BooleanField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
||||||
|
|
||||||
|
MAPPING = {
|
||||||
|
"": HalfLifeModel.OTHER,
|
||||||
|
"HS-SFO": HalfLifeModel.HS_SFO,
|
||||||
|
"FOMC": HalfLifeModel.FOMC,
|
||||||
|
"FOTC": HalfLifeModel.DFOP,
|
||||||
|
"FMOC": HalfLifeModel.FOMC,
|
||||||
|
"DFOP": HalfLifeModel.DFOP,
|
||||||
|
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
||||||
|
"first order kinetics": HalfLifeModel.SFO,
|
||||||
|
"SFO²": HalfLifeModel.SFO,
|
||||||
|
"HS": HalfLifeModel.HS,
|
||||||
|
"top down": HalfLifeModel.OTHER,
|
||||||
|
"SFO": HalfLifeModel.SFO,
|
||||||
|
"First Order": HalfLifeModel.SFO,
|
||||||
|
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
||||||
|
"true": HalfLifeModel.SFO,
|
||||||
|
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||||
|
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
||||||
|
|
||||||
|
for hl in hls:
|
||||||
|
data = hl.data
|
||||||
|
data["model"] = MAPPING[data["model"]].value
|
||||||
|
hl.data = HalfLife(**data).model_dump(mode="json")
|
||||||
|
hl.save()
|
||||||
|
|
||||||
|
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
||||||
|
|
||||||
|
for hl in hlws:
|
||||||
|
data = hl.data
|
||||||
|
data["model"] = MAPPING[data["model"]].value
|
||||||
|
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
||||||
|
hl.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0024_user_contacted"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
335
epdb/models.py
335
epdb/models.py
@ -30,7 +30,7 @@ from sklearn.metrics import jaccard_score, precision_score, recall_score
|
|||||||
from sklearn.model_selection import ShuffleSplit
|
from sklearn.model_selection import ShuffleSplit
|
||||||
|
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from bridge.dto import RunResult, PredictedProperty
|
from bridge.dto import RunResult, PropertyPrediction
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
ApplicabilityDomainPCA,
|
||||||
@ -75,6 +75,7 @@ class User(AbstractUser):
|
|||||||
blank=False,
|
blank=False,
|
||||||
)
|
)
|
||||||
is_reviewer = models.BooleanField(default=False)
|
is_reviewer = models.BooleanField(default=False)
|
||||||
|
contacted = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
USERNAME_FIELD = "email"
|
||||||
REQUIRED_FIELDS = ["username"]
|
REQUIRED_FIELDS = ["username"]
|
||||||
@ -203,6 +204,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -765,7 +767,12 @@ class Package(EnviPathModel):
|
|||||||
|
|
||||||
|
|
||||||
class Compound(
|
class Compound(
|
||||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
PolymorphicModel,
|
||||||
|
EnviPathModel,
|
||||||
|
AliasMixin,
|
||||||
|
ScenarioMixin,
|
||||||
|
ChemicalIdentifierMixin,
|
||||||
|
AdditionalInformationMixin,
|
||||||
):
|
):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
@ -861,18 +868,25 @@ class Compound(
|
|||||||
|
|
||||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
|
|
||||||
|
subclasses = CompoundStructure.__subclasses__()
|
||||||
|
|
||||||
|
qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package)
|
||||||
|
if subclasses:
|
||||||
|
qs = qs.not_instance_of(*subclasses)
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES
|
# Check if we find a direct match for a given SMILES
|
||||||
if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists():
|
if qs.exists():
|
||||||
return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound
|
return qs.first().compound
|
||||||
|
|
||||||
|
|
||||||
|
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||||
|
if subclasses:
|
||||||
|
qs = qs.not_instance_of(*subclasses)
|
||||||
|
|
||||||
# Check if we can find the standardized one
|
# Check if we can find the standardized one
|
||||||
if CompoundStructure.objects.filter(
|
if qs.exists():
|
||||||
smiles=standardized_smiles, compound__package=package
|
|
||||||
).exists():
|
|
||||||
# TODO should we add a structure?
|
# TODO should we add a structure?
|
||||||
return CompoundStructure.objects.get(
|
return qs.first().compound
|
||||||
smiles=standardized_smiles, compound__package=package
|
|
||||||
).compound
|
|
||||||
|
|
||||||
# Generate Compound
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
@ -1095,13 +1109,19 @@ class Compound(
|
|||||||
|
|
||||||
|
|
||||||
class CompoundStructure(
|
class CompoundStructure(
|
||||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
PolymorphicModel,
|
||||||
|
EnviPathModel,
|
||||||
|
AliasMixin,
|
||||||
|
ScenarioMixin,
|
||||||
|
ChemicalIdentifierMixin,
|
||||||
|
AdditionalInformationMixin,
|
||||||
):
|
):
|
||||||
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
||||||
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
||||||
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
||||||
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
||||||
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
||||||
|
molfile = models.TextField(blank=True, null=True, verbose_name="Molfile")
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
@ -1198,6 +1218,9 @@ class CompoundStructure(
|
|||||||
|
|
||||||
return dict(hls)
|
return dict(hls)
|
||||||
|
|
||||||
|
def d3_json(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
||||||
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
|
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
|
||||||
@ -1775,9 +1798,9 @@ class Reaction(
|
|||||||
edges = Edge.objects.filter(edge_label=self)
|
edges = Edge.objects.filter(edge_label=self)
|
||||||
for e in edges:
|
for e in edges:
|
||||||
for scen in e.scenarios.all():
|
for scen in e.scenarios.all():
|
||||||
for ai in scen.additional_information.keys():
|
for ai in scen.get_additional_information():
|
||||||
if ai == "Enzyme":
|
if ai.type == "Enzyme":
|
||||||
res.extend(scen.additional_information[ai])
|
res.append(ai.get())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@ -2163,6 +2186,56 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
):
|
):
|
||||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||||
|
|
||||||
|
def update_depths(self):
|
||||||
|
# Collect number of in and out links per node
|
||||||
|
in_count = defaultdict(lambda: 0)
|
||||||
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
for e in self.edges:
|
||||||
|
for react in e.start_nodes.all():
|
||||||
|
out_count[str(react.uuid)] += 1
|
||||||
|
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
in_count[str(prod.uuid)] += 1
|
||||||
|
|
||||||
|
depth_map = {}
|
||||||
|
depth_map[0] = list()
|
||||||
|
|
||||||
|
for n in self.nodes:
|
||||||
|
num_parents = in_count[str(n.uuid)]
|
||||||
|
if num_parents == 0:
|
||||||
|
# must be a root node or unconnected node
|
||||||
|
if n.depth != 0:
|
||||||
|
n.depth = 0
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
# Only root node may have children
|
||||||
|
if out_count[str(n.uuid)] > 0:
|
||||||
|
depth_map[0].append(n)
|
||||||
|
|
||||||
|
# At most depth len(nodes) is possible
|
||||||
|
for i in range(self.nodes.count()):
|
||||||
|
level_nodes = depth_map.get(i, [])
|
||||||
|
|
||||||
|
if len(level_nodes) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
unique_next_level = set()
|
||||||
|
for n in level_nodes:
|
||||||
|
for e in self.edges:
|
||||||
|
if n in e.start_nodes.all():
|
||||||
|
for p in e.end_nodes.all():
|
||||||
|
unique_next_level.add(p)
|
||||||
|
|
||||||
|
if len(unique_next_level) > 0:
|
||||||
|
depth_map[i + 1] = list(unique_next_level)
|
||||||
|
|
||||||
|
for depth, nodes in depth_map.items():
|
||||||
|
for n in nodes:
|
||||||
|
if n.depth != depth:
|
||||||
|
n.depth = depth
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
|
||||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2201,10 +2274,14 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
predicted_properties = defaultdict(list)
|
predicted_properties = defaultdict(list)
|
||||||
for ai in self.additional_information.all():
|
for ai in self.additional_information.all():
|
||||||
if isinstance(ai.get(), PredictedProperty):
|
if isinstance(ai.get(), PropertyPrediction):
|
||||||
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
||||||
|
|
||||||
return {
|
# If we have Subclasses of a CompoundStructure we can overwrite keys (e.g. images)
|
||||||
|
# by overwriting keys
|
||||||
|
structure_data = self.default_node_label.d3_json()
|
||||||
|
|
||||||
|
res = {
|
||||||
"depth": self.depth,
|
"depth": self.depth,
|
||||||
"stereo_removed": self.stereo_removed,
|
"stereo_removed": self.stereo_removed,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
@ -2213,6 +2290,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"image_svg": IndigoUtils.mol_to_svg(
|
"image_svg": IndigoUtils.mol_to_svg(
|
||||||
self.default_node_label.smiles, width=40, height=40
|
self.default_node_label.smiles, width=40, height=40
|
||||||
),
|
),
|
||||||
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
"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()],
|
||||||
@ -2225,8 +2303,11 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"predicted_properties": predicted_properties,
|
"predicted_properties": predicted_properties,
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||||
"timeseries": self.get_timeseries_data(),
|
"timeseries": self.get_timeseries_data(),
|
||||||
|
**structure_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
@ -2334,7 +2415,10 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"reaction_probability": self.kv.get("probability"),
|
"reaction_probability": self.kv.get("probability"),
|
||||||
"start_node_urls": [x.url for x in self.start_nodes.all()],
|
"start_node_urls": [x.url for x in self.start_nodes.all()],
|
||||||
"end_node_urls": [x.url for x in self.end_nodes.all()],
|
"end_node_urls": [x.url for x in self.end_nodes.all()],
|
||||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
"scenarios": [
|
||||||
|
{"name": s.get_name(), "url": s.url, "review_status": s.package.reviewed}
|
||||||
|
for s in self.scenarios.all()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
for n in self.start_nodes.all():
|
for n in self.start_nodes.all():
|
||||||
@ -2486,6 +2570,7 @@ class PackageBasedModel(EPModel):
|
|||||||
s.EPDB_PACKAGE_MODEL,
|
s.EPDB_PACKAGE_MODEL,
|
||||||
verbose_name="Data Packages",
|
verbose_name="Data Packages",
|
||||||
related_name="%(app_label)s_%(class)s_data_packages",
|
related_name="%(app_label)s_%(class)s_data_packages",
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
eval_packages = models.ManyToManyField(
|
eval_packages = models.ManyToManyField(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
s.EPDB_PACKAGE_MODEL,
|
||||||
@ -3458,9 +3543,7 @@ class EnviFormer(PackageBasedModel):
|
|||||||
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
||||||
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
||||||
canon_smiles = [
|
canon_smiles = [
|
||||||
".".join(
|
".".join([FormatConverter.standardize(s, remove_stereo=True) for s in smi.split(".")])
|
||||||
[FormatConverter.standardize(s, remove_stereo=True) for s in smiles.split(".")]
|
|
||||||
)
|
|
||||||
for smi in smiles
|
for smi in smiles
|
||||||
]
|
]
|
||||||
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
||||||
@ -3810,6 +3893,211 @@ class EnviFormer(PackageBasedModel):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class ClassifierPluginModel(PackageBasedModel):
|
||||||
|
plugin_identifier = models.CharField(max_length=255)
|
||||||
|
plugin_config = JSONField(null=True, blank=True, default=dict)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@transaction.atomic
|
||||||
|
def create(
|
||||||
|
package: "Package",
|
||||||
|
plugin_identifier: str,
|
||||||
|
rule_packages: List["Package"] | None,
|
||||||
|
data_packages: List["Package"] | None,
|
||||||
|
name: "str" = None,
|
||||||
|
description: str = None,
|
||||||
|
config: EnviPyModel | None = None,
|
||||||
|
):
|
||||||
|
mod = ClassifierPluginModel()
|
||||||
|
mod.package = package
|
||||||
|
|
||||||
|
# Clean for potential XSS
|
||||||
|
if name is not None:
|
||||||
|
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
if name is None or name == "":
|
||||||
|
name = f"ClassifierPluginModel {ClassifierPluginModel.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
|
mod.name = name
|
||||||
|
|
||||||
|
if description is not None and description.strip() != "":
|
||||||
|
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
if plugin_identifier is None:
|
||||||
|
raise ValueError("Plugin identifier must be set")
|
||||||
|
|
||||||
|
impl = s.CLASSIFIER_PLUGINS.get(plugin_identifier, None)
|
||||||
|
|
||||||
|
if impl is None:
|
||||||
|
raise ValueError(f"Unknown plugin identifier: {plugin_identifier}")
|
||||||
|
|
||||||
|
mod.plugin_identifier = plugin_identifier
|
||||||
|
mod.plugin_config = config.__class__(
|
||||||
|
**json.loads(nh3.clean(config.model_dump_json()).strip())
|
||||||
|
).model_dump(mode="json")
|
||||||
|
|
||||||
|
if impl.requires_rule_packages() and (rule_packages is None or len(rule_packages) == 0):
|
||||||
|
raise ValueError("Plugin requires rules but none were provided")
|
||||||
|
elif not impl.requires_rule_packages() and (
|
||||||
|
rule_packages is not None and len(rule_packages) > 0
|
||||||
|
):
|
||||||
|
raise ValueError("Plugin does not require rules but some were provided")
|
||||||
|
|
||||||
|
if rule_packages is None:
|
||||||
|
rule_packages = []
|
||||||
|
|
||||||
|
if impl.requires_data_packages() and (data_packages is None or len(data_packages) == 0):
|
||||||
|
raise ValueError("Plugin requires data but none were provided")
|
||||||
|
elif not impl.requires_data_packages() and (
|
||||||
|
data_packages is not None and len(data_packages) > 0
|
||||||
|
):
|
||||||
|
raise ValueError("Plugin does not require data but some were provided")
|
||||||
|
|
||||||
|
if data_packages is None:
|
||||||
|
data_packages = []
|
||||||
|
|
||||||
|
mod.save()
|
||||||
|
|
||||||
|
for p in rule_packages:
|
||||||
|
mod.rule_packages.add(p)
|
||||||
|
|
||||||
|
for p in data_packages:
|
||||||
|
mod.data_packages.add(p)
|
||||||
|
|
||||||
|
mod.save()
|
||||||
|
return mod
|
||||||
|
|
||||||
|
def instance(self) -> "Property":
|
||||||
|
"""
|
||||||
|
Returns an instance of the plugin implementation.
|
||||||
|
|
||||||
|
This method retrieves the implementation of the plugin identified by
|
||||||
|
`self.plugin_identifier` from the `CLASSIFIER_PLUGINS` mapping, then
|
||||||
|
instantiates and returns it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
object: An instance of the plugin implementation.
|
||||||
|
"""
|
||||||
|
impl = s.CLASSIFIER_PLUGINS[self.plugin_identifier]
|
||||||
|
conf = impl.parse_config(data=self.plugin_config)
|
||||||
|
instance = impl(conf)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def build_dataset(self):
|
||||||
|
"""
|
||||||
|
Required by general model contract but actual implementation resides in plugin.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def build_model(self):
|
||||||
|
from bridge.dto import BaseDTO
|
||||||
|
|
||||||
|
self.model_status = self.BUILDING
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
compounds = CompoundStructure.objects.filter(compound__package__in=self.data_packages.all())
|
||||||
|
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||||
|
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||||
|
|
||||||
|
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||||
|
|
||||||
|
instance = self.instance()
|
||||||
|
|
||||||
|
_ = instance.build(eP)
|
||||||
|
|
||||||
|
self.model_status = self.BUILT_NOT_EVALUATED
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||||
|
return self.predict_batch([smiles], *args, **kwargs)[0]
|
||||||
|
|
||||||
|
def predict_batch(self, smiles: List[str], *args, **kwargs) -> List[List["PredictionResult"]]:
|
||||||
|
from bridge.dto import BaseDTO, CompoundProto
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TempCompound(CompoundProto):
|
||||||
|
url = None
|
||||||
|
name = None
|
||||||
|
smiles: str
|
||||||
|
|
||||||
|
batch = [TempCompound(smiles=smi) for smi in smiles]
|
||||||
|
|
||||||
|
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||||
|
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||||
|
|
||||||
|
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, batch, reactions, rules)
|
||||||
|
|
||||||
|
instance = self.instance()
|
||||||
|
|
||||||
|
rr: RunResult = instance.run(eP, *args, **kwargs)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
for smi in smiles:
|
||||||
|
pred_res = rr.result
|
||||||
|
|
||||||
|
if not isinstance(pred_res, list):
|
||||||
|
pred_res = [pred_res]
|
||||||
|
|
||||||
|
for r in pred_res:
|
||||||
|
if smi == r.substrate:
|
||||||
|
sub_res = []
|
||||||
|
for prod, prob in r.products.items():
|
||||||
|
sub_res.append(PredictionResult([ProductSet(prod.split("."))], prob, None))
|
||||||
|
|
||||||
|
res.append(sub_res)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
||||||
|
from bridge.dto import BaseDTO
|
||||||
|
|
||||||
|
if self.model_status != self.BUILT_NOT_EVALUATED:
|
||||||
|
raise ValueError("Model must be built before evaluation")
|
||||||
|
|
||||||
|
self.model_status = self.EVALUATING
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if eval_packages is not None:
|
||||||
|
for p in eval_packages:
|
||||||
|
self.eval_packages.add(p)
|
||||||
|
|
||||||
|
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||||
|
|
||||||
|
if self.eval_packages.count() > 0:
|
||||||
|
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||||
|
compounds = CompoundStructure.objects.filter(
|
||||||
|
compound__package__in=self.data_packages.all()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reactions = Reaction.objects.filter(package__in=self.eval_packages.all())
|
||||||
|
compounds = CompoundStructure.objects.filter(
|
||||||
|
compound__package__in=self.eval_packages.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||||
|
|
||||||
|
instance = self.instance()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.eval_packages.count() > 0:
|
||||||
|
res = instance.evaluate(eP, **kwargs)
|
||||||
|
self.eval_results = res.data
|
||||||
|
else:
|
||||||
|
res = instance.build_and_evaluate(eP)
|
||||||
|
self.eval_results = res.data
|
||||||
|
|
||||||
|
self.model_status = self.FINISHED
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during evaluation: {type(e).__name__}, {e}")
|
||||||
|
self.model_status = self.ERROR
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class PropertyPluginModel(PackageBasedModel):
|
class PropertyPluginModel(PackageBasedModel):
|
||||||
plugin_identifier = models.CharField(max_length=255)
|
plugin_identifier = models.CharField(max_length=255)
|
||||||
|
|
||||||
@ -4138,7 +4426,7 @@ class Scenario(EnviPathModel):
|
|||||||
ais = AdditionalInformation.objects.filter(scenario=self)
|
ais = AdditionalInformation.objects.filter(scenario=self)
|
||||||
|
|
||||||
if direct_only:
|
if direct_only:
|
||||||
return ais.filter(content_object__isnull=True)
|
return ais.filter(object_id__isnull=True)
|
||||||
else:
|
else:
|
||||||
return ais
|
return ais
|
||||||
|
|
||||||
@ -4180,7 +4468,6 @@ class AdditionalInformation(models.Model):
|
|||||||
ai: "EnviPyModel",
|
ai: "EnviPyModel",
|
||||||
scenario=None,
|
scenario=None,
|
||||||
content_object=None,
|
content_object=None,
|
||||||
skip_cleaning=False,
|
|
||||||
):
|
):
|
||||||
add_inf = AdditionalInformation()
|
add_inf = AdditionalInformation()
|
||||||
add_inf.package = package
|
add_inf.package = package
|
||||||
@ -4241,7 +4528,7 @@ class AdditionalInformation(models.Model):
|
|||||||
# Generic FK must be complete or empty
|
# Generic FK must be complete or empty
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
name="ck_addinfo_gfk_pair",
|
name="ck_addinfo_gfk_pair",
|
||||||
check=(
|
condition=(
|
||||||
(Q(content_type__isnull=True) & Q(object_id__isnull=True))
|
(Q(content_type__isnull=True) & Q(object_id__isnull=True))
|
||||||
| (Q(content_type__isnull=False) & Q(object_id__isnull=False))
|
| (Q(content_type__isnull=False) & Q(object_id__isnull=False))
|
||||||
),
|
),
|
||||||
@ -4249,7 +4536,7 @@ class AdditionalInformation(models.Model):
|
|||||||
# Disallow "floating" info
|
# Disallow "floating" info
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
name="ck_addinfo_not_both_null",
|
name="ck_addinfo_not_both_null",
|
||||||
check=Q(scenario__isnull=False) | Q(content_type__isnull=False),
|
condition=Q(scenario__isnull=False) | Q(content_type__isnull=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
17
epdb/template_registry.py
Normal file
17
epdb/template_registry.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
_registry = defaultdict(list)
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def register_template(slot: str, template_name: str, *, order: int = 100):
|
||||||
|
item = (order, template_name)
|
||||||
|
with _lock:
|
||||||
|
if item not in _registry[slot]:
|
||||||
|
_registry[slot].append(item)
|
||||||
|
_registry[slot].sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
|
||||||
|
def get_templates(slot: str):
|
||||||
|
return [template_name for _, template_name in _registry.get(slot, [])]
|
||||||
@ -2,6 +2,8 @@ from django import template
|
|||||||
from pydantic import AnyHttpUrl, ValidationError
|
from pydantic import AnyHttpUrl, ValidationError
|
||||||
from pydantic.type_adapter import TypeAdapter
|
from pydantic.type_adapter import TypeAdapter
|
||||||
|
|
||||||
|
from epdb.template_registry import get_templates
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
url_adapter = TypeAdapter(AnyHttpUrl)
|
url_adapter = TypeAdapter(AnyHttpUrl)
|
||||||
@ -19,3 +21,8 @@ def is_url(value):
|
|||||||
return True
|
return True
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def epdb_slot_templates(slot):
|
||||||
|
return get_templates(slot)
|
||||||
|
|||||||
176
epdb/views.py
176
epdb/views.py
@ -3,6 +3,7 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Iterable
|
from typing import Any, Dict, List, Iterable
|
||||||
|
|
||||||
|
import requests
|
||||||
import nh3
|
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
|
||||||
@ -32,6 +33,7 @@ from .models import (
|
|||||||
APIToken,
|
APIToken,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
|
ClassifierPluginModel,
|
||||||
Edge,
|
Edge,
|
||||||
EnviFormer,
|
EnviFormer,
|
||||||
EnzymeLink,
|
EnzymeLink,
|
||||||
@ -147,6 +149,11 @@ def handler500(request):
|
|||||||
def login(request):
|
def login(request):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
context["CAP_ENABLED"] = s.CAP_ENABLED
|
||||||
|
context["CAP_API_BASE"] = s.CAP_API_BASE
|
||||||
|
context["CAP_SITE_KEY"] = s.CAP_SITE_KEY
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context["title"] = "enviPath"
|
context["title"] = "enviPath"
|
||||||
context["next"] = request.GET.get("next", "")
|
context["next"] = request.GET.get("next", "")
|
||||||
@ -224,6 +231,11 @@ def logout(request):
|
|||||||
def register(request):
|
def register(request):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
context["CAP_ENABLED"] = s.CAP_ENABLED
|
||||||
|
context["CAP_API_BASE"] = s.CAP_API_BASE
|
||||||
|
context["CAP_SITE_KEY"] = s.CAP_SITE_KEY
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
# Redirect to unified login page with signup tab
|
# Redirect to unified login page with signup tab
|
||||||
next_url = request.GET.get("next", "")
|
next_url = request.GET.get("next", "")
|
||||||
@ -238,6 +250,33 @@ def register(request):
|
|||||||
if next := request.POST.get("next"):
|
if next := request.POST.get("next"):
|
||||||
context["next"] = next
|
context["next"] = next
|
||||||
|
|
||||||
|
# Catpcha
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
cap_token = request.POST.get("cap-token")
|
||||||
|
|
||||||
|
if not cap_token:
|
||||||
|
context["message"] = "Missing CAP Token."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
|
||||||
|
verify_url = f"{s.CAP_API_BASE}/{s.CAP_SITE_KEY}/siteverify"
|
||||||
|
payload = {
|
||||||
|
"secret": s.CAP_SECRET_KEY,
|
||||||
|
"response": cap_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(verify_url, json=payload, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
verify_data = resp.json()
|
||||||
|
except requests.RequestException:
|
||||||
|
context["message"] = "Captcha verification failed."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
|
||||||
|
if not verify_data.get("success"):
|
||||||
|
context["message"] = "Captcha check failed. Please try again."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
# End Captcha
|
||||||
|
|
||||||
username = request.POST.get("username", "").strip()
|
username = request.POST.get("username", "").strip()
|
||||||
email = request.POST.get("email", "").strip()
|
email = request.POST.get("email", "").strip()
|
||||||
password = request.POST.get("password", "").strip()
|
password = request.POST.get("password", "").strip()
|
||||||
@ -349,6 +388,9 @@ 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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -517,7 +559,7 @@ def packages(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "package"
|
context["entity_type"] = "package"
|
||||||
context["api_endpoint"] = "/api/v1/packages/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/packages/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "packages"
|
context["list_title"] = "packages"
|
||||||
|
|
||||||
@ -548,10 +590,38 @@ 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":
|
||||||
@ -575,7 +645,7 @@ def compounds(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "compound"
|
context["entity_type"] = "compound"
|
||||||
context["api_endpoint"] = "/api/v1/compounds/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/compounds/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_mode"] = "tabbed"
|
context["list_mode"] = "tabbed"
|
||||||
context["list_title"] = "compounds"
|
context["list_title"] = "compounds"
|
||||||
@ -604,7 +674,7 @@ def rules(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "rule"
|
context["entity_type"] = "rule"
|
||||||
context["api_endpoint"] = "/api/v1/rules/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/rules/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "rules"
|
context["list_title"] = "rules"
|
||||||
|
|
||||||
@ -632,7 +702,7 @@ def reactions(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "reaction"
|
context["entity_type"] = "reaction"
|
||||||
context["api_endpoint"] = "/api/v1/reactions/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/reactions/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "reactions"
|
context["list_title"] = "reactions"
|
||||||
|
|
||||||
@ -660,7 +730,7 @@ def pathways(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "pathway"
|
context["entity_type"] = "pathway"
|
||||||
context["api_endpoint"] = "/api/v1/pathways/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/pathways/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "pathways"
|
context["list_title"] = "pathways"
|
||||||
|
|
||||||
@ -690,7 +760,7 @@ def scenarios(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "scenario"
|
context["entity_type"] = "scenario"
|
||||||
context["api_endpoint"] = "/api/v1/scenarios/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/scenarios/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "scenarios"
|
context["list_title"] = "scenarios"
|
||||||
|
|
||||||
@ -736,21 +806,21 @@ def models(request):
|
|||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
context["model_types"][v().display()] = {
|
context["model_types"][v.display()] = {
|
||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": True,
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
}
|
}
|
||||||
for k, v in s.PROPERTY_PLUGINS.items():
|
for k, v in s.PROPERTY_PLUGINS.items():
|
||||||
context["model_types"][v().display()] = {
|
context["model_types"][v.display()] = {
|
||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": v().requires_rule_packages,
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": v().requires_data_packages,
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "model"
|
context["entity_type"] = "model"
|
||||||
context["api_endpoint"] = "/api/v1/models/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "models"
|
context["list_title"] = "models"
|
||||||
|
|
||||||
@ -832,7 +902,7 @@ def package_models(request, package_uuid):
|
|||||||
context["object_type"] = "model"
|
context["object_type"] = "model"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "model")
|
context["breadcrumbs"] = breadcrumbs(current_package, "model")
|
||||||
context["entity_type"] = "model"
|
context["entity_type"] = "model"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/model/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/model/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "models"
|
context["list_title"] = "models"
|
||||||
|
|
||||||
@ -867,25 +937,30 @@ def package_models(request, package_uuid):
|
|||||||
"requires_rule_packages": True,
|
"requires_rule_packages": True,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
"EnviFormer": {
|
}
|
||||||
|
|
||||||
|
if s.ENVIFORMER_PRESENT:
|
||||||
|
context["model_types"]["EnviFormer"] = {
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
context["model_types"][v().display()] = {
|
context["model_types"][v.display()] = {
|
||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": True,
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
|
"additional_parameters": v.Config.__name__.lower()
|
||||||
|
if v.Config.__name__ != ""
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
for k, v in s.PROPERTY_PLUGINS.items():
|
for k, v in s.PROPERTY_PLUGINS.items():
|
||||||
context["model_types"][v().display()] = {
|
context["model_types"][v.display()] = {
|
||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": v().requires_rule_packages,
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": v().requires_data_packages,
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "collections/models_paginated.html", context)
|
return render(request, "collections/models_paginated.html", context)
|
||||||
@ -917,7 +992,7 @@ def package_models(request, package_uuid):
|
|||||||
params["threshold"] = threshold
|
params["threshold"] = threshold
|
||||||
|
|
||||||
mod = EnviFormer.create(**params)
|
mod = EnviFormer.create(**params)
|
||||||
elif model_type == "mlrr":
|
elif model_type == "ml-relative-reasoning":
|
||||||
# ML Specific
|
# ML Specific
|
||||||
threshold = float(request.POST.get("model-threshold", 0.5))
|
threshold = float(request.POST.get("model-threshold", 0.5))
|
||||||
# TODO handle additional fingerprinter
|
# TODO handle additional fingerprinter
|
||||||
@ -941,27 +1016,41 @@ def package_models(request, package_uuid):
|
|||||||
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
|
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
|
||||||
|
|
||||||
mod = MLRelativeReasoning.create(**params)
|
mod = MLRelativeReasoning.create(**params)
|
||||||
elif model_type == "rbrr":
|
elif model_type == "rule-based-relative-reasoning":
|
||||||
params["rule_packages"] = [
|
params["rule_packages"] = [
|
||||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||||
]
|
]
|
||||||
|
|
||||||
mod = RuleBasedRelativeReasoning.create(**params)
|
mod = RuleBasedRelativeReasoning.create(**params)
|
||||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
|
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
|
||||||
pass
|
|
||||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
|
|
||||||
params["plugin_identifier"] = model_type
|
params["plugin_identifier"] = model_type
|
||||||
impl = s.PROPERTY_PLUGINS[model_type]
|
impl = s.CLASSIFIER_PLUGINS[model_type]
|
||||||
inst = impl()
|
|
||||||
|
|
||||||
if inst.requires_rule_packages():
|
if impl.requires_rule_packages():
|
||||||
params["rule_packages"] = [
|
params["rule_packages"] = [
|
||||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
params["rule_packages"] = []
|
params["rule_packages"] = []
|
||||||
|
|
||||||
if not inst.requires_data_packages():
|
if not impl.requires_data_packages():
|
||||||
|
params["data_packages"] = []
|
||||||
|
|
||||||
|
params["config"] = impl.parse_config(request.POST.dict())
|
||||||
|
|
||||||
|
mod = ClassifierPluginModel.create(**params)
|
||||||
|
elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
|
||||||
|
params["plugin_identifier"] = model_type
|
||||||
|
impl = s.PROPERTY_PLUGINS[model_type]
|
||||||
|
|
||||||
|
if impl.requires_rule_packages():
|
||||||
|
params["rule_packages"] = [
|
||||||
|
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
params["rule_packages"] = []
|
||||||
|
|
||||||
|
if not impl.requires_data_packages():
|
||||||
del params["data_packages"]
|
del params["data_packages"]
|
||||||
|
|
||||||
mod = PropertyPluginModel.create(**params)
|
mod = PropertyPluginModel.create(**params)
|
||||||
@ -1285,7 +1374,7 @@ def package_compounds(request, package_uuid):
|
|||||||
context["object_type"] = "compound"
|
context["object_type"] = "compound"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "compound")
|
context["breadcrumbs"] = breadcrumbs(current_package, "compound")
|
||||||
context["entity_type"] = "compound"
|
context["entity_type"] = "compound"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/compound/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_mode"] = "tabbed"
|
context["list_mode"] = "tabbed"
|
||||||
context["list_title"] = "compounds"
|
context["list_title"] = "compounds"
|
||||||
@ -1438,7 +1527,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
|||||||
context["entity_type"] = "structure"
|
context["entity_type"] = "structure"
|
||||||
context["page_title"] = f"{current_compound.get_name()} - Structures"
|
context["page_title"] = f"{current_compound.get_name()} - Structures"
|
||||||
context["api_endpoint"] = (
|
context["api_endpoint"] = (
|
||||||
f"/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
|
f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
|
||||||
)
|
)
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["compound"] = current_compound
|
context["compound"] = current_compound
|
||||||
@ -1601,7 +1690,7 @@ def package_rules(request, package_uuid):
|
|||||||
context["object_type"] = "rule"
|
context["object_type"] = "rule"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "rule")
|
context["breadcrumbs"] = breadcrumbs(current_package, "rule")
|
||||||
context["entity_type"] = "rule"
|
context["entity_type"] = "rule"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/rule/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/rule/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "rules"
|
context["list_title"] = "rules"
|
||||||
|
|
||||||
@ -1809,7 +1898,7 @@ def package_reactions(request, package_uuid):
|
|||||||
context["object_type"] = "reaction"
|
context["object_type"] = "reaction"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
|
context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
|
||||||
context["entity_type"] = "reaction"
|
context["entity_type"] = "reaction"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/reaction/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/reaction/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "reactions"
|
context["list_title"] = "reactions"
|
||||||
|
|
||||||
@ -1959,7 +2048,7 @@ def package_pathways(request, package_uuid):
|
|||||||
context["object_type"] = "pathway"
|
context["object_type"] = "pathway"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
|
context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
|
||||||
context["entity_type"] = "pathway"
|
context["entity_type"] = "pathway"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/pathway/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/pathway/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "pathways"
|
context["list_title"] = "pathways"
|
||||||
|
|
||||||
@ -2450,6 +2539,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
|||||||
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update depths as sideeffect of above operation
|
||||||
|
current_pathway.update_depths()
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -2535,7 +2627,7 @@ def package_scenarios(request, package_uuid):
|
|||||||
context["object_type"] = "scenario"
|
context["object_type"] = "scenario"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
|
context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
|
||||||
context["entity_type"] = "scenario"
|
context["entity_type"] = "scenario"
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/scenario/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/scenario/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "scenarios"
|
context["list_title"] = "scenarios"
|
||||||
|
|
||||||
@ -2790,9 +2882,15 @@ def groups(request):
|
|||||||
{"Group": s.SERVER_URL + "/group"},
|
{"Group": s.SERVER_URL + "/group"},
|
||||||
]
|
]
|
||||||
|
|
||||||
context["objects"] = Group.objects.all()
|
# Context for paginated template
|
||||||
|
context["entity_type"] = "group"
|
||||||
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/groups/"
|
||||||
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
|
context["list_title"] = "groups"
|
||||||
|
context["list_mode"] = "combined"
|
||||||
|
|
||||||
|
return render(request, "collections/groups_paginated.html", context)
|
||||||
|
|
||||||
return render(request, "collections/objects_list.html", context)
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
group_name = request.POST.get("group-name")
|
group_name = request.POST.get("group-name")
|
||||||
group_description = request.POST.get("group-description", s.DEFAULT_VALUES["description"])
|
group_description = request.POST.get("group-description", s.DEFAULT_VALUES["description"])
|
||||||
@ -2884,7 +2982,7 @@ def settings(request):
|
|||||||
|
|
||||||
# Context for paginated template
|
# Context for paginated template
|
||||||
context["entity_type"] = "setting"
|
context["entity_type"] = "setting"
|
||||||
context["api_endpoint"] = "/api/v1/settings/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/settings/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
context["list_title"] = "settings"
|
context["list_title"] = "settings"
|
||||||
context["list_mode"] = "combined"
|
context["list_mode"] = "combined"
|
||||||
|
|||||||
0
epiuclid/__init__.py
Normal file
0
epiuclid/__init__.py
Normal file
22
epiuclid/api.py
Normal file
22
epiuclid/api.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from ninja import Router
|
||||||
|
from epapi.v1.interfaces.iuclid.projections import get_pathway_for_iuclid_export
|
||||||
|
|
||||||
|
from .serializers.i6z import I6ZSerializer
|
||||||
|
from .serializers.pathway_mapper import PathwayMapper
|
||||||
|
|
||||||
|
router = Router(tags=["iuclid"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pathway/{uuid:pathway_uuid}/export/iuclid")
|
||||||
|
def export_pathway_iuclid(request, pathway_uuid: UUID):
|
||||||
|
export = get_pathway_for_iuclid_export(request.user, pathway_uuid)
|
||||||
|
bundle = PathwayMapper().map(export)
|
||||||
|
i6z_bytes = I6ZSerializer().serialize(bundle)
|
||||||
|
return HttpResponse(
|
||||||
|
i6z_bytes,
|
||||||
|
content_type="application/zip",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="pathway-{pathway_uuid}.i6z"'},
|
||||||
|
)
|
||||||
6
epiuclid/apps.py
Normal file
6
epiuclid/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class EpiuclidConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "epiuclid"
|
||||||
0
epiuclid/builders/__init__.py
Normal file
0
epiuclid/builders/__init__.py
Normal file
105
epiuclid/builders/base.py
Normal file
105
epiuclid/builders/base.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# IUCLID 6 XML namespaces
|
||||||
|
NS_PLATFORM_CONTAINER = "http://iuclid6.echa.europa.eu/namespaces/platform-container/v2"
|
||||||
|
NS_PLATFORM_METADATA = "http://iuclid6.echa.europa.eu/namespaces/platform-metadata/v1"
|
||||||
|
NS_PLATFORM_FIELDS = "http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1"
|
||||||
|
NS_XLINK = "http://www.w3.org/1999/xlink"
|
||||||
|
|
||||||
|
# Register namespace prefixes for clean output
|
||||||
|
ET.register_namespace("i6c", NS_PLATFORM_CONTAINER)
|
||||||
|
ET.register_namespace("i6m", NS_PLATFORM_METADATA)
|
||||||
|
ET.register_namespace("i6", NS_PLATFORM_FIELDS)
|
||||||
|
ET.register_namespace("xlink", NS_XLINK)
|
||||||
|
|
||||||
|
IUCLID_VERSION = "6.0.0"
|
||||||
|
DEFINITION_VERSION = "10.0"
|
||||||
|
CREATION_TOOL = "enviPath"
|
||||||
|
|
||||||
|
|
||||||
|
def _tag(ns: str, local: str) -> str:
|
||||||
|
return f"{{{ns}}}{local}"
|
||||||
|
|
||||||
|
|
||||||
|
def _sub(parent: ET.Element, ns: str, local: str, text: str | None = None) -> ET.Element:
|
||||||
|
"""Create a sub-element under parent. Only sets text if not None."""
|
||||||
|
elem = ET.SubElement(parent, _tag(ns, local))
|
||||||
|
if text is not None:
|
||||||
|
elem.text = str(text)
|
||||||
|
return elem
|
||||||
|
|
||||||
|
|
||||||
|
def _sub_if(parent: ET.Element, ns: str, local: str, text: str | None = None) -> ET.Element | None:
|
||||||
|
"""Create a sub-element only when text is not None."""
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
return _sub(parent, ns, local, text)
|
||||||
|
|
||||||
|
|
||||||
|
def build_platform_metadata(
|
||||||
|
document_key: str,
|
||||||
|
document_type: str,
|
||||||
|
name: str,
|
||||||
|
document_sub_type: str | None = None,
|
||||||
|
parent_document_key: str | None = None,
|
||||||
|
order_in_section_no: int | None = None,
|
||||||
|
) -> ET.Element:
|
||||||
|
"""Build the <i6c:PlatformMetadata> element for an i6d document."""
|
||||||
|
pm = ET.Element(_tag(NS_PLATFORM_CONTAINER, "PlatformMetadata"))
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "iuclidVersion", IUCLID_VERSION)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "documentKey", document_key)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "documentType", document_type)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "definitionVersion", DEFINITION_VERSION)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "creationDate", now)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "lastModificationDate", now)
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "name", name)
|
||||||
|
if document_sub_type:
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "documentSubType", document_sub_type)
|
||||||
|
if parent_document_key:
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "parentDocumentKey", parent_document_key)
|
||||||
|
if order_in_section_no is not None:
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "orderInSectionNo", str(order_in_section_no))
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "i5Origin", "false")
|
||||||
|
_sub(pm, NS_PLATFORM_METADATA, "creationTool", CREATION_TOOL)
|
||||||
|
|
||||||
|
return pm
|
||||||
|
|
||||||
|
|
||||||
|
def build_document(
|
||||||
|
document_key: str,
|
||||||
|
document_type: str,
|
||||||
|
name: str,
|
||||||
|
content_element: ET.Element,
|
||||||
|
document_sub_type: str | None = None,
|
||||||
|
parent_document_key: str | None = None,
|
||||||
|
order_in_section_no: int | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Build a complete i6d document XML string."""
|
||||||
|
root = ET.Element(_tag(NS_PLATFORM_CONTAINER, "Document"))
|
||||||
|
|
||||||
|
pm = build_platform_metadata(
|
||||||
|
document_key=document_key,
|
||||||
|
document_type=document_type,
|
||||||
|
name=name,
|
||||||
|
document_sub_type=document_sub_type,
|
||||||
|
parent_document_key=parent_document_key,
|
||||||
|
order_in_section_no=order_in_section_no,
|
||||||
|
)
|
||||||
|
root.append(pm)
|
||||||
|
|
||||||
|
content_wrapper = _sub(root, NS_PLATFORM_CONTAINER, "Content")
|
||||||
|
content_wrapper.append(content_element)
|
||||||
|
|
||||||
|
_sub(root, NS_PLATFORM_CONTAINER, "Attachments")
|
||||||
|
_sub(root, NS_PLATFORM_CONTAINER, "ModificationHistory")
|
||||||
|
|
||||||
|
return ET.tostring(root, encoding="unicode", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def document_key(uuid) -> str:
|
||||||
|
"""Format a UUID as an IUCLID document key (uuid/0 for raw data)."""
|
||||||
|
return f"{uuid}/0"
|
||||||
259
epiuclid/builders/endpoint_study.py
Normal file
259
epiuclid/builders/endpoint_study.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from epiuclid.serializers.pathway_mapper import IUCLIDEndpointStudyRecordData, SoilPropertiesData
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
NS_PLATFORM_FIELDS,
|
||||||
|
_sub,
|
||||||
|
_tag,
|
||||||
|
build_document,
|
||||||
|
document_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
NS_ESR_BIODEG = (
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/ENDPOINT_STUDY_RECORD-BiodegradationInSoil/10.0"
|
||||||
|
)
|
||||||
|
ET.register_namespace("", NS_ESR_BIODEG)
|
||||||
|
|
||||||
|
DOC_SUBTYPE = "BiodegradationInSoil"
|
||||||
|
PICKLIST_OTHER_CODE = "1342"
|
||||||
|
SOIL_TYPE_CODE_BY_KEY = {
|
||||||
|
"CLAY": "257",
|
||||||
|
"CLAY_LOAM": "258",
|
||||||
|
"LOAM": "1026",
|
||||||
|
"LOAMY_SAND": "1027",
|
||||||
|
"SAND": "1522",
|
||||||
|
"SANDY_CLAY_LOAM": "1523",
|
||||||
|
"SANDY_LOAM": "1524",
|
||||||
|
"SANDY_CLAY": "1525",
|
||||||
|
"SILT": "1549",
|
||||||
|
"SILT_LOAM": "1550",
|
||||||
|
"SILTY_CLAY": "1551",
|
||||||
|
"SILTY_CLAY_LOAM": "1552",
|
||||||
|
}
|
||||||
|
SOIL_CLASSIFICATION_CODE_BY_KEY = {
|
||||||
|
"USDA": "1649",
|
||||||
|
"DE": "314",
|
||||||
|
"INTERNATIONAL": "1658",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointStudyRecordBuilder:
|
||||||
|
def build(self, data: IUCLIDEndpointStudyRecordData) -> str:
|
||||||
|
esr = ET.Element(f"{{{NS_ESR_BIODEG}}}ENDPOINT_STUDY_RECORD.{DOC_SUBTYPE}")
|
||||||
|
|
||||||
|
soil_entries = list(data.soil_properties_entries)
|
||||||
|
if not soil_entries and data.soil_properties is not None:
|
||||||
|
soil_entries = [data.soil_properties]
|
||||||
|
|
||||||
|
has_materials = bool(
|
||||||
|
data.model_name_and_version
|
||||||
|
or data.software_name_and_version
|
||||||
|
or data.model_remarks
|
||||||
|
or soil_entries
|
||||||
|
)
|
||||||
|
if has_materials:
|
||||||
|
materials = _sub(esr, NS_ESR_BIODEG, "MaterialsAndMethods")
|
||||||
|
|
||||||
|
if soil_entries:
|
||||||
|
self._build_soil_structured_full(materials, soil_entries)
|
||||||
|
|
||||||
|
if data.model_name_and_version or data.software_name_and_version or data.model_remarks:
|
||||||
|
model_info = _sub(materials, NS_ESR_BIODEG, "ModelAndSoftware")
|
||||||
|
|
||||||
|
for model_name in data.model_name_and_version:
|
||||||
|
_sub(model_info, NS_ESR_BIODEG, "ModelNameAndVersion", model_name)
|
||||||
|
|
||||||
|
for software_name in data.software_name_and_version:
|
||||||
|
_sub(model_info, NS_ESR_BIODEG, "SoftwareNameAndVersion", software_name)
|
||||||
|
|
||||||
|
for remark in data.model_remarks:
|
||||||
|
_sub(model_info, NS_ESR_BIODEG, "Remarks", remark)
|
||||||
|
|
||||||
|
has_results = (
|
||||||
|
data.half_lives or data.transformation_products or data.temperature is not None
|
||||||
|
)
|
||||||
|
if has_results:
|
||||||
|
results = _sub(esr, NS_ESR_BIODEG, "ResultsAndDiscussion")
|
||||||
|
|
||||||
|
if data.half_lives or data.temperature is not None:
|
||||||
|
dt_parent = _sub(results, NS_ESR_BIODEG, "DTParentCompound")
|
||||||
|
|
||||||
|
if data.half_lives:
|
||||||
|
for hl in data.half_lives:
|
||||||
|
entry = ET.SubElement(dt_parent, _tag(NS_ESR_BIODEG, "entry"))
|
||||||
|
entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))
|
||||||
|
|
||||||
|
if hl.soil_no_code:
|
||||||
|
soil_no = _sub(entry, NS_ESR_BIODEG, "SoilNo")
|
||||||
|
_sub(soil_no, NS_ESR_BIODEG, "value", hl.soil_no_code)
|
||||||
|
|
||||||
|
value_range = _sub(entry, NS_ESR_BIODEG, "Value")
|
||||||
|
_sub(value_range, NS_ESR_BIODEG, "unitCode", "2329") # days
|
||||||
|
_sub(value_range, NS_ESR_BIODEG, "lowerValue", str(hl.dt50_start))
|
||||||
|
_sub(value_range, NS_ESR_BIODEG, "upperValue", str(hl.dt50_end))
|
||||||
|
|
||||||
|
temperature = (
|
||||||
|
hl.temperature if hl.temperature is not None else data.temperature
|
||||||
|
)
|
||||||
|
if temperature is not None:
|
||||||
|
temp_range = _sub(entry, NS_ESR_BIODEG, "Temp")
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "unitCode", "2493") # degree Celsius
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "lowerValue", str(temperature[0]))
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "upperValue", str(temperature[1]))
|
||||||
|
|
||||||
|
_sub(entry, NS_ESR_BIODEG, "KineticParameters", hl.model)
|
||||||
|
else:
|
||||||
|
# Temperature without half-lives: single entry with only Temp
|
||||||
|
assert data.temperature is not None
|
||||||
|
entry = ET.SubElement(dt_parent, _tag(NS_ESR_BIODEG, "entry"))
|
||||||
|
entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))
|
||||||
|
temp_range = _sub(entry, NS_ESR_BIODEG, "Temp")
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "unitCode", "2493") # degree Celsius
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "lowerValue", str(data.temperature[0]))
|
||||||
|
_sub(temp_range, NS_ESR_BIODEG, "upperValue", str(data.temperature[1]))
|
||||||
|
|
||||||
|
if data.transformation_products:
|
||||||
|
tp_details = _sub(results, NS_ESR_BIODEG, "TransformationProductsDetails")
|
||||||
|
for tp in data.transformation_products:
|
||||||
|
entry = ET.SubElement(tp_details, _tag(NS_ESR_BIODEG, "entry"))
|
||||||
|
entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(tp.uuid))
|
||||||
|
|
||||||
|
_sub(
|
||||||
|
entry,
|
||||||
|
NS_ESR_BIODEG,
|
||||||
|
"IdentityOfCompound",
|
||||||
|
document_key(tp.product_reference_uuid),
|
||||||
|
)
|
||||||
|
|
||||||
|
if tp.parent_reference_uuids:
|
||||||
|
parents = _sub(entry, NS_ESR_BIODEG, "ParentCompoundS")
|
||||||
|
for parent_uuid in tp.parent_reference_uuids:
|
||||||
|
_sub(parents, NS_PLATFORM_FIELDS, "key", document_key(parent_uuid))
|
||||||
|
|
||||||
|
if tp.kinetic_formation_fraction is not None:
|
||||||
|
_sub(
|
||||||
|
entry,
|
||||||
|
NS_ESR_BIODEG,
|
||||||
|
"KineticFormationFraction",
|
||||||
|
str(tp.kinetic_formation_fraction),
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_key = document_key(data.uuid)
|
||||||
|
return build_document(
|
||||||
|
document_key=doc_key,
|
||||||
|
document_type="ENDPOINT_STUDY_RECORD",
|
||||||
|
document_sub_type=DOC_SUBTYPE,
|
||||||
|
name=data.name,
|
||||||
|
content_element=esr,
|
||||||
|
parent_document_key=document_key(data.substance_uuid),
|
||||||
|
order_in_section_no=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_soil_structured_full(
|
||||||
|
materials: ET.Element,
|
||||||
|
props_entries: list[SoilPropertiesData],
|
||||||
|
) -> None:
|
||||||
|
study_design = _sub(materials, NS_ESR_BIODEG, "StudyDesign")
|
||||||
|
|
||||||
|
soil_classification = None
|
||||||
|
for props in props_entries:
|
||||||
|
soil_classification = EndpointStudyRecordBuilder._soil_classification(props)
|
||||||
|
if soil_classification:
|
||||||
|
break
|
||||||
|
|
||||||
|
if soil_classification:
|
||||||
|
soil_classification_el = _sub(study_design, NS_ESR_BIODEG, "SoilClassification")
|
||||||
|
value, other = EndpointStudyRecordBuilder._picklist_value_and_other(
|
||||||
|
soil_classification,
|
||||||
|
SOIL_CLASSIFICATION_CODE_BY_KEY,
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
_sub(soil_classification_el, NS_ESR_BIODEG, "value", value)
|
||||||
|
if other:
|
||||||
|
_sub(soil_classification_el, NS_ESR_BIODEG, "other", other)
|
||||||
|
|
||||||
|
soil_props = _sub(study_design, NS_ESR_BIODEG, "SoilProperties")
|
||||||
|
|
||||||
|
for props in props_entries:
|
||||||
|
entry = ET.SubElement(soil_props, _tag(NS_ESR_BIODEG, "entry"))
|
||||||
|
entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))
|
||||||
|
|
||||||
|
if props.soil_no_code:
|
||||||
|
soil_no = _sub(entry, NS_ESR_BIODEG, "SoilNo")
|
||||||
|
_sub(soil_no, NS_ESR_BIODEG, "value", props.soil_no_code)
|
||||||
|
|
||||||
|
soil_type = props.soil_type.strip() if props.soil_type else None
|
||||||
|
if soil_type:
|
||||||
|
soil_type_el = _sub(entry, NS_ESR_BIODEG, "SoilType")
|
||||||
|
value, other = EndpointStudyRecordBuilder._picklist_value_and_other(
|
||||||
|
soil_type,
|
||||||
|
SOIL_TYPE_CODE_BY_KEY,
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
_sub(soil_type_el, NS_ESR_BIODEG, "value", value)
|
||||||
|
if other:
|
||||||
|
_sub(soil_type_el, NS_ESR_BIODEG, "other", other)
|
||||||
|
|
||||||
|
if props.clay is not None:
|
||||||
|
clay_el = _sub(entry, NS_ESR_BIODEG, "Clay")
|
||||||
|
_sub(clay_el, NS_ESR_BIODEG, "lowerValue", str(props.clay))
|
||||||
|
|
||||||
|
if props.silt is not None:
|
||||||
|
silt_el = _sub(entry, NS_ESR_BIODEG, "Silt")
|
||||||
|
_sub(silt_el, NS_ESR_BIODEG, "lowerValue", str(props.silt))
|
||||||
|
|
||||||
|
if props.sand is not None:
|
||||||
|
sand_el = _sub(entry, NS_ESR_BIODEG, "Sand")
|
||||||
|
_sub(sand_el, NS_ESR_BIODEG, "lowerValue", str(props.sand))
|
||||||
|
|
||||||
|
if props.org_carbon is not None:
|
||||||
|
orgc_el = _sub(entry, NS_ESR_BIODEG, "OrgC")
|
||||||
|
_sub(orgc_el, NS_ESR_BIODEG, "lowerValue", str(props.org_carbon))
|
||||||
|
|
||||||
|
if props.ph_lower is not None or props.ph_upper is not None:
|
||||||
|
ph_el = _sub(entry, NS_ESR_BIODEG, "Ph")
|
||||||
|
if props.ph_lower is not None:
|
||||||
|
_sub(ph_el, NS_ESR_BIODEG, "lowerValue", str(props.ph_lower))
|
||||||
|
if props.ph_upper is not None:
|
||||||
|
_sub(ph_el, NS_ESR_BIODEG, "upperValue", str(props.ph_upper))
|
||||||
|
|
||||||
|
ph_method = props.ph_method.strip() if props.ph_method else None
|
||||||
|
if ph_method:
|
||||||
|
_sub(entry, NS_ESR_BIODEG, "PHMeasuredIn", ph_method)
|
||||||
|
|
||||||
|
if props.cec is not None:
|
||||||
|
cec_el = _sub(entry, NS_ESR_BIODEG, "CEC")
|
||||||
|
_sub(cec_el, NS_ESR_BIODEG, "lowerValue", str(props.cec))
|
||||||
|
|
||||||
|
if props.moisture_content is not None:
|
||||||
|
moisture_el = _sub(entry, NS_ESR_BIODEG, "MoistureContent")
|
||||||
|
_sub(moisture_el, NS_ESR_BIODEG, "lowerValue", str(props.moisture_content))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _soil_classification(props: SoilPropertiesData) -> str | None:
|
||||||
|
if props.soil_classification:
|
||||||
|
value = props.soil_classification.strip()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
if props.soil_type:
|
||||||
|
return "USDA"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _picklist_value_and_other(
|
||||||
|
raw_value: str,
|
||||||
|
code_map: dict[str, str],
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
value = raw_value.strip()
|
||||||
|
if not value:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
key = value.upper().replace("-", "_").replace(" ", "_")
|
||||||
|
code = code_map.get(key)
|
||||||
|
if code is not None:
|
||||||
|
return code, None
|
||||||
|
|
||||||
|
return PICKLIST_OTHER_CODE, value.replace("_", " ")
|
||||||
54
epiuclid/builders/reference_substance.py
Normal file
54
epiuclid/builders/reference_substance.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from epiuclid.serializers.pathway_mapper import IUCLIDReferenceSubstanceData
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
_sub,
|
||||||
|
_sub_if,
|
||||||
|
build_document,
|
||||||
|
document_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
NS_REFERENCE_SUBSTANCE = "http://iuclid6.echa.europa.eu/namespaces/REFERENCE_SUBSTANCE/10.0"
|
||||||
|
ET.register_namespace("", NS_REFERENCE_SUBSTANCE)
|
||||||
|
|
||||||
|
|
||||||
|
class ReferenceSubstanceBuilder:
|
||||||
|
def build(self, data: IUCLIDReferenceSubstanceData) -> str:
|
||||||
|
ref = ET.Element(f"{{{NS_REFERENCE_SUBSTANCE}}}REFERENCE_SUBSTANCE")
|
||||||
|
|
||||||
|
_sub(ref, NS_REFERENCE_SUBSTANCE, "ReferenceSubstanceName", data.name)
|
||||||
|
_sub_if(ref, NS_REFERENCE_SUBSTANCE, "IupacName", data.iupac_name)
|
||||||
|
if data.cas_number:
|
||||||
|
inventory = _sub(ref, NS_REFERENCE_SUBSTANCE, "Inventory")
|
||||||
|
_sub(inventory, NS_REFERENCE_SUBSTANCE, "CASNumber", data.cas_number)
|
||||||
|
|
||||||
|
has_structural_info = any(
|
||||||
|
[
|
||||||
|
data.molecular_formula,
|
||||||
|
data.molecular_weight is not None,
|
||||||
|
data.smiles,
|
||||||
|
data.inchi,
|
||||||
|
data.inchi_key,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if has_structural_info:
|
||||||
|
structural = _sub(ref, NS_REFERENCE_SUBSTANCE, "MolecularStructuralInfo")
|
||||||
|
_sub_if(structural, NS_REFERENCE_SUBSTANCE, "MolecularFormula", data.molecular_formula)
|
||||||
|
|
||||||
|
if data.molecular_weight is not None:
|
||||||
|
mw = _sub(structural, NS_REFERENCE_SUBSTANCE, "MolecularWeightRange")
|
||||||
|
_sub(mw, NS_REFERENCE_SUBSTANCE, "lowerValue", f"{data.molecular_weight:.2f}")
|
||||||
|
_sub(mw, NS_REFERENCE_SUBSTANCE, "upperValue", f"{data.molecular_weight:.2f}")
|
||||||
|
|
||||||
|
_sub_if(structural, NS_REFERENCE_SUBSTANCE, "SmilesNotation", data.smiles)
|
||||||
|
_sub_if(structural, NS_REFERENCE_SUBSTANCE, "InChl", data.inchi)
|
||||||
|
_sub_if(structural, NS_REFERENCE_SUBSTANCE, "InChIKey", data.inchi_key)
|
||||||
|
|
||||||
|
doc_key = document_key(data.uuid)
|
||||||
|
return build_document(
|
||||||
|
document_key=doc_key,
|
||||||
|
document_type="REFERENCE_SUBSTANCE",
|
||||||
|
name=data.name,
|
||||||
|
content_element=ref,
|
||||||
|
)
|
||||||
37
epiuclid/builders/substance.py
Normal file
37
epiuclid/builders/substance.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from epiuclid.serializers.pathway_mapper import IUCLIDSubstanceData
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
_sub,
|
||||||
|
build_document,
|
||||||
|
document_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
NS_SUBSTANCE = "http://iuclid6.echa.europa.eu/namespaces/SUBSTANCE/10.0"
|
||||||
|
ET.register_namespace("", NS_SUBSTANCE)
|
||||||
|
|
||||||
|
|
||||||
|
class SubstanceBuilder:
|
||||||
|
def build(self, data: IUCLIDSubstanceData) -> str:
|
||||||
|
substance = ET.Element(f"{{{NS_SUBSTANCE}}}SUBSTANCE")
|
||||||
|
|
||||||
|
_sub(substance, NS_SUBSTANCE, "Templates")
|
||||||
|
_sub(substance, NS_SUBSTANCE, "ChemicalName", data.name)
|
||||||
|
|
||||||
|
if data.reference_substance_uuid:
|
||||||
|
ref_sub = _sub(substance, NS_SUBSTANCE, "ReferenceSubstance")
|
||||||
|
_sub(
|
||||||
|
ref_sub,
|
||||||
|
NS_SUBSTANCE,
|
||||||
|
"ReferenceSubstance",
|
||||||
|
document_key(data.reference_substance_uuid),
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_key = document_key(data.uuid)
|
||||||
|
return build_document(
|
||||||
|
document_key=doc_key,
|
||||||
|
document_type="SUBSTANCE",
|
||||||
|
name=data.name,
|
||||||
|
content_element=substance,
|
||||||
|
)
|
||||||
90
epiuclid/schemas/loader.py
Normal file
90
epiuclid/schemas/loader.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""Load and cache IUCLID XSD schemas with cross-reference resolution.
|
||||||
|
|
||||||
|
The bundled XSD schemas use bare ``schemaLocation`` filenames (e.g.
|
||||||
|
``platform-fields.xsd``, ``commonTypesDomainV10.xsd``) that don't match the
|
||||||
|
actual directory layout. This module builds an explicit namespace → file-path
|
||||||
|
mapping so that ``xmlschema`` can resolve every import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import xmlschema
|
||||||
|
|
||||||
|
_SCHEMA_ROOT = Path(__file__).resolve().parent / "v10"
|
||||||
|
|
||||||
|
# Namespace → relative file-path (from _SCHEMA_ROOT) for schemas that are
|
||||||
|
# referenced by bare filename from subdirectories that don't contain them.
|
||||||
|
_NS_LOCATIONS: dict[str, str] = {
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1": "platform-fields.xsd",
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/platform-metadata/v1": "platform-metadata.xsd",
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/platform-container/v2": "platform-container-v2.xsd",
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/platform-attachment/v1": "platform-attachment.xsd",
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/platform-modification-history/v1": (
|
||||||
|
"platform-modification-history.xsd"
|
||||||
|
),
|
||||||
|
"http://www.w3.org/1999/xlink": "xlink.xsd",
|
||||||
|
"http://www.w3.org/XML/1998/namespace": "xml.xsd",
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/domain/v10": ("domain/v10/commonTypesDomainV10.xsd"),
|
||||||
|
"http://iuclid6.echa.europa.eu/namespaces/oecd/v10": ("oecd/v10/commonTypesOecdV10.xsd"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# doc_type → (subdir, filename-pattern)
|
||||||
|
_DOC_TYPE_PATHS: dict[str, tuple[str, str]] = {
|
||||||
|
"SUBSTANCE": ("domain/v10", "SUBSTANCE-10.0.xsd"),
|
||||||
|
"REFERENCE_SUBSTANCE": ("domain/v10", "REFERENCE_SUBSTANCE-10.0.xsd"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _absolute_locations() -> list[tuple[str, str]]:
|
||||||
|
"""Return (namespace, absolute-file-URI) pairs for all known schemas."""
|
||||||
|
return [(ns, (_SCHEMA_ROOT / rel).as_uri()) for ns, rel in _NS_LOCATIONS.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def _esr_path(subtype: str) -> Path:
|
||||||
|
"""Return the path to an Endpoint Study Record schema."""
|
||||||
|
return _SCHEMA_ROOT / "oecd" / "v10" / f"ENDPOINT_STUDY_RECORD-{subtype}-10.0.xsd"
|
||||||
|
|
||||||
|
|
||||||
|
def _doc_type_path(doc_type: str, subtype: str | None = None) -> Path:
|
||||||
|
if doc_type == "ENDPOINT_STUDY_RECORD":
|
||||||
|
if not subtype:
|
||||||
|
raise ValueError("subtype is required for ENDPOINT_STUDY_RECORD schemas")
|
||||||
|
return _esr_path(subtype)
|
||||||
|
info = _DOC_TYPE_PATHS.get(doc_type)
|
||||||
|
if info is None:
|
||||||
|
raise ValueError(f"Unknown document type: {doc_type}")
|
||||||
|
subdir, filename = info
|
||||||
|
return _SCHEMA_ROOT / subdir / filename
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=32)
|
||||||
|
def get_content_schema(doc_type: str, subtype: str | None = None) -> xmlschema.XMLSchema:
|
||||||
|
"""Return a compiled XSD schema for validating content elements.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
doc_type:
|
||||||
|
IUCLID document type (``SUBSTANCE``, ``REFERENCE_SUBSTANCE``,
|
||||||
|
``ENDPOINT_STUDY_RECORD``).
|
||||||
|
subtype:
|
||||||
|
Required for ``ENDPOINT_STUDY_RECORD`` (e.g. ``BiodegradationInSoil``).
|
||||||
|
"""
|
||||||
|
path = _doc_type_path(doc_type, subtype)
|
||||||
|
return xmlschema.XMLSchema(str(path), locations=_absolute_locations())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_document_schema() -> xmlschema.XMLSchema:
|
||||||
|
"""Return a compiled XSD schema for the ``platform-container-v2`` wrapper.
|
||||||
|
|
||||||
|
This validates the full ``<Document>`` element (PlatformMetadata + Content +
|
||||||
|
Attachments + ModificationHistory). Content is validated with
|
||||||
|
``processContents="strict"`` via ``xs:any``, but only if the content
|
||||||
|
namespace has been loaded. For full content validation, use
|
||||||
|
:func:`get_content_schema` separately.
|
||||||
|
"""
|
||||||
|
path = _SCHEMA_ROOT / "platform-container-v2.xsd"
|
||||||
|
return xmlschema.XMLSchema(str(path), locations=_absolute_locations())
|
||||||
237
epiuclid/schemas/v10/domain/v10/REFERENCE_SUBSTANCE-10.0.xsd
Normal file
237
epiuclid/schemas/v10/domain/v10/REFERENCE_SUBSTANCE-10.0.xsd
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://iuclid6.echa.europa.eu/namespaces/REFERENCE_SUBSTANCE/10.0" xmlns:ct="http://iuclid6.echa.europa.eu/namespaces/domain/v10" xmlns:i6="http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://iuclid6.echa.europa.eu/namespaces/REFERENCE_SUBSTANCE/10.0">
|
||||||
|
<xs:import namespace="http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1" schemaLocation="platform-fields.xsd"/>
|
||||||
|
<xs:import namespace="http://iuclid6.echa.europa.eu/namespaces/domain/v10" schemaLocation="commonTypesDomainV10.xsd"/>
|
||||||
|
<xs:element name="REFERENCE_SUBSTANCE">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="DataProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="ReferenceSubstanceName">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="i6:textFieldMultiLine">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="IupacName" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Description" type="i6:multilingualTextFieldLarge"/>
|
||||||
|
<xs:element minOccurs="0" name="Inventory">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="InventoryEntry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry" type="i6:inventoryEntry"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="InventoryEntryJustification">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N95"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="remarks" type="i6:multilingualTextField"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="CASNumber" type="i6:textFieldSmall"/>
|
||||||
|
<xs:element minOccurs="0" name="CASName" type="i6:textFieldMultiLine"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Synonyms">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="Synonyms">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:repeatableEntryType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="DataProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Identifier">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:PG6_60192"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Name" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Remarks" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="MolecularStructuralInfo">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="DataProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="MolecularFormula" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element minOccurs="0" name="MolecularWeightRange">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePhysicalQuantityRangeField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="lowerQualifier" type="i6:lowerQualifier"/>
|
||||||
|
<xs:element minOccurs="0" name="upperQualifier" type="i6:upperQualifier"/>
|
||||||
|
<xs:element minOccurs="0" name="lowerValue" type="xs:decimal"/>
|
||||||
|
<xs:element minOccurs="0" name="upperValue" type="xs:decimal"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="SmilesNotation" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element minOccurs="0" name="InChl" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="InChIKey" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
<xs:element minOccurs="0" name="StructuralFormula" type="i6:attachmentField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Remarks" type="i6:multilingualTextFieldLarge"/>
|
||||||
|
<xs:element minOccurs="0" name="ChemicalStructureFiles">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:repeatableEntryType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="StructureFile" type="i6:attachmentField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="RemarksChemStruct" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="RelatedSubstances">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="RelatedSubstances">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:repeatableEntryType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="Identifier">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:PG6_60192"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Identity" type="i6:textFieldLarge"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Remarks" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
<xs:element minOccurs="0" name="Relation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N05"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="GroupCategoryInfo" type="i6:multilingualTextFieldMultiLine"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:schema>
|
||||||
266
epiuclid/schemas/v10/domain/v10/SUBSTANCE-10.0.xsd
Normal file
266
epiuclid/schemas/v10/domain/v10/SUBSTANCE-10.0.xsd
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://iuclid6.echa.europa.eu/namespaces/SUBSTANCE/10.0" xmlns:ct="http://iuclid6.echa.europa.eu/namespaces/domain/v10" xmlns:i6="http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://iuclid6.echa.europa.eu/namespaces/SUBSTANCE/10.0">
|
||||||
|
<xs:import namespace="http://iuclid6.echa.europa.eu/namespaces/platform-fields/v1" schemaLocation="platform-fields.xsd"/>
|
||||||
|
<xs:import namespace="http://iuclid6.echa.europa.eu/namespaces/domain/v10" schemaLocation="commonTypesDomainV10.xsd"/>
|
||||||
|
<xs:element name="SUBSTANCE">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Templates">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Template" type="xs:string"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="ChemicalName">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="i6:textFieldMultiLine">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="PublicName" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element minOccurs="0" name="OtherNames">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:repeatableEntryType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="DataProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="NameType">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N97"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Name" type="i6:textFieldMultiLine"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Country">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:A31"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Relation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:PG6_60200"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="Remarks" type="i6:multilingualTextFieldLarge"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="OwnerLegalEntityProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="OwnerLegalEntity" type="i6:documentReferenceField"/>
|
||||||
|
<xs:element minOccurs="0" name="ThirdPartyProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="ThirdParty" type="i6:documentReferenceField"/>
|
||||||
|
<xs:element minOccurs="0" name="ContactPersons">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="entry">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:repeatableEntryType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="DataProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="ContactPerson" type="i6:documentReferenceField"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="ReferenceSubstance">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="Protection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="ReferenceSubstance" type="i6:documentReferenceField"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="TypeOfSubstance">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="Composition">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N08"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Origin">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:basePicklistField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N58"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="other" type="i6:multilingualTextFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="RoleInSupplyChain">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="RoleProtection">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="i6:baseDataProtectionField">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="confidentiality" type="ct:N64"/>
|
||||||
|
<xs:element minOccurs="0" name="justification" type="i6:textField"/>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="legislation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" name="value" type="ct:N78"/>
|
||||||
|
<xs:element minOccurs="0" name="other" type="i6:textFieldSmall"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" name="Manufacturer" nillable="true" type="i6:booleanField"/>
|
||||||
|
<xs:element minOccurs="0" name="Importer" nillable="true" type="i6:booleanField"/>
|
||||||
|
<xs:element minOccurs="0" name="OnlyRepresentative" nillable="true" type="i6:booleanField"/>
|
||||||
|
<xs:element minOccurs="0" name="DownstreamUser" nillable="true" type="i6:booleanField"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:schema>
|
||||||
24395
epiuclid/schemas/v10/domain/v10/commonTypesDomainV10.xsd
Normal file
24395
epiuclid/schemas/v10/domain/v10/commonTypesDomainV10.xsd
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
34308
epiuclid/schemas/v10/oecd/v10/commonTypesOecdV10.xsd
Normal file
34308
epiuclid/schemas/v10/oecd/v10/commonTypesOecdV10.xsd
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user