forked from enviPath/enviPy
Compare commits
36 Commits
8cdf91c8fb
...
develop-ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 520fb9f510 | |||
| 8ed3b506cc | |||
| ac297b2e25 | |||
| e2da59634b | |||
| f8d01e4477 | |||
| d381effdaf | |||
| 19d90b51eb | |||
| cca121af21 | |||
| 74489094c9 | |||
| 14cfc1e4d7 | |||
| 868bbf5c05 | |||
| be5ee1d1d7 | |||
| 20fd949dfd | |||
| c9b643fe6e | |||
| 1a9f1cf9af | |||
| c7c7e17e43 | |||
| 674e10c7fa | |||
| 8079b80d57 | |||
| 76e63fda2c | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e | |||
| 9d70db2ca2 | |||
| fec26d0233 | |||
| 689f7998eb | |||
| 8498e59fa1 | |||
| b508511cd6 | |||
| 877804c0ff | |||
| 964574c700 | |||
| 5029a8cda5 | |||
| d06bd0d4fd | |||
| f7c45b8015 | |||
| 68aea97013 | |||
| 3cc7fa9e8b | |||
| 21f3390a43 |
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
|
||||||
|
|||||||
105
Dockerfile
Normal file
105
Dockerfile
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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 \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Node 22 + pnpm
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& corepack enable \
|
||||||
|
&& corepack prepare pnpm@latest --activate \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
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
237
bayer/models.py
Normal file
237
bayer/models.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
from typing import List
|
||||||
|
import urllib.parse
|
||||||
|
import nh3
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.db import models, transaction
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from epdb.models import (
|
||||||
|
EnviPathModel,
|
||||||
|
Compound,
|
||||||
|
CompoundStructure,
|
||||||
|
ParallelRule,
|
||||||
|
SequentialRule,
|
||||||
|
SimpleAmbitRule,
|
||||||
|
SimpleRDKitRule,
|
||||||
|
)
|
||||||
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
|
|
||||||
|
class Package(EnviPathModel):
|
||||||
|
reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False)
|
||||||
|
license = models.ForeignKey(
|
||||||
|
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Classification(models.IntegerChoices):
|
||||||
|
INTERNAL = 0, "Internal"
|
||||||
|
RESTRICTED = 10 , "Restricted"
|
||||||
|
SECRET = 20, "Secret"
|
||||||
|
|
||||||
|
classification_level = models.IntegerField(
|
||||||
|
choices=Classification,
|
||||||
|
default=Classification.RESTRICTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
|
verbose_name="Data pool", default=None)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
# explicitly handle related Rules
|
||||||
|
for r in self.rules.all():
|
||||||
|
r.delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} (pk={self.pk})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def compounds(self) -> QuerySet:
|
||||||
|
return self.compound_set.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rules(self) -> QuerySet:
|
||||||
|
return self.rule_set.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reactions(self) -> QuerySet:
|
||||||
|
return self.reaction_set.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pathways(self) -> QuerySet:
|
||||||
|
return self.pathway_set.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scenarios(self) -> QuerySet:
|
||||||
|
return self.scenario_set.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def models(self) -> QuerySet:
|
||||||
|
return self.epmodel_set.all()
|
||||||
|
|
||||||
|
def _url(self):
|
||||||
|
return "{}/package/{}".format(s.SERVER_URL, self.uuid)
|
||||||
|
|
||||||
|
def get_applicable_rules(self) -> List["Rule"]:
|
||||||
|
"""
|
||||||
|
Returns a ordered set of rules where the following applies:
|
||||||
|
1. All Composite will be added to result
|
||||||
|
2. All SimpleRules will be added if theres no CompositeRule present using the SimpleRule
|
||||||
|
Ordering is based on "url" field.
|
||||||
|
"""
|
||||||
|
rules = []
|
||||||
|
rule_qs = self.rules
|
||||||
|
|
||||||
|
reflected_simple_rules = set()
|
||||||
|
|
||||||
|
for r in rule_qs:
|
||||||
|
if isinstance(r, ParallelRule) or isinstance(r, SequentialRule):
|
||||||
|
rules.append(r)
|
||||||
|
for sr in r.simple_rules.all():
|
||||||
|
reflected_simple_rules.add(sr)
|
||||||
|
|
||||||
|
for r in rule_qs:
|
||||||
|
if isinstance(r, SimpleAmbitRule) or isinstance(r, SimpleRDKitRule):
|
||||||
|
if r not in reflected_simple_rules:
|
||||||
|
rules.append(r)
|
||||||
|
|
||||||
|
rules = sorted(rules, key=lambda x: x.url)
|
||||||
|
return rules
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "epdb_package"
|
||||||
|
|
||||||
|
|
||||||
|
class PESCompound(Compound):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@transaction.atomic
|
||||||
|
def create(
|
||||||
|
package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs
|
||||||
|
) -> "Compound":
|
||||||
|
|
||||||
|
pes_url = pes_data["pes_url"]
|
||||||
|
|
||||||
|
# Check if we find a direct match for a given pes_link
|
||||||
|
if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists():
|
||||||
|
# Due to normalization we might end up in having multiple structures
|
||||||
|
# All of them point to the same compound -> pick any
|
||||||
|
return PESStructure.objects.filter(pes_link=pes_url, compound__package=package).first().compound
|
||||||
|
|
||||||
|
# Generate Compound
|
||||||
|
c = PESCompound()
|
||||||
|
c.package = package
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
# Clean for potential XSS
|
||||||
|
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
if name is None or name == "":
|
||||||
|
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
|
c.name = name
|
||||||
|
|
||||||
|
# We have a default here only set the value if it carries some payload
|
||||||
|
if description is not None and description.strip() != "":
|
||||||
|
c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
molfile = pes_data.get("representativeStructures", [{}])[0].get("ctab")
|
||||||
|
|
||||||
|
if molfile is None:
|
||||||
|
raise ValueError("PES data does not contain a valid mol file!")
|
||||||
|
|
||||||
|
smiles = FormatConverter.to_smiles(FormatConverter.from_molfile(molfile))
|
||||||
|
|
||||||
|
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
|
|
||||||
|
is_standardized = standardized_smiles == smiles
|
||||||
|
|
||||||
|
if not is_standardized:
|
||||||
|
_ = PESStructure.create(
|
||||||
|
c,
|
||||||
|
pes_url,
|
||||||
|
molfile,
|
||||||
|
standardized_smiles,
|
||||||
|
name="Normalized structure of {}".format(name),
|
||||||
|
description="{} (in its normalized form)".format(description),
|
||||||
|
normalized_structure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cs = PESStructure.create(
|
||||||
|
c,
|
||||||
|
pes_url,
|
||||||
|
molfile,
|
||||||
|
smiles,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized
|
||||||
|
)
|
||||||
|
|
||||||
|
c.default_structure = cs
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
class PESStructure(CompoundStructure):
|
||||||
|
pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@transaction.atomic
|
||||||
|
def create(
|
||||||
|
compound: Compound,
|
||||||
|
pes_link: str,
|
||||||
|
mol_file: str,
|
||||||
|
smiles: str,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
if compound.pk is None:
|
||||||
|
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||||
|
|
||||||
|
cs = PESStructure()
|
||||||
|
# Clean for potential XSS
|
||||||
|
if name is not None:
|
||||||
|
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
if description is not None:
|
||||||
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
cs.smiles = smiles
|
||||||
|
cs.mol_file = mol_file
|
||||||
|
cs.pes_link = pes_link
|
||||||
|
cs.compound = compound
|
||||||
|
|
||||||
|
if "normalized_structure" in kwargs:
|
||||||
|
cs.normalized_structure = kwargs["normalized_structure"]
|
||||||
|
|
||||||
|
cs.save()
|
||||||
|
|
||||||
|
return cs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def add_structure(
|
||||||
|
self,
|
||||||
|
smiles: str,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
default_structure: bool = False,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> "CompoundStructure":
|
||||||
|
raise ValueError("Not supported!")
|
||||||
|
|
||||||
|
def d3_json(self):
|
||||||
|
return {
|
||||||
|
"is_pes": True,
|
||||||
|
"pes_link": self.pes_link,
|
||||||
|
# Will overwrite image from Node
|
||||||
|
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
||||||
|
"image_type": "png",
|
||||||
|
}
|
||||||
9
bayer/templates/actions/collections/new_pes.html
Normal file
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
163
bayer/views.py
Normal file
163
bayer/views.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
from bayer.models import PESCompound
|
||||||
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import Pathway, Node
|
||||||
|
from epdb.views import _anonymous_or_real
|
||||||
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
|
@package_permission_required()
|
||||||
|
def create_pes(request, package_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
|
compound_name = request.POST.get('compound-name')
|
||||||
|
compound_description = request.POST.get('compound-description')
|
||||||
|
pes_link = request.POST.get('pes-link')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
except ValueError as e:
|
||||||
|
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
|
return redirect(pes.url)
|
||||||
|
else:
|
||||||
|
return BadRequest("Please provide a PES link.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@package_permission_required()
|
||||||
|
def create_pes_node(request, package_uuid, pathway_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
|
||||||
|
current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
|
|
||||||
|
compound_name = request.POST.get('compound-name')
|
||||||
|
compound_description = request.POST.get('compound-description')
|
||||||
|
pes_link = request.POST.get('pes-link')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
except ValueError as e:
|
||||||
|
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
|
if current_package.classification_level != Package.Classification.SECRET:
|
||||||
|
return BadRequest("Cannot create PESs for non-secret packages.")
|
||||||
|
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
|
return BadRequest(
|
||||||
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
|
node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure)
|
||||||
|
if node_qs.exists():
|
||||||
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
|
n = Node()
|
||||||
|
n.stereo_removed = False
|
||||||
|
n.pathway = current_pathway
|
||||||
|
n.depth = 0
|
||||||
|
|
||||||
|
n.default_node_label = pes.default_structure
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
n.node_labels.add(pes.default_structure)
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return BadRequest("Please provide a PES link.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_pes(request, pes_url) -> dict:
|
||||||
|
from epauth.views import get_access_token_from_request
|
||||||
|
token = get_access_token_from_request(request)
|
||||||
|
|
||||||
|
if token:
|
||||||
|
for k, v in s.PES_API_MAPPING.items():
|
||||||
|
if pes_url.startswith(k):
|
||||||
|
pes_id = pes_url.split('/')[-1]
|
||||||
|
|
||||||
|
if pes_id == 'dummy':
|
||||||
|
import json
|
||||||
|
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
||||||
|
res_data["pes_url"] = pes_url
|
||||||
|
return res_data
|
||||||
|
else:
|
||||||
|
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
||||||
|
params = {"pes_reg_entity_corporate_id": pes_id}
|
||||||
|
|
||||||
|
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res.raise_for_status()
|
||||||
|
pes_data = res.json()
|
||||||
|
|
||||||
|
if len(pes_data) == 0:
|
||||||
|
raise ValueError(f"PES with id {pes_id} not found")
|
||||||
|
|
||||||
|
res_data = pes_data[0]
|
||||||
|
res_data["pes_url"] = pes_url
|
||||||
|
return res_data
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown URL {pes_url}")
|
||||||
|
else:
|
||||||
|
raise ValueError("Could not fetch access token from request.")
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_pes(request):
|
||||||
|
pes_link = request.GET.get('pesLink')
|
||||||
|
|
||||||
|
if pes_link:
|
||||||
|
pes_data = fetch_pes(request, pes_link)
|
||||||
|
|
||||||
|
representations = pes_data.get('representations')
|
||||||
|
|
||||||
|
for rep in representations:
|
||||||
|
if rep.get('type') == 'color':
|
||||||
|
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
|
||||||
|
return HttpResponse(image_data, content_type="image/png")
|
||||||
183
bb4g/__init__.py
Normal file
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",
|
||||||
@ -142,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
|
||||||
@ -190,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"}
|
||||||
@ -216,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",)
|
||||||
@ -292,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"
|
||||||
|
|
||||||
@ -332,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 = {}
|
||||||
@ -362,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,
|
||||||
@ -370,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"
|
||||||
@ -382,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",
|
||||||
@ -391,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:
|
||||||
@ -408,3 +436,60 @@ 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")
|
||||||
|
|
||||||
|
os.environ["NO_PROXY"] = "localhost,127.0.0.1,epbiotransformer3"
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
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)
|
||||||
26
epapi/v1/endpoints/joblogs.py
Normal file
26
epapi/v1/endpoints/joblogs.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django.conf import settings as s
|
||||||
|
from ninja import Router
|
||||||
|
from ninja_extra.pagination import paginate
|
||||||
|
|
||||||
|
from epdb.models import JobLog
|
||||||
|
from ..pagination import EnhancedPageNumberPagination
|
||||||
|
from ..schemas import JobLogOutSchema
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
|
||||||
|
@paginate(
|
||||||
|
EnhancedPageNumberPagination,
|
||||||
|
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||||
|
)
|
||||||
|
def list_all_joblogs(request):
|
||||||
|
"""
|
||||||
|
List all JobLogs from reviewed packages.
|
||||||
|
"""
|
||||||
|
current_user = request.user
|
||||||
|
|
||||||
|
if current_user.is_superuser:
|
||||||
|
return JobLog.objects.all().order_by("-created")
|
||||||
|
else:
|
||||||
|
return JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
@ -15,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,8 @@ from .endpoints import (
|
|||||||
structure,
|
structure,
|
||||||
additional_information,
|
additional_information,
|
||||||
settings,
|
settings,
|
||||||
|
groups,
|
||||||
|
joblogs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main router with authentication
|
# Main router with authentication
|
||||||
@ -34,3 +37,10 @@ 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)
|
||||||
|
router.add_router("", joblogs.router)
|
||||||
|
|
||||||
|
if s.IUCLID_EXPORT_ENABLED:
|
||||||
|
from epiuclid.api import router as iuclid_router
|
||||||
|
|
||||||
|
router.add_router("", iuclid_router)
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
from ninja import FilterSchema, FilterLookup, Schema
|
from datetime import datetime
|
||||||
from typing import Annotated, Optional, List, Dict, Any
|
from typing import Annotated, Optional, List, Dict, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from ninja import Field, FilterSchema, FilterLookup, Schema
|
||||||
|
|
||||||
|
|
||||||
# Filter schema for query parameters
|
# Filter schema for query parameters
|
||||||
class ReviewStatusFilter(FilterSchema):
|
class ReviewStatusFilter(FilterSchema):
|
||||||
@ -126,3 +129,30 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleUserOutSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
url: str
|
||||||
|
name: str = Field(alias="username")
|
||||||
|
|
||||||
|
|
||||||
|
class JobLogOutSchema(Schema):
|
||||||
|
user: SimpleUserOutSchema
|
||||||
|
id: UUID = Field(alias="task_id")
|
||||||
|
url: str
|
||||||
|
name: str = Field(alias="job_name")
|
||||||
|
created: datetime = Field(alias="created")
|
||||||
|
status: str = Field(alias="status")
|
||||||
|
done: Optional[datetime] = Field(None, alias="done_at")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_url(obj):
|
||||||
|
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
|
||||||
|
|||||||
@ -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):
|
||||||
@ -144,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 #
|
||||||
@ -377,23 +439,50 @@ class PackageSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_readers(obj: Package):
|
def resolve_readers(obj: Package):
|
||||||
users = User.objects.filter(
|
readers = []
|
||||||
id__in=UserPackagePermission.objects.filter(
|
|
||||||
package=obj, permission=UserPackagePermission.READ[0]
|
|
||||||
).values_list("user", flat=True)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
return [{u.id: u.get_name()} for u in users]
|
user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=user_ids).distinct()
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||||
|
|
||||||
|
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
|
||||||
|
"group", flat=True
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||||
|
|
||||||
|
return readers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_writers(obj: Package):
|
def resolve_writers(obj: Package):
|
||||||
users = User.objects.filter(
|
writers = []
|
||||||
id__in=UserPackagePermission.objects.filter(
|
|
||||||
package=obj, permission=UserPackagePermission.WRITE[0]
|
|
||||||
).values_list("user", flat=True)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
return [{u.id: u.get_name()} for u in users]
|
user_ids = UserPackagePermission.objects.filter(
|
||||||
|
package=obj,
|
||||||
|
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=user_ids).distinct()
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||||
|
|
||||||
|
group_ids = GroupPackagePermission.objects.filter(
|
||||||
|
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
|
||||||
|
).values_list("group", flat=True)
|
||||||
|
|
||||||
|
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||||
|
|
||||||
|
return writers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_comment(obj):
|
def resolve_review_comment(obj):
|
||||||
@ -758,6 +847,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")
|
||||||
@ -769,6 +859,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
|
||||||
)
|
)
|
||||||
@ -790,6 +899,27 @@ def delete_compound(request, package_uuid, compound_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCompoundStructure(Schema):
|
||||||
|
smiles: str
|
||||||
|
name: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
inchi: str | None = None
|
||||||
|
molfile: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
|
||||||
|
def create_package_compound_structure(
|
||||||
|
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
||||||
|
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
|
||||||
|
return redirect(cs.url)
|
||||||
|
except ValueError as e:
|
||||||
|
return 400, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||||
)
|
)
|
||||||
@ -1316,6 +1446,7 @@ class ScenarioSchema(Schema):
|
|||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||||
collectionID: Optional[str] = None
|
collectionID: Optional[str] = None
|
||||||
|
date: str = Field(None, alias="scenario_date")
|
||||||
description: str = Field(None, alias="description")
|
description: str = Field(None, alias="description")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
identifier: str = "scenario"
|
identifier: str = "scenario"
|
||||||
@ -1329,7 +1460,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):
|
||||||
@ -1394,7 +1532,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.get("adInfoTypes[]", "").split(",")
|
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))
|
||||||
|
|
||||||
@ -1436,6 +1578,77 @@ 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)
|
||||||
|
|
||||||
|
if request.POST.get("adInfoTypes[]"):
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif request.POST.get("ais"):
|
||||||
|
import json
|
||||||
|
|
||||||
|
parsed_ais = json.loads(request.POST.get("ais"))
|
||||||
|
|
||||||
|
for ai_type, ais in parsed_ais.items():
|
||||||
|
for ai in ais:
|
||||||
|
attach_obj = None
|
||||||
|
if ai.get("related"):
|
||||||
|
url_parser = EPDBURLParser(ai.get("related").get("url"))
|
||||||
|
attach_obj = url_parser.get_object()
|
||||||
|
|
||||||
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
|
if not attach_obj.url.startswith(p.url):
|
||||||
|
raise ValueError(
|
||||||
|
"Additional Information can only be set to objects stored in the same package!"
|
||||||
|
)
|
||||||
|
|
||||||
|
AdditionalInformation.create(
|
||||||
|
p,
|
||||||
|
AdditionalInformation.from_dict(ai_type, ai),
|
||||||
|
scenario=scenario,
|
||||||
|
content_object=attach_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO implement additional information endpoint ?
|
||||||
|
return redirect(f"{scenario.url}")
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Pathway #
|
# Pathway #
|
||||||
###########
|
###########
|
||||||
@ -1477,6 +1690,7 @@ class PathwayNode(Schema):
|
|||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||||
|
pseudo: bool = Field(False, alias="pseudo")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_atom_count(obj: Node):
|
def resolve_atom_count(obj: Node):
|
||||||
@ -1630,6 +1844,29 @@ def create_package_pathway(
|
|||||||
return 403, {"message": str(e)}
|
return 403, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
|
def update_pathway(request, package_uuid, pathway_uuid):
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
|
||||||
|
if request.POST.get("scenario"):
|
||||||
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
|
||||||
|
|
||||||
|
pw.scenarios.add(scen)
|
||||||
|
pw.save()
|
||||||
|
|
||||||
|
return redirect(f"{pw.url}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return 400, {"message": "No scenario specified!"}
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||||
try:
|
try:
|
||||||
@ -1738,25 +1975,67 @@ 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(
|
||||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||||
response={200: str | Any, 403: Error},
|
response={200: str | Any, 400: Error, 403: Error},
|
||||||
)
|
)
|
||||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
|
||||||
|
# TODO Code Dup from bayer.views
|
||||||
|
|
||||||
|
if n.pesLink:
|
||||||
|
from bayer.views import fetch_pes
|
||||||
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
|
try:
|
||||||
|
pes_data = fetch_pes(request, n.pesLink)
|
||||||
|
except ValueError as e:
|
||||||
|
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
||||||
|
|
||||||
|
classification = pes_data.get("classificationLevel", "")
|
||||||
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
|
if p.classification_level != Package.Classification.SECRET:
|
||||||
|
return 400, "Cannot create PESs for non-secret packages."
|
||||||
|
|
||||||
|
data_pools = pes_data.get("dataPools")
|
||||||
|
if data_pools:
|
||||||
|
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||||
|
return 400, {
|
||||||
|
"messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"
|
||||||
|
}
|
||||||
|
|
||||||
|
c = PESCompound.create(p, pes_data, n.nodeName, n.nodeReason)
|
||||||
|
|
||||||
|
node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure)
|
||||||
|
if node_qs.exists():
|
||||||
|
return redirect(pw.url)
|
||||||
|
|
||||||
|
node = Node()
|
||||||
|
node.stereo_removed = False
|
||||||
|
node.pathway = pw
|
||||||
|
node.depth = 0
|
||||||
|
|
||||||
|
node.default_node_label = c.default_structure
|
||||||
|
node.save()
|
||||||
|
|
||||||
|
node.node_labels.add(c.default_structure)
|
||||||
|
node.save()
|
||||||
|
else:
|
||||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||||
node_depth = int(n.nodeDepth)
|
node_depth = int(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!"}
|
||||||
|
|
||||||
@ -1866,13 +2145,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,
|
||||||
)
|
)
|
||||||
@ -1883,7 +2165,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,
|
||||||
)
|
)
|
||||||
@ -1895,6 +2178,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
for pr in e.products.split(","):
|
for pr in e.products.split(","):
|
||||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
||||||
|
|
||||||
|
multi_step = False
|
||||||
|
if e.multistep and e.multistep.strip() == "true":
|
||||||
|
multi_step = True
|
||||||
|
|
||||||
new_e = Edge.create(
|
new_e = Edge.create(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
start_nodes=educts,
|
start_nodes=educts,
|
||||||
@ -1902,11 +2189,15 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
rule=None,
|
rule=None,
|
||||||
name=None,
|
name=None,
|
||||||
description=e.edgeReason,
|
description=e.edgeReason,
|
||||||
|
multi_step=multi_step,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update depths as sideeffect of above operation
|
||||||
|
pw.update_depths()
|
||||||
|
|
||||||
return redirect(new_e.url)
|
return redirect(new_e.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding node failed!"}
|
return 403, {"message": "Adding Edge failed!"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
||||||
|
|||||||
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',),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
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),
|
||||||
|
]
|
||||||
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-06-02 17:18
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import DOI
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
refs = AdditionalInformation.objects.filter(type="Reference")
|
||||||
|
|
||||||
|
remaining = []
|
||||||
|
|
||||||
|
for ref in refs:
|
||||||
|
r = ref.data["reference"]
|
||||||
|
try:
|
||||||
|
# PubMed IDs are plain ints, try parsing
|
||||||
|
_ = int(r)
|
||||||
|
# Nothing to do
|
||||||
|
except ValueError:
|
||||||
|
DOMAINS = [
|
||||||
|
"http://dx.doi.org/",
|
||||||
|
"https://dx.doi.org/",
|
||||||
|
"http://doi.org/",
|
||||||
|
"https://doi.org/",
|
||||||
|
]
|
||||||
|
for d in DOMAINS:
|
||||||
|
r = r.replace(d, "")
|
||||||
|
|
||||||
|
if r.startswith("10."):
|
||||||
|
ref.type = DOI.__name__
|
||||||
|
ref.data = {"doi": r}
|
||||||
|
ref.save()
|
||||||
|
else:
|
||||||
|
remaining.append(ref)
|
||||||
|
|
||||||
|
if len(remaining) > 0:
|
||||||
|
raise ValueError(f"Could not parse {len(remaining)} references")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0025_auto_20260511_2025"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
321
epdb/models.py
321
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"
|
||||||
)
|
)
|
||||||
@ -866,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()
|
||||||
@ -1112,6 +1121,7 @@ class CompoundStructure(
|
|||||||
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
||||||
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey")
|
||||||
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
normalized_structure = models.BooleanField(null=False, blank=False, default=False)
|
||||||
|
molfile = models.TextField(blank=True, null=True, verbose_name="Molfile")
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
@ -1208,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)
|
||||||
@ -1690,7 +1703,7 @@ class Reaction(
|
|||||||
if name is not None and name.strip() != "":
|
if name is not None and name.strip() != "":
|
||||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if description is not None and name.strip() != "":
|
if description is not None and description.strip() != "":
|
||||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
r.multi_step = multi_step
|
r.multi_step = multi_step
|
||||||
@ -1768,7 +1781,7 @@ class Reaction(
|
|||||||
return new_reaction
|
return new_reaction
|
||||||
|
|
||||||
def smirks(self):
|
def smirks(self):
|
||||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
return f"{'.'.join([cs.smiles for cs in self.educts.all().order_by('-pk')])}>>{'.'.join([cs.smiles for cs in self.products.all().order_by('-pk')])}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
@ -1881,6 +1894,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
if n not in queue:
|
if n not in queue:
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
|
|
||||||
|
for i in queue:
|
||||||
|
processed.add(i)
|
||||||
|
|
||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
@ -2173,6 +2189,50 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
):
|
):
|
||||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||||
|
|
||||||
|
def update_depths(self):
|
||||||
|
# Collect number of in and out links per node
|
||||||
|
in_count = defaultdict(lambda: 0)
|
||||||
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
for e in self.edges:
|
||||||
|
for react in e.start_nodes.all():
|
||||||
|
out_count[str(react.uuid)] += 1
|
||||||
|
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
in_count[str(prod.uuid)] += 1
|
||||||
|
|
||||||
|
depth_map = {}
|
||||||
|
depth_map[0] = list()
|
||||||
|
processed = set()
|
||||||
|
|
||||||
|
for n in self.root_nodes:
|
||||||
|
depth_map[0].append(n)
|
||||||
|
|
||||||
|
# At most depth len(nodes) is possible
|
||||||
|
for i in range(self.nodes.count()):
|
||||||
|
level_nodes = depth_map.get(i, [])
|
||||||
|
|
||||||
|
if len(level_nodes) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
unique_next_level = set()
|
||||||
|
for n in level_nodes:
|
||||||
|
processed.add(n)
|
||||||
|
for e in self.edges:
|
||||||
|
if n in e.start_nodes.all():
|
||||||
|
for p in e.end_nodes.all():
|
||||||
|
if p not in processed:
|
||||||
|
unique_next_level.add(p)
|
||||||
|
|
||||||
|
if len(unique_next_level) > 0:
|
||||||
|
depth_map[i + 1] = list(unique_next_level)
|
||||||
|
|
||||||
|
for depth, nodes in depth_map.items():
|
||||||
|
for n in nodes:
|
||||||
|
if n.depth != depth and depth != 0:
|
||||||
|
n.depth = depth
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
|
||||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2211,10 +2271,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,
|
||||||
@ -2223,6 +2287,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()],
|
||||||
@ -2235,8 +2300,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(
|
||||||
@ -2392,6 +2460,8 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
rule: Optional[Rule] = None,
|
rule: Optional[Rule] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
e = Edge()
|
e = Edge()
|
||||||
e.pathway = pathway
|
e.pathway = pathway
|
||||||
@ -2421,7 +2491,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||||
rules=rule,
|
rules=rule,
|
||||||
multi_step=False,
|
multi_step=kwargs.get("multi_step", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
e.edge_label = r
|
e.edge_label = r
|
||||||
@ -2499,6 +2569,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,
|
||||||
@ -3821,6 +3892,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)
|
||||||
|
|
||||||
@ -4191,7 +4467,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
|
||||||
@ -4224,18 +4499,22 @@ class AdditionalInformation(models.Model):
|
|||||||
|
|
||||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||||
|
|
||||||
def get(self) -> "EnviPyModel":
|
@staticmethod
|
||||||
|
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
try:
|
try:
|
||||||
inst = MAPPING[self.type](**self.data)
|
inst = MAPPING[ai_type](**ai_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading {self.type}: {e}")
|
print(f"Error loading {ai_type}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
inst.__dict__["uuid"] = str(self.uuid)
|
return inst
|
||||||
|
|
||||||
|
def get(self) -> "EnviPyModel":
|
||||||
|
inst = AdditionalInformation.from_dict(self.type, self.data)
|
||||||
|
inst.__dict__["uuid"] = str(self.uuid)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -4252,7 +4531,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))
|
||||||
),
|
),
|
||||||
@ -4260,7 +4539,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)
|
||||||
|
|||||||
191
epdb/views.py
191
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)
|
||||||
@ -948,20 +1023,34 @@ def package_models(request, package_uuid):
|
|||||||
|
|
||||||
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"
|
||||||
@ -3015,12 +3113,21 @@ def jobs(request):
|
|||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||||
]
|
]
|
||||||
if current_user.is_superuser:
|
# if current_user.is_superuser:
|
||||||
context["jobs"] = JobLog.objects.all().order_by("-created")
|
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||||
else:
|
# else:
|
||||||
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
|
|
||||||
return render(request, "collections/joblog.html", context)
|
# Context for paginated template
|
||||||
|
context["entity_type"] = "joblog"
|
||||||
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
|
||||||
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
|
context["list_title"] = "joblog"
|
||||||
|
context["list_mode"] = "combined"
|
||||||
|
|
||||||
|
return render(request, "collections/joblog_paginated.html", context)
|
||||||
|
|
||||||
|
# return render(request, "collections/joblog.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
job_name = request.POST.get("job-name")
|
job_name = request.POST.get("job-name")
|
||||||
|
|||||||
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
73
epiuclid/schemas/v10/platform-attachment.xsd
Normal file
73
epiuclid/schemas/v10/platform-attachment.xsd
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns="http://iuclid6.echa.europa.eu/namespaces/platform-attachment/v1"
|
||||||
|
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
targetNamespace="http://iuclid6.echa.europa.eu/namespaces/platform-attachment/v1"
|
||||||
|
elementFormDefault="qualified" attributeFormDefault="qualified">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:appinfo>XML Schema Definition of the IUCLID6 Attachment entity</xs:appinfo>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:import namespace="http://www.w3.org/1999/xlink" schemaLocation="xlink.xsd"/>
|
||||||
|
<xs:element name="Attachment">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Defines the attachment metadata information</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="documentKey" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The unique identifier of the attachment</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="name" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The name of the uploaded attachment</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="creationDate" type="xs:dateTime">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The date that the attachment was created</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="lastModificationDate" type="xs:dateTime">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The last modification date of the attachment</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="remarks" type="xs:string" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The remarks provided by the user during the attachment uploading</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="md5" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The MD5 hash of the uploaded attachment content</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="mimetype" type="xs:string" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The media type of the attachment content</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element type="xs:boolean" name="symbolic" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Indicates that the actual attachment file is not included in the i6z file</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="content" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The name/location of the attachment binary under the "attachments" directory inside the i6z archive file</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute ref="xlink:type"/>
|
||||||
|
<xs:attribute ref="xlink:href"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="AttachmentRef" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Specifies the unique identifier of an attachment that is directly linked to a IUCLID6 document</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:schema>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user