Compare commits

..
44 Commits
Author SHA1 Message Date
jebusandTim Lorsbach fedd1b5280 [Feature] Management Command to Compute and Store Reaction <-> Rule Associations (#453)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#453
2026-08-14 20:45:12 +12:00
jebusandTim Lorsbach 2d3dca6a75 [Fix] Adjust Rules according to manual QA, Add missing Rule (#452)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#452
2026-08-13 17:37:38 +12:00
jebusandTim Lorsbach ada270aa3c [Fix] Canonicalize SMILES when checking overlaps in smiles_covered_by (#443)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#443
2026-08-12 21:01:41 +12:00
jebusandTim Lorsbach 093daa5ecf [Fix] 'Object of type ConfidenceLevel is not JSON serializable' in Pathway.d3_json (#450)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#450
2026-08-07 19:58:36 +12:00
jebusandTim Lorsbach f4f284925a [Fix] Entrypoint Location (#446)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#446
2026-08-05 09:15:09 +12:00
jebusandTim Lorsbach ca6e926b30 [Fix] Fix Image Tags (#445)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#445
2026-08-05 08:55:35 +12:00
jebusandTim Lorsbach 2504d7045b [Chore] Build Docker Images on pushes to develop, main (#444)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#444
2026-08-05 08:36:24 +12:00
jebusandTim Lorsbach 032ebc30a2 [Fix] Return a File instead of raw text for new export endpoint (#442)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#442
2026-07-31 08:24:14 +12:00
jebusandTim Lorsbach 703f377b7f [Fix] Export IUCLID Properties, Show Model Params and Statistics, Adjust Batch Prediction Settings (#434)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#434
2026-07-30 08:30:18 +12:00
jebusandTim Lorsbach 7639b23e4e [Feature] Package Pathway Export to CSV (#439)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#439
2026-07-30 08:03:01 +12:00
jebusandTim Lorsbach 7632b3a029 [Fix] All thresholds, Increase Batch Predict Limit, Make "Back" Button on error page trigger a new pageload. (#432)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#432
2026-07-24 09:29:05 +12:00
jebusandTim Lorsbach d657c0285a [Fix] Catch invalid reactions smiles on reaction creation (#431)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#431
2026-07-23 07:45:36 +12:00
jebusandTim Lorsbach f4c198981b [Feature] SPathway to_json outputs Bayes Probabilities (#430)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#430
2026-07-23 07:20:26 +12:00
t03i cdd51fc7aa [Fix] Iuclid export produce only one root substance (#429)
Reviewed-on: enviPath/enviPy#429
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2026-07-23 06:37:02 +12:00
jebusandTim Lorsbach ac8df05913 [Feature] Attach AdditionalInformation to Nodes (#428)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#428
2026-07-20 09:32:28 +12:00
jebusandTim Lorsbach 31c57299e4 [Feature] Ketcher V3 (#427)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#427
2026-07-17 08:16:09 +12:00
jebusandTim Lorsbach 3566571b42 [Chore] Leftovers (#426)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#426
2026-07-16 21:33:12 +12:00
jebusandTim Lorsbach 72a63b4876 [Feature] Sync Features from Client Repo (#425)
- New PackageImporter
- Fix persisting Molfile on Structure creation
- Add NonPersistent Prediction
- Pathway View Options
- Edit Node Fix

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#425
2026-07-16 07:50:46 +12:00
jebusandTim Lorsbach 2c2437e3f5 [Feature] Generate Compounds by Molfile (#423)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#423
2026-07-15 23:56:24 +12:00
jebusandTim Lorsbach 9bc9f86ff1 [Fix] Pathway SVG Export has Edges again, Fix In/Out Edge coloring (#422)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#422
2026-07-03 07:24:02 +12:00
jebusandTim Lorsbach dba6514013 [Chore] Package Export, Blanks for fields, Styling (#421)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#421
2026-07-02 20:12:26 +12:00
jebusandTim Lorsbach a092d4a558 [Chore] Updated Dockerfile (#418)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#418
2026-06-17 08:33:02 +12:00
jebusandTim Lorsbach 2502c020f7 [Fix] Collection of minor Bugfixes (#417)
User feedback from on-prem installation

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#417
2026-06-17 07:56:32 +12:00
jebusandTim Lorsbach 6ab9180291 [Feature] Vizualize Reaction Circles (#410)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#410
2026-06-11 02:07:49 +12:00
jebusandTim Lorsbach 3657d14659 [Fix] Cascade deletion of Reactions to Edges (#409)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#409
2026-06-11 00:24:06 +12:00
jebusandTim Lorsbach ef6091d416 [Fix] Set depth to -1 when adding a Node via Pathway.add_node to be in sync with legacy_api (#408)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#408
2026-06-09 23:55:16 +12:00
jebusandTim Lorsbach 14cfc1e4d7 [Feature] Integrate DOI Links, Handle Cycles in Pathway Viz (#407)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#407
2026-06-03 06:00:38 +12:00
jebusandTim Lorsbach 868bbf5c05 [Fix] False as multi_step default in legacy API (#406)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#406
2026-05-29 08:12:52 +12:00
jebusandTim Lorsbach be5ee1d1d7 [Fix] Propagate multi_step in Edge.create to Reaction.create to ensure deduplication is working (#405)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#405
2026-05-29 07:39:32 +12:00
jebusandTim Lorsbach 20fd949dfd [Fix] Implement legacy PFASConfidence object construction (#404)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#404
2026-05-28 23:57:34 +12:00
jebusandTim Lorsbach c9b643fe6e [Feature] Make JobLog Page Paginated (#403)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#403
2026-05-28 23:01:27 +12:00
jebusandTim Lorsbach 1a9f1cf9af [Fix] Legacy API Node Depth Parsing, description validation in Reaction.create (#402)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#402
2026-05-28 09:53:39 +12:00
jebusandTim Lorsbach c7c7e17e43 [Feature] Implement legacy endpoints to enable copy via python client (#400)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#400
2026-05-28 01:41:28 +12:00
jebusandTim Lorsbach 674e10c7fa [Fix] Legacy API Package Endpoint, Pepper Unit in Pathway View (#399)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#399
2026-05-27 21:09:17 +12:00
jebusandTim Lorsbach 8079b80d57 [Fix] Fix Typo in multi_step assignment (#397)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#397
2026-05-21 09:56:21 +12:00
jebusandTim Lorsbach 76e63fda2c [Fix] Fix BART Upload (#396)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#396
2026-05-21 03:16:02 +12:00
jebusandTim Lorsbach 1e43c298d2 [Fix] Simplify Depth adjustment (#386)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#386
2026-05-12 21:04:56 +12:00
jebusandTim Lorsbach b39fc7eaf8 [Fix] Update Node depth when adding new Edges to a Pathway (#384)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#384
2026-05-12 09:40:35 +12:00
jebusandTim Lorsbach a2fc9f72cb [Feature] Make use of HalfLifeModel Enum (#383)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#383
2026-05-12 09:23:56 +12:00
t03i 734b02767e [Fix] Update plotting imports and thread handling in Pepper class (#382)
- plt.subplot does not work reliably with async/ threads.
- Bug in thread run that would fail with env set (string to number)

Reviewed-on: enviPath/enviPy#382
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2026-05-12 06:43:26 +12:00
jebusandTim Lorsbach 9d70db2ca2 [Fix] Wrong indentation in welcome mail (#373)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#373
2026-04-22 08:47:05 +12:00
jebusandTim Lorsbach fec26d0233 [Feature] Admin Actions for Activation and Affiliation Request (#372)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#372
2026-04-22 08:36:31 +12:00
jebusandTim Lorsbach 689f7998eb [Dep] Updated enviFormer, additional information lib, aiohttp, fsspec (#371)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#371
2026-04-22 06:38:31 +12:00
jebusandTim Lorsbach 8498e59fa1 [Feature] Changes required for non public tenants (#370)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#370
2026-04-22 06:08:39 +12:00
22645 changed files with 8907 additions and 2195030 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Build Docker Image
# Trigger when a PR to main/develop is completed.
on:
pull_request:
branches:
- main
- develop
types:
- closed
jobs:
build-and-push:
if: ${{ github.event.pull_request.merged == true }}
runs-on: ubuntu-latest
steps:
# Fetch the repository content for the Docker build context.
- name: Checkout repository
uses: actions/checkout@v4
# Enable Buildx for BuildKit features (incl. SSH mount support).
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Authenticate against the container registry before pushing images.
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: git.envipath.com
username: ${{ secrets.CI_REGISTRY_USER }}
password: ${{ secrets.CI_REGISTRY_PASSWORD }}
# Generate image tags/labels:
# - PRs targeting main get "latest" and "main-sha"
# - PRs targeting develop get "dev" and "dev-sha"
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: git.envipath.com/envipath/envipy
tags: |
type=raw,value=latest,enable=${{ github.event.pull_request.base.ref == 'main' }}
type=sha,prefix=main-,enable=${{ github.event.pull_request.base.ref == 'main' }}
type=raw,value=dev,enable=${{ github.event.pull_request.base.ref == 'develop' }}
type=sha,prefix=dev-,enable=${{ github.event.pull_request.base.ref == 'develop' }}
# Load SSH key so Docker can pull private git+ssh dependencies during build.
- name: Setup SSH for private git dependencies
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ENVIPY_CI_PRIVATE_KEY }}
# Build and push the production image; forward SSH agent without registry cache reuse.
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
ssh: default
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -2
View File
@@ -7,10 +7,10 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
exclude: epiuclid/schemas/ exclude: epiuclid/schemas/
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: epiuclid/schemas/ exclude: ^epiuclid/schemas/|^static/js/ketcher3/
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
exclude: ^static/images/|^epiuclid/schemas/|^fixtures/ exclude: ^static/images/|^epiuclid/schemas/|^fixtures/|^static/js/ketcher3/
- 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
+18 -8
View File
@@ -6,18 +6,23 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \ build-essential \
libpq-dev \ libpq-dev \
curl \ curl \
openssh-client \ openssh-client \
git \ git \
nodejs \ ca-certificates \
npm \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install pnpm # Install Node 22 + pnpm
RUN npm install -g pnpm RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& corepack enable \
&& corepack prepare pnpm@latest --activate \
&& rm -rf /var/lib/apt/lists/*
RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}" ENV PATH="/root/.local/bin:${PATH}"
@@ -32,17 +37,17 @@ RUN mkdir -p -m 0700 /root/.ssh \
# We'll need access to private repos, let docker make use of host ssh agent and use it like: # We'll need access to private repos, let docker make use of host ssh agent and use it like:
# docker build --ssh default -t envipath/envipy:1.0 . # docker build --ssh default -t envipath/envipy:1.0 .
RUN --mount=type=ssh \ RUN --mount=type=ssh \
uv sync --locked --extra ms-login --extra pepper-plugin uv sync --locked --extra pepper-plugin
# Now copy source and do a final sync to install the project itself # Now copy source and do a final sync to install the project itself
# Ensure .dockerignore is reasonable # Ensure .dockerignore is reasonable
COPY bayer bayer
COPY bridge bridge
COPY biotransformer biotransformer COPY biotransformer biotransformer
COPY bridge bridge
COPY envipath envipath COPY envipath envipath
COPY epapi epapi COPY epapi epapi
COPY epauth epauth COPY epauth epauth
COPY epdb epdb COPY epdb epdb
COPY epiuclid epiuclid
COPY fixtures fixtures COPY fixtures fixtures
COPY migration migration COPY migration migration
COPY pepper pepper COPY pepper pepper
@@ -53,6 +58,10 @@ COPY tests tests
COPY utilities utilities COPY utilities utilities
COPY manage.py . COPY manage.py .
# Used to run migrations etc
COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh
# Install frontend deps # Install frontend deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -95,4 +104,5 @@ USER django
EXPOSE 8000 EXPOSE 8000
CMD ["gunicorn", "envipath.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"] ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "envipath.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "8"]
View File
-3
View File
@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.
-6
View File
@@ -1,6 +0,0 @@
from django.apps import AppConfig
class BayerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bayer'
-35
View File
@@ -1,35 +0,0 @@
# Generated by Django 5.2.7 on 2026-03-06 10:51
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Package',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
('classification_level', models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted'), (20, 'Secret')], default=10)),
],
options={
'db_table': 'epdb_package',
},
),
]
-22
View File
@@ -1,22 +0,0 @@
# Generated by Django 5.2.7 on 2026-03-06 10:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('bayer', '0001_initial'),
('epdb', '0019_remove_scenario_additional_information_and_more'),
]
operations = [
migrations.AddField(
model_name='package',
name='license',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License'),
),
]
@@ -1,20 +0,0 @@
# Generated by Django 6.0.3 on 2026-04-14 19:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bayer', '0002_initial'),
('epdb', '0023_alter_compoundstructure_options_and_more'),
]
operations = [
migrations.AddField(
model_name='package',
name='data_pool',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'),
),
]
View File
-98
View File
@@ -1,98 +0,0 @@
from typing import List
from django.conf import settings as s
from django.db import models
from django.db.models import QuerySet
from epdb.models import (
EnviPathModel,
ParallelRule,
SequentialRule,
SimpleAmbitRule,
SimpleRDKitRule,
)
class Package(EnviPathModel):
reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False)
license = models.ForeignKey(
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
)
class Classification(models.IntegerChoices):
INTERNAL = 0, "Internal"
RESTRICTED = 10 , "Restricted"
SECRET = 20, "Secret"
classification_level = models.IntegerField(
choices=Classification,
default=Classification.RESTRICTED,
)
data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True,
verbose_name="Data pool", default=None)
def delete(self, *args, **kwargs):
# explicitly handle related Rules
for r in self.rules.all():
r.delete()
super().delete(*args, **kwargs)
def __str__(self):
return f"{self.name} (pk={self.pk})"
@property
def compounds(self) -> QuerySet:
return self.compound_set.all()
@property
def rules(self) -> QuerySet:
return self.rule_set.all()
@property
def reactions(self) -> QuerySet:
return self.reaction_set.all()
@property
def pathways(self) -> QuerySet:
return self.pathway_set.all()
@property
def scenarios(self) -> QuerySet:
return self.scenario_set.all()
@property
def models(self) -> QuerySet:
return self.epmodel_set.all()
def _url(self):
return "{}/package/{}".format(s.SERVER_URL, self.uuid)
def get_applicable_rules(self) -> List["Rule"]:
"""
Returns a ordered set of rules where the following applies:
1. All Composite will be added to result
2. All SimpleRules will be added if theres no CompositeRule present using the SimpleRule
Ordering is based on "url" field.
"""
rules = []
rule_qs = self.rules
reflected_simple_rules = set()
for r in rule_qs:
if isinstance(r, ParallelRule) or isinstance(r, SequentialRule):
rules.append(r)
for sr in r.simple_rules.all():
reflected_simple_rules.add(sr)
for r in rule_qs:
if isinstance(r, SimpleAmbitRule) or isinstance(r, SimpleRDKitRule):
if r not in reflected_simple_rules:
rules.append(r)
rules = sorted(rules, key=lambda x: x.url)
return rules
class Meta:
db_table = "epdb_package"
@@ -1,181 +0,0 @@
{% load static %}
<dialog
id="new_package_modal"
class="modal"
x-data="{
isSubmitting: false,
packageClassification: null,
reset() {
this.isSubmitting = false;
this.selectedType = '';
this.buildAppDomain = false;
this.requiresRulePackages = false;
this.requiresDataPackages = false;
this.additional_parameters = null;
},
setFormData(data) {
this.formData = data;
},
get isSecret() {
return this.packageClassification === '20';
},
submit(formId) {
const form = document.getElementById(formId);
// Remove previously injected inputs
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
// Add values from dynamic form into the html form
if (this.formData) {
Object.entries(this.formData).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
input.classList.add('dynamic-param');
form.appendChild(input);
});
}
if (form && form.checkValidity()) {
this.isSubmitting = true;
form.submit();
} else if (form) {
form.reportValidity();
}
}
}"
@close="reset()"
>
<div class="modal-box max-w-3xl">
<!-- Header -->
<h3 class="text-lg font-bold">New Package</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
✕
</button>
</form>
<!-- Body -->
<div class="py-4">
<form
id="new_package_form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %}
<!-- Name -->
<div class="form-control mb-3">
<label class="label" for="package-name">
<span class="label-text">Name</span>
</label>
<input
id="package-name"
class="input input-bordered w-full"
name="package-name"
placeholder="Name"
required
/>
</div>
<!-- Description -->
<div class="form-control mb-3">
<label class="label" for="package-description">
<span class="label-text">Description</span>
</label>
<input
id="package-description"
type="text"
class="input input-bordered w-full"
placeholder="Description..."
name="package-description"
/>
</div>
<!-- Classification Level -->
<div class="form-control mb-3">
<label class="label" for="package-classification">
<span class="label-text">Package Classification</span>
</label>
<select
id="package-classification"
name="package-classification"
class="select select-bordered w-full"
x-model="packageClassification"
required
>
<option value="null" disabled selected>Select Classification</option>
<option value="0">Internal</option>
<option value="10">Restricted</option>
<option value="20">Secret</option>
</select>
</div>
<!-- Secret Groups -->
<div class="form-control mb-3" x-show="isSecret" x-cloak>
<label class="label" for="package-data-pool">
<span class="label-text">Data Pool for SECRET Package</span>
</label>
<p>Only users with this role can be granted access to this package</p>
<select
id="package-data-pool"
name="package-data-pool"
class="select select-bordered w-full"
>
<option value="" disabled selected>Select Data Pool</option>
{% for obj in meta.available_groups %}
{% if obj.secret %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('new_package_form')"
:disabled="isSubmitting || !selectedType || loadingSchemas"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div>
</div>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
-97
View File
@@ -1,97 +0,0 @@
{% extends "framework_modern.html" %}
{% block content %}
{% block action_modals %}
{% include "modals/objects/edit_package_modal.html" %}
{% include "modals/objects/edit_package_permissions_modal.html" %}
{% include "modals/objects/publish_package_modal.html" %}
{% include "modals/objects/set_license_modal.html" %}
{% include "modals/objects/export_package_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="space-y-2 p-4">
<!-- Header Section -->
<div class="card bg-base-100">
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-2xl">{{ package.name }} - ({{ package.get_classification_level_display }})</h2>
<div id="actionsButton" class="dropdown dropdown-e nd hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-wrench"
>
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
/>
</svg>
Actions
</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
>
{% block actions %}
{% include "actions/objects/package.html" %}
{% endblock %}
</ul>
</div>
</div>
<p class="mt-2">{{ package.description|safe }}</p>
<ul class="menu bg-base-200 rounded-box mt-4 w-full">
<li>
<a href="{{ package.url }}/pathway" class="hover:bg-base-300"
>Pathways ({{ package.pathways.count }})</a
>
</li>
<li>
<a href="{{ package.url }}/rule" class="hover:bg-base-300"
>Rules ({{ package.rules.count }})</a
>
</li>
<li>
<a href="{{ package.url }}/compound" class="hover:bg-base-300"
>Compounds ({{ package.compounds.count }})</a
>
</li>
<li>
<a href="{{ package.url }}/reaction" class="hover:bg-base-300"
>Reactions ({{ package.reactions.count }})</a
>
</li>
<li>
<a href="{{ package.url }}/model" class="hover:bg-base-300"
>Models ({{ package.models.count }})</a
>
</li>
<li>
<a href="{{ package.url }}/scenario" class="hover:bg-base-300"
>Scenarios ({{ package.scenarios.count }})</a
>
</li>
</ul>
</div>
</div>
</div>
<script>
// Show actions button if there are actions
document.addEventListener("DOMContentLoaded", function () {
const actionsButton = document.getElementById("actionsButton");
const actionsList = actionsButton?.querySelector("ul");
if (actionsList && actionsList.children.length > 0) {
actionsButton?.classList.remove("hidden");
}
});
</script>
{% endblock content %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
-3
View File
@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.
+21 -1
View File
@@ -1,4 +1,5 @@
import enum import enum
from typing import Any, Dict
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from envipy_additional_information import EnviPyModel from envipy_additional_information import EnviPyModel
@@ -69,6 +70,12 @@ class Plugin(ABC):
class Property(Plugin): class Property(Plugin):
def parameters(self) -> Dict[str, Any]:
"""
Returns the parameters of the PropertyPlugin.
"""
return {}
@classmethod @classmethod
@abstractmethod @abstractmethod
def requires_rule_packages(cls) -> bool: def requires_rule_packages(cls) -> bool:
@@ -254,7 +261,14 @@ class Classifier(Plugin):
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None: def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
if cls.Config is None: if cls.Config is None:
return None return None
return cls.Config(**(data or {}))
# remove empty strings a.k.a unset params to not overwrite defaults
cpy = {}
if data is not None:
for k, v in data.items():
if v != "":
cpy[k] = v
return cls.Config(**cpy)
@classmethod @classmethod
def create(cls, data: dict | None = None): def create(cls, data: dict | None = None):
@@ -293,6 +307,12 @@ class Classifier(Plugin):
""" """
pass pass
def parameters(self) -> Dict[str, Any]:
"""
Returns the parameters of the ClassifierPlugin.
"""
return {}
@abstractmethod @abstractmethod
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None: def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
""" """
+7 -35
View File
@@ -1,15 +1,15 @@
services: services:
db: db:
image: postgres:18 image: postgres:18
container_name: eppostgres container_name: envipath-postgres
environment: environment:
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: postgres
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: envipath
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- ep_bayer_postgres_data:/var/lib/postgresql - postgres_data:/var/lib/postgresql
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"] test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s interval: 5s
@@ -18,37 +18,9 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: epredis container_name: envipath-redis
ports: ports:
- "6379:6379" - "6379:6379"
volumes:
- ep_bayer_redis_data:/data
biotransformer3:
image: envipath/biotransformer3:1.0
container_name: epbiotransformer3
# web:
# image: envipath/envipy-bayer:1.0
# container_name: epdjango
# ports:
# - "127.0.0.1:8000:8000"
# env_file:
# - .env
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
# volumes:
# - ep_bayer_data:/opt/enviPy/
celery_worker:
image: envipath/envipy-bayer:1.0
container_name: epcelery
env_file:
- .env.dev
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
volumes:
- ep_bayer_data:/opt/enviPy/
volumes: volumes:
ep_bayer_postgres_data: postgres_data:
ep_bayer_redis_data:
ep_bayer_data:
+9 -9
View File
@@ -7,7 +7,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- ep_bayer_postgres_data:/var/lib/postgresql - ep_postgres_data:/var/lib/postgresql
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ] test: [ "CMD-SHELL", "pg_isready -U postgres" ]
interval: 5s interval: 5s
@@ -18,14 +18,14 @@ services:
image: redis:7-alpine image: redis:7-alpine
container_name: epredis container_name: epredis
volumes: volumes:
- ep_bayer_redis_data:/data - ep_redis_data:/data
biotransformer3: biotransformer3:
image: envipath/biotransformer3:1.0 image: envipath/biotransformer3:1.0
container_name: epbiotransformer3 container_name: epbiotransformer3
web: web:
image: envipath/envipy-bayer:1.0 image: envipath/envipy:1.0
container_name: epdjango container_name: epdjango
ports: ports:
- "127.0.0.1:8000:8000" - "127.0.0.1:8000:8000"
@@ -33,18 +33,18 @@ services:
- .env - .env
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3 command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
volumes: volumes:
- ep_bayer_data:/opt/enviPy/ - ep_data:/opt/enviPy/
celery_worker: celery_worker:
image: envipath/envipy-bayer:1.0 image: envipath/envipy:1.0
container_name: epcelery container_name: epcelery
env_file: env_file:
- .env - .env
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
volumes: volumes:
- ep_bayer_data:/opt/enviPy/ - ep_data:/opt/enviPy/
volumes: volumes:
ep_bayer_postgres_data: ep_postgres_data:
ep_bayer_redis_data: ep_redis_data:
ep_bayer_data: ep_data:
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
if [ "${SKIP_DJANGO_SETUP:-false}" != "true" ]; then
python manage.py migrate --no-input
python manage.py collectstatic --no-input
fi
exec "$@"
+3 -13
View File
@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import json
import os import os
from pathlib import Path from pathlib import Path
@@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev") ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env")
print(f"Loading env from {ENV_PATH}") print(f"Loading env from {ENV_PATH}")
load_dotenv(ENV_PATH, override=False) load_dotenv(ENV_PATH, override=False)
@@ -340,6 +340,7 @@ DEFAULT_MODEL_PARAMS = {
DEFAULT_MAX_NUMBER_OF_NODES = 50 DEFAULT_MAX_NUMBER_OF_NODES = 50
DEFAULT_MAX_DEPTH = 8 DEFAULT_MAX_DEPTH = 8
DEFAULT_MODEL_THRESHOLD = 0.25 DEFAULT_MODEL_THRESHOLD = 0.25
BATCH_PREDICT_MAX_COMPOUNDS = 150
# Loading Plugins # Loading Plugins
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True" PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
@@ -442,14 +443,3 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
if BIOTRANSFORMER_ENABLED: if BIOTRANSFORMER_ENABLED:
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None) BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
# PES
PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None)
if PES_API_MAPPING:
import json
PES_API_MAPPING = json.loads(PES_API_MAPPING)
else:
PES_API_MAPPING = {}
# AD Group Mapping
+3
View File
@@ -40,6 +40,9 @@ if "migration" in s.INSTALLED_APPS:
if s.MS_ENTRA_ENABLED: if s.MS_ENTRA_ENABLED:
urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls"))) urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
if s.TENANT != "public":
urlpatterns.append(path(f"{PATH_PREFIX}", include(f"{s.TENANT}.urls")))
# Custom error handlers # Custom error handlers
handler400 = "epdb.views.handler400" handler400 = "epdb.views.handler400"
handler403 = "epdb.views.handler403" handler403 = "epdb.views.handler403"
+10 -7
View File
@@ -117,25 +117,28 @@ class APIPermissionTestBase(TestCase):
# Create test compounds in each package # Create test compounds in each package
cls.reviewed_compound = Compound.create( cls.reviewed_compound = Compound.create(
cls.reviewed_package, "C", "Reviewed Compound", "Test compound" cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
) )
cls.owned_compound = Compound.create( cls.owned_compound = Compound.create(
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound" cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
) )
cls.read_compound = Compound.create( cls.read_compound = Compound.create(
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound" cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
) )
cls.write_compound = Compound.create( cls.write_compound = Compound.create(
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound" cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
) )
cls.all_compound = Compound.create( cls.all_compound = Compound.create(
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound" cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
) )
cls.no_access_compound = Compound.create( cls.no_access_compound = Compound.create(
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound" cls.unreviewed_package_no_access,
"CCCCCC",
name="No Access Compound",
description="Test compound",
) )
cls.group_compound = Compound.create( cls.group_compound = Compound.create(
cls.group_package, "CCCCCCC", "Group Compound", "Test compound" cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
) )
+4 -4
View File
@@ -294,8 +294,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
return Compound.create( return Compound.create(
package, package,
smiles, smiles,
f"Reviewed Compound {idx:03d}", name=f"Reviewed Compound {idx:03d}",
"Compound for pagination tests", description="Compound for pagination tests",
) )
@classmethod @classmethod
@@ -305,8 +305,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
return Compound.create( return Compound.create(
package, package,
smiles, smiles,
f"Draft Compound {idx:03d}", name=f"Draft Compound {idx:03d}",
"Compound for pagination tests", description="Compound for pagination tests",
) )
+57 -2
View File
@@ -9,8 +9,8 @@ from envipy_additional_information import registry
from envipy_additional_information.groups import GroupEnum from envipy_additional_information.groups import GroupEnum
from epapi.utils.schema_transformers import build_rjsf_output from epapi.utils.schema_transformers import build_rjsf_output
from epapi.utils.validation_errors import handle_validation_error from epapi.utils.validation_errors import handle_validation_error
from epdb.models import AdditionalInformation from epdb.models import AdditionalInformation, Scenario, Node
from ..dal import get_scenario_for_read, get_scenario_for_write from ..dal import get_scenario_for_read, get_scenario_for_write, get_package_for_write
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,6 +58,61 @@ def list_scenario_info(request, scenario_uuid: UUID):
return result return result
@router.post("/information/{model_name}/")
def add_object_info(request, model_name: str, payload: Dict[str, Any] = Body(...)):
from epdb.views import EPDBURLParser
cls = registry.get_model(model_name.lower())
if not cls:
raise HttpError(404, f"Unknown model: {model_name}")
try:
instance = cls(**payload) # Pydantic validates
except ValidationError as e:
handle_validation_error(e)
if "attach_obj_url" in payload:
url_parser = EPDBURLParser(payload["attach_obj_url"])
if url_parser.contains_package_url():
package = get_package_for_write(request.user, url_parser.get_objects()[0].uuid)
attach_obj = url_parser.get_object()
if "scenario_uuid" in payload:
scenario = get_scenario_for_read(request.user, payload["scenario_uuid"])
else:
scenario = Scenario.create(
package,
name=f"Scenario {Scenario.objects.filter(package=package).count() + 1}",
description="no description",
scenario_date=None,
scenario_type=None,
additional_information=[],
)
if isinstance(attach_obj, Node):
ai = add_info_to_node(package, instance, scenario, attach_obj)
else:
raise HttpError(404, f"Bad request - Not implemented for {type(attach_obj)}!")
return {"status": "created", "uuid": ai.uuid}
raise HttpError(404, "Bad request!")
def add_info_to_node(package, add_inf, scenario, node):
ai = AdditionalInformation.create(
package,
add_inf,
scenario=scenario,
content_object=node,
)
node.pathway.scenarios.add(scenario)
return ai
@router.post("/scenario/{uuid:scenario_uuid}/information/{model_name}/") @router.post("/scenario/{uuid:scenario_uuid}/information/{model_name}/")
def add_scenario_info( def add_scenario_info(
request, scenario_uuid: UUID, model_name: str, payload: Dict[str, Any] = Body(...) request, scenario_uuid: UUID, model_name: str, payload: Dict[str, Any] = Body(...)
+26
View 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")
+1 -1
View File
@@ -56,7 +56,7 @@ def get_pathway_for_iuclid_export(user, pathway_uuid: UUID) -> PathwayExportDTO:
ai_for_node = [] ai_for_node = []
scenario_entries: list[PathwayScenarioDTO] = [] scenario_entries: list[PathwayScenarioDTO] = []
for scenario in sorted(node.scenarios.all(), key=lambda item: item.pk): for scenario in sorted(node.get_scenarios(), key=lambda item: item.pk):
ai_for_scenario = list(scenario.get_additional_information(direct_only=True)) ai_for_scenario = list(scenario.get_additional_information(direct_only=True))
ai_for_node.extend(ai_for_scenario) ai_for_node.extend(ai_for_scenario)
scenario_entries.append( scenario_entries.append(
+2
View File
@@ -15,6 +15,7 @@ from .endpoints import (
additional_information, additional_information,
settings, settings,
groups, groups,
joblogs,
) )
# Main router with authentication # Main router with authentication
@@ -37,6 +38,7 @@ router.add_router("", structure.router)
router.add_router("", additional_information.router) router.add_router("", additional_information.router)
router.add_router("", settings.router) router.add_router("", settings.router)
router.add_router("", groups.router) router.add_router("", groups.router)
router.add_router("", joblogs.router)
if s.IUCLID_EXPORT_ENABLED: if s.IUCLID_EXPORT_ENABLED:
from epiuclid.api import router as iuclid_router from epiuclid.api import router as iuclid_router
+24 -1
View File
@@ -1,7 +1,10 @@
from ninja import FilterSchema, FilterLookup, Schema from datetime import datetime
from typing import Annotated, Optional, List, Dict, Any from typing import Annotated, Optional, List, Dict, Any
from uuid import UUID from uuid import UUID
from django.urls import reverse
from ninja import Field, FilterSchema, FilterLookup, Schema
# Filter schema for query parameters # Filter schema for query parameters
class ReviewStatusFilter(FilterSchema): class ReviewStatusFilter(FilterSchema):
@@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
url: str = "" url: str = ""
name: str name: str
description: str description: str
class SimpleUserOutSchema(Schema):
uuid: UUID
url: str
name: str = Field(alias="username")
class JobLogOutSchema(Schema):
user: SimpleUserOutSchema
id: UUID = Field(alias="task_id")
url: str
name: str = Field(alias="job_name")
created: datetime = Field(alias="created")
status: str = Field(alias="status")
done: Optional[datetime] = Field(None, alias="done_at")
@staticmethod
def resolve_url(obj):
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
+85 -10
View File
@@ -1,12 +1,32 @@
import msal import msal
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.contrib.auth import login from django.contrib.auth import login
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth import get_user_model
from epdb.logic import UserManager from epdb.logic import UserManager
def get_msal_app_with_cache(request):
"""
Create MSAL app with session-based token cache.
"""
cache = msal.SerializableTokenCache()
# Load cache from session if it exists
if request.session.get("msal_token_cache"):
cache.deserialize(request.session["msal_token_cache"])
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY,
token_cache=cache,
)
return msal_app, cache
def entra_login(request): def entra_login(request):
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID, client_id=s.MS_ENTRA_CLIENT_ID,
@@ -23,11 +43,7 @@ def entra_login(request):
def entra_callback(request): def entra_callback(request):
msal_app = msal.ConfidentialClientApplication( msal_app, cache = get_msal_app_with_cache(request)
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY,
)
flow = request.session.pop("msal_auth_flow", None) flow = request.session.pop("msal_auth_flow", None)
if not flow: if not flow:
@@ -36,11 +52,18 @@ def entra_callback(request):
# Acquire token using the flow and callback request # Acquire token using the flow and callback request
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET) result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
# Save the token cache to session
if cache.has_state_changed:
request.session["msal_token_cache"] = cache.serialize()
claims = result["id_token_claims"] claims = result["id_token_claims"]
user_name = claims["name"] user_name = claims.get("name")
user_email = claims["emailaddress"] user_email = claims.get("emailaddress", claims.get("email"))
user_oid = claims["oid"] user_oid = claims.get("oid")
if not all([user_name, user_email, user_oid]):
raise ValueError("Missing required claims in ID token")
# Get implementing class # Get implementing class
User = get_user_model() User = get_user_model()
@@ -57,4 +80,56 @@ def entra_callback(request):
login(request, u) login(request, u)
return redirect("/") # Handle errors return redirect(s.SERVER_URL) # Handle errors
def get_access_token_from_request(request, scopes=None):
"""
Get an access token from the request using MSAL token cache.
"""
# Check if auth via Access Token
if request.headers.get("Authorization"):
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
if scopes is None:
scopes = s.MS_ENTRA_SCOPES
# Get user from request (must be authenticated)
if not request.user.is_authenticated:
return None
# Create MSAL app with persistent cache
msal_app, cache = get_msal_app_with_cache(request)
# Try to get accounts from cache
accounts = msal_app.get_accounts()
if not accounts:
return None
# Find the account that matches the current user
user_account = None
for account in accounts:
if account.get("local_account_id") == str(request.user.uuid):
user_account = account
break
# If no matching account found, use the first available account
if not user_account and accounts:
user_account = accounts[0]
if not user_account:
return None
# Try to acquire token silently from cache
result = msal_app.acquire_token_silent(scopes=scopes, account=user_account)
# Save cache changes back to session
if cache.has_state_changed:
request.session["msal_token_cache"] = cache.serialize()
if result and "access_token" in result:
return result
return None
+112
View File
@@ -1,5 +1,8 @@
import logging
from django.conf import settings as s from django.conf import settings as s
from django.contrib import admin from django.contrib import admin
from django.contrib import messages
from .models import ( from .models import (
AdditionalInformation, AdditionalInformation,
@@ -29,6 +32,8 @@ from .models import (
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
class AdditionalInformationAdmin(admin.ModelAdmin): class AdditionalInformationAdmin(admin.ModelAdmin):
pass pass
@@ -45,6 +50,113 @@ class UserAdmin(admin.ModelAdmin):
"date_joined", "date_joined",
] ]
actions = ["send_welcome_mail", "send_affiliation_mail"]
@admin.action(description="Send welcome mail")
def send_welcome_mail(self, request, queryset):
from django.core.mail import EmailMultiAlternatives
tpl = """Hello {username},
Your account has been successfully activated.
To log in, please visit
https://envipath.org/password_reset/
and request a new password.
If you have any questions or feedback, feel free to visit our community forum at
https://community.envipath.org/.
You do not need to register again for the forum - you can log in using your enviPath account by clicking "Log In" and then "Log in with enviPath."
Best regards,
The enviPath Team"""
users = []
for user in queryset:
if user.is_active:
logger.info(f"{user.username} already active - not sending mail again")
continue
try:
msg = EmailMultiAlternatives(
"Your enviPath Account Is Now Active",
tpl.format(username=user.username),
"admin@envipath.org",
[user.email],
bcc=["admin@envipath.org"],
)
msg.send(fail_silently=False)
user.is_active = True
user.password = "ASDF"
user.save()
users.append(user)
logger.info(f"{user.username} -> {user.email} mail sent")
except Exception as e:
logger.info(f"Error sending mail to {user.username}: {e}")
self.message_user(
request, f"Sent welcome mail to {[u.email for u in users]}", messages.SUCCESS
)
@admin.action(description="Send affiliation mail")
def send_affiliation_mail(self, request, queryset):
from django.core.mail import EmailMultiAlternatives
tpl = """Dear {username},
Thank you for your interest in enviPath!
Please note that the public enviPath system is intended for non-commercial use only.
We see that you registered using the email address {email}.
If possible, we kindly ask you to register using an official email address that reflects your affiliation (e.g., a university, NGO, or research organization).
If you would like us to update your account, simply reply to this email and let us know which address we should use.
We will then change it in our system, and you will receive a password reset email at the new address.
If you are registering with a company email address and are interested in commercial use, you are very welcome to book a meeting with us so we can discuss how we can best support you.
To book a meeting, please visit https://envipath.com/book
If changing to an affiliation email address is not possible, please contact us at registration@envipath.org
Best regards,
enviPath team"""
users = []
for user in queryset:
if user.is_active or user.contacted:
logger.info(
f"{user.username} already active or already contacted - not sending mail again"
)
continue
try:
msg = EmailMultiAlternatives(
"Regarding your enviPath registration",
tpl.format(username=user.username, email=user.email),
"admin@envipath.org",
[user.email],
bcc=["admin@envipath.org"],
)
msg.send(fail_silently=False)
user.contacted = True
user.save()
users.append(user)
logger.info(f"{user.username} -> {user.email} affiliation mail sent")
except Exception as e:
logger.info(f"Error sending mail to {user.username}: {e}")
self.message_user(
request, f"Sent affiliation mail to {[u.email for u in users]}", messages.SUCCESS
)
class UserPackagePermissionAdmin(admin.ModelAdmin): class UserPackagePermissionAdmin(admin.ModelAdmin):
pass pass
+10
View File
@@ -0,0 +1,10 @@
class InvalidSMILESException(Exception):
pass
class InvalidMolfileException(Exception):
pass
class PackageImportException(Exception):
pass
+210 -36
View File
@@ -46,6 +46,10 @@ from .models import (
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
def get_package_for_read(user, package_uuid):
return PackageManager.get_package_by_id(user, package_uuid)
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):
@@ -386,23 +390,50 @@ class PackageSchema(Schema):
@staticmethod @staticmethod
def resolve_readers(obj: Package): def resolve_readers(obj: Package):
users = User.objects.filter( readers = []
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.READ[0]
).values_list("user", flat=True)
).distinct()
return [{u.id: u.get_name()} for u in users] user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
users = User.objects.filter(id__in=user_ids).distinct()
for u in users:
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
"group", flat=True
)
groups = Group.objects.filter(id__in=group_ids).distinct()
for g in groups:
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
return readers
@staticmethod @staticmethod
def resolve_writers(obj: Package): def resolve_writers(obj: Package):
users = User.objects.filter( writers = []
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.WRITE[0]
).values_list("user", flat=True)
).distinct()
return [{u.id: u.get_name()} for u in users] user_ids = UserPackagePermission.objects.filter(
package=obj,
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
).values_list("user", flat=True)
users = User.objects.filter(id__in=user_ids).distinct()
for u in users:
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
group_ids = GroupPackagePermission.objects.filter(
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
).values_list("group", flat=True)
groups = Group.objects.filter(id__in=group_ids).distinct()
for g in groups:
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
return writers
@staticmethod @staticmethod
def resolve_review_comment(obj): def resolve_review_comment(obj):
@@ -764,6 +795,7 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
class CreateCompound(Schema): class CreateCompound(Schema):
compoundSmiles: str compoundSmiles: str
compoundMolFile: str | None = None
compoundName: str | None = None compoundName: str | None = None
compoundDescription: str | None = None compoundDescription: str | None = None
inchi: str | None = None inchi: str | None = None
@@ -777,9 +809,13 @@ def create_package_compound(
): ):
try: try:
p = get_package_for_write(request.user, package_uuid) p = get_package_for_write(request.user, package_uuid)
# inchi is not used atm
c = Compound.create( c = Compound.create(
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi p,
c.compoundSmiles,
molfile=c.compoundMolFile,
name=c.compoundName,
description=c.compoundDescription,
inchi=c.inchi,
) )
return redirect(c.url) return redirect(c.url)
except ValueError as e: except ValueError as e:
@@ -799,6 +835,27 @@ def delete_compound(request, package_uuid, compound_uuid):
} }
class CreateCompoundStructure(Schema):
smiles: str
name: str | None = None
description: str | None = None
inchi: str | None = None
molfile: str | None = None
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
def create_package_compound_structure(
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
):
try:
p = get_package_for_write(request.user, package_uuid)
c = Compound.objects.get(package=p, uuid=compound_uuid)
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
return redirect(cs.url)
except ValueError as e:
return 400, {"message": str(e)}
@router.delete( @router.delete(
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}" "/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
) )
@@ -1325,6 +1382,7 @@ class ScenarioSchema(Schema):
aliases: List[str] = Field([], alias="aliases") aliases: List[str] = Field([], alias="aliases")
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection") collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
collectionID: Optional[str] = None collectionID: Optional[str] = None
date: str = Field(None, alias="scenario_date")
description: str = Field(None, alias="description") description: str = Field(None, alias="description")
id: str = Field(None, alias="url") id: str = Field(None, alias="url")
identifier: str = "scenario" identifier: str = "scenario"
@@ -1468,6 +1526,7 @@ def create_package_additional_information(request, package_uuid):
scen = request.POST.get("scenario") scen = request.POST.get("scenario")
scenario = Scenario.objects.get(package=p, url=scen) scenario = Scenario.objects.get(package=p, url=scen)
if request.POST.get("adInfoTypes[]"):
url_parser = EPDBURLParser(request.POST.get("attach_obj")) url_parser = EPDBURLParser(request.POST.get("attach_obj"))
attach_obj = url_parser.get_object() attach_obj = url_parser.get_object()
@@ -1491,6 +1550,33 @@ def create_package_additional_information(request, package_uuid):
content_object=attach_obj, content_object=attach_obj,
) )
elif request.POST.get("ais"):
import json
parsed_ais = json.loads(request.POST.get("ais"))
for ai_type, ais in parsed_ais.items():
for ai in ais:
attach_obj = None
if ai.get("related"):
url_parser = EPDBURLParser(ai.get("related").get("url"))
attach_obj = url_parser.get_object()
if not hasattr(attach_obj, "additional_information"):
raise ValueError("Can't attach additional information to this object!")
if not attach_obj.url.startswith(p.url):
raise ValueError(
"Additional Information can only be set to objects stored in the same package!"
)
AdditionalInformation.create(
p,
AdditionalInformation.from_dict(ai_type, ai),
scenario=scenario,
content_object=attach_obj,
)
# TODO implement additional information endpoint ? # TODO implement additional information endpoint ?
return redirect(f"{scenario.url}") return redirect(f"{scenario.url}")
except ValueError: except ValueError:
@@ -1533,13 +1619,14 @@ class PathwayNode(Schema):
dt50s: List[Dict[str, str]] = Field([], alias="dt50s") dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate") engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
id: str = Field(None, alias="url") id: str = Field(None, alias="url")
idcomp: str = Field(None, alias="default_node_label.url") idcomp: str = Field(None, alias="node_label_id")
idreact: str = Field(None, alias="default_node_label.url") idreact: str = Field(None, alias="node_label_id")
image: str = Field(None, alias="image") image: str = Field(None, alias="image")
imageSize: int = Field(None, alias="image_size") imageSize: int = Field(None, alias="image_size")
name: str = Field(None, alias="name") name: str = Field(None, alias="name")
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate") proposed: List[Dict[str, Any]] = []
smiles: str = Field(None, alias="default_node_label.smiles") smiles: str = Field(None, alias="smiles")
pseudo: bool = Field(False, alias="pseudo")
@staticmethod @staticmethod
def resolve_atom_count(obj: Node): def resolve_atom_count(obj: Node):
@@ -1552,24 +1639,10 @@ class PathwayNode(Schema):
# TODO # TODO
return [] return []
@staticmethod
def resolve_engineered_intermediate(obj: Node):
# TODO
return False
@staticmethod
def resolve_image(obj: Node):
return f"{obj.default_node_label.url}?image=svg"
@staticmethod @staticmethod
def resolve_image_size(obj: Node): def resolve_image_size(obj: Node):
return 400 return 400
@staticmethod
def resolve_proposed_intermediate(obj: Node):
# TODO
return []
class PathwaySchema(Schema): class PathwaySchema(Schema):
aliases: List[str] = Field([], alias="aliases") aliases: List[str] = Field([], alias="aliases")
@@ -1693,6 +1766,29 @@ def create_package_pathway(
return 403, {"message": str(e)} return 403, {"message": str(e)}
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
def update_pathway(request, package_uuid, pathway_uuid):
try:
p = get_package_for_write(request.user, package_uuid)
if request.POST.get("scenario"):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
pw.scenarios.add(scen)
pw.save()
return redirect(f"{pw.url}")
else:
return 400, {"message": "No scenario specified!"}
except ValueError:
return 403, {
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}") @router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
def delete_pathway(request, package_uuid, pathway_uuid): def delete_pathway(request, package_uuid, pathway_uuid):
try: try:
@@ -1798,6 +1894,7 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
class CreateNode(Schema): class CreateNode(Schema):
nodeAsSmiles: str nodeAsSmiles: str
nodeAsMolFile: str | None = None
nodeName: str | None = None nodeName: str | None = None
nodeReason: str | None = None nodeReason: str | None = None
nodeDepth: str | None = None nodeDepth: str | None = None
@@ -1813,13 +1910,20 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid) pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
if n.nodeDepth is not None and n.nodeDepth.strip() != "": if n.nodeDepth is not None and n.nodeDepth.strip() != "":
node_depth = int(n.nodeDepth) node_depth = int(float(n.nodeDepth))
else: else:
node_depth = -1 node_depth = -1
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason) node = Node.create(
pw,
n.nodeAsSmiles,
node_depth,
molfile=n.nodeAsMolFile,
name=n.nodeName,
description=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!"}
@@ -1958,6 +2062,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
for pr in e.products.split(","): for pr in e.products.split(","):
products.append(Node.objects.get(pathway=pw, url=pr.strip())) products.append(Node.objects.get(pathway=pw, url=pr.strip()))
multi_step = False
if e.multistep and e.multistep.strip() == "true":
multi_step = True
new_e = Edge.create( new_e = Edge.create(
pathway=pw, pathway=pw,
start_nodes=educts, start_nodes=educts,
@@ -1965,11 +2073,15 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
rule=None, rule=None,
name=None, name=None,
description=e.edgeReason, description=e.edgeReason,
multi_step=multi_step,
) )
# Update depths as sideeffect of above operation
pw.update_depths()
return redirect(new_e.url) return redirect(new_e.url)
except ValueError: except ValueError:
return 403, {"message": "Adding node failed!"} return 403, {"message": "Adding Edge failed!"}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}") @router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
@@ -2160,3 +2272,65 @@ def get_setting(request, setting_uuid):
return 403, { return 403, {
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!" "message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
} }
########
# Util #
########
class NonPersistent(Schema):
smiles: str
setting_url: str = Field(..., alias="settingUri")
@router.post("/util", response={200: Any, 403: Error})
def predict(request, np: Form[NonPersistent]):
try:
from epdb.logic import SPathway
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
spw.predict()
return spw.to_json()
except ValueError:
return 403, {
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
}
##########
# Export #
##########
class PackageExportInSchema(Schema):
package_uuid: str
additional_information_types: List[str] | None = None
@router.get("/export", response={200: Any, 403: Error})
def export(request, q: Query[PackageExportInSchema]):
try:
p = get_package_for_read(request.user, q.package_uuid)
from envipy_additional_information import registry
from utilities.misc import PathwayExporter
ai_types = []
if q.additional_information_types is not None:
for ai_type in q.additional_information_types:
if registry.get_model(ai_type) is None:
return 400, {
"message": f"Exporting Package with id {q.package_uuid} failed as {ai_type} is not a valid additional information type!"
}
ai_types.append(ai_type)
exporter = PathwayExporter(p, add_infs_to_export=ai_types)
res = exporter.do_export()
filename = f"{p.get_name().replace(' ', '_')}_{p.uuid}.tsv"
response = HttpResponse(res, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
except ValueError:
return 403, {
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
}
+72 -94
View File
@@ -44,7 +44,7 @@ class EPDBURLParser:
MODEL_PATTERNS = { MODEL_PATTERNS = {
"epdb.User": re.compile(rf"^.*/user/{UUID_PATTERN}"), "epdb.User": re.compile(rf"^.*/user/{UUID_PATTERN}"),
"epdb.Group": re.compile(rf"^.*/group/{UUID_PATTERN}"), "epdb.Group": re.compile(rf"^.*/group/{UUID_PATTERN}"),
"epdb.Package": re.compile(rf"^.*/package/{UUID_PATTERN}"), s.EPDB_PACKAGE_MODEL: re.compile(rf"^.*/package/{UUID_PATTERN}"),
"epdb.Compound": re.compile(rf"^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}"), "epdb.Compound": re.compile(rf"^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}"),
"epdb.CompoundStructure": re.compile( "epdb.CompoundStructure": re.compile(
rf"^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}/structure/{UUID_PATTERN}" rf"^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}/structure/{UUID_PATTERN}"
@@ -94,7 +94,7 @@ class EPDBURLParser:
def contains_package_url(self): def contains_package_url(self):
return ( return (
bool(self.MODEL_PATTERNS["epdb.Package"].findall(self.url)) bool(self.MODEL_PATTERNS[s.EPDB_PACKAGE_MODEL].findall(self.url))
and not self.is_package_url() and not self.is_package_url()
) )
@@ -122,7 +122,7 @@ class EPDBURLParser:
"epdb.EPModel", "epdb.EPModel",
"epdb.Pathway", "epdb.Pathway",
# 1st level # 1st level
"epdb.Package", s.EPDB_PACKAGE_MODEL,
"epdb.Setting", "epdb.Setting",
"epdb.Group", "epdb.Group",
"epdb.User", "epdb.User",
@@ -144,7 +144,7 @@ class EPDBURLParser:
hierarchy_order = [ hierarchy_order = [
# 1st level # 1st level
"epdb.Package", s.EPDB_PACKAGE_MODEL,
"epdb.Setting", "epdb.Setting",
"epdb.Group", "epdb.Group",
"epdb.User", "epdb.User",
@@ -264,8 +264,12 @@ class GroupManager(object):
return bool(re.findall(GroupManager.group_pattern, url)) return bool(re.findall(GroupManager.group_pattern, url))
@staticmethod @staticmethod
def create_group(current_user, name, description): def create_group(current_user, name, description, *args, **kwargs):
g = Group() g = Group()
if "uuid" in kwargs:
g.uuid = kwargs["uuid"]
# Clean for potential XSS # Clean for potential XSS
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@@ -341,52 +345,17 @@ class PackageManager(object):
@staticmethod @staticmethod
def readable(user, package): def readable(user, package):
if ( return (
UserPackagePermission.objects.filter(package=package, user=user).exists() PackageManager.has_package_permission(user, package, "read") | package.reviewed is True
or GroupPackagePermission.objects.filter(
package=package, group__in=GroupManager.get_groups(user)
) )
or package.reviewed is True
or user.is_superuser
):
return True
return False
@staticmethod @staticmethod
def writable(user, package): def writable(user, package):
if ( return PackageManager.has_package_permission(user, package, "write")
UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.WRITE[0]
).exists()
or GroupPackagePermission.objects.filter(
package=package,
group__in=GroupManager.get_groups(user),
permission=Permission.WRITE[0],
).exists()
or UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.ALL[0]
).exists()
or user.is_superuser
):
return True
return False
@staticmethod @staticmethod
def administrable(user, package): def administrable(user, package):
if ( return PackageManager.has_package_permission(user, package, "all")
UserPackagePermission.objects.filter(
package=package, user=user, permission=Permission.ALL[0]
).exists()
or GroupPackagePermission.objects.filter(
package=package,
group__in=GroupManager.get_groups(user),
permission=Permission.ALL[0],
).exists()
or user.is_superuser
):
return True
return False
@staticmethod @staticmethod
def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str): def has_package_permission(user: "User", package: Union[str, UUID, "Package"], permission: str):
@@ -470,7 +439,9 @@ class PackageManager(object):
# remove package if user is owner and package is reviewed e.g. admin # remove package if user is owner and package is reviewed e.g. admin
qs = qs.filter(reviewed=False) qs = qs.filter(reviewed=False)
return qs.distinct() qs = qs.distinct()
return qs
@staticmethod @staticmethod
def get_all_writeable_packages(user): def get_all_writeable_packages(user):
@@ -514,7 +485,9 @@ class PackageManager(object):
qs = qs.filter(reviewed=False) qs = qs.filter(reviewed=False)
return qs.distinct() qs = qs.distinct()
return qs
@staticmethod @staticmethod
def get_packages(): def get_packages():
@@ -716,6 +689,10 @@ class PackageManager(object):
struc.description = structure["description"] struc.description = structure["description"]
struc.aliases = structure.get("aliases", []) struc.aliases = structure.get("aliases", [])
struc.smiles = structure["smiles"] struc.smiles = structure["smiles"]
if structure.get("molfile"):
struc.molfile = structure["molfile"]
struc.save() struc.save()
for scen in structure["scenarios"]: for scen in structure["scenarios"]:
@@ -1018,52 +995,9 @@ class PackageManager(object):
print("Fixing Node depths...") print("Fixing Node depths...")
total_pws = Pathway.objects.filter(package=pack).count() total_pws = Pathway.objects.filter(package=pack).count()
for p, pw in enumerate(Pathway.objects.filter(package=pack)): for p, pw in enumerate(Pathway.objects.filter(package=pack)):
in_count = defaultdict(lambda: 0) pw.update_depths()
out_count = defaultdict(lambda: 0)
for e in pw.edges:
# TODO check if this will remain
for react in e.start_nodes.all():
out_count[str(react.uuid)] += 1
for prod in e.end_nodes.all():
in_count[str(prod.uuid)] += 1
root_nodes = []
for n in pw.nodes:
num_parents = in_count[str(n.uuid)]
if num_parents == 0:
# must be a root node or unconnected node
if n.depth != 0:
n.depth = 0
n.save()
# Only root node may have children
if out_count[str(n.uuid)] > 0:
root_nodes.append(n)
levels = [root_nodes]
seen = set()
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
for i, level_nodes in enumerate(levels):
new_level = []
for n in level_nodes:
for e in n.out_edges.all():
for prod in e.end_nodes.all():
if str(prod.uuid) not in seen:
old_depth = prod.depth
if old_depth != i + 1:
prod.depth = i + 1
prod.save()
new_level.append(prod)
seen.add(str(n.uuid))
if new_level:
levels.append(new_level)
print(f"{p + 1}/{total_pws} fixed.", end="\r") print(f"{p + 1}/{total_pws} fixed.", end="\r")
return pack return pack
@@ -1074,10 +1008,8 @@ class PackageManager(object):
data: Dict[str, Any], data: Dict[str, Any],
owner: User, owner: User,
preserve_uuids=False, preserve_uuids=False,
add_import_timestamp=True,
trust_reviewed=False,
) -> Package: ) -> Package:
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed) importer = PackageImporter(data, preserve_uuids)
imported_package = importer.do_import() imported_package = importer.do_import()
up = UserPackagePermission() up = UserPackagePermission()
@@ -1873,12 +1805,51 @@ class SPathway(object):
logger.info("Update done!") logger.info("Update done!")
def compute_bayes_probabilities(self) -> Dict[SEdge, float]:
"""
Computes Bayes-adjusted probabilities for all edges in the pathway
by iterating level by level from depth 0 upwards, keyed on educt depth.
Returns:
A dict mapping each SEdge to its Bayes-adjusted probability.
"""
bayes_probs: Dict[SEdge, float] = {}
# Group edges by their educt depth
edges_by_depth: Dict[int, List[SEdge]] = {}
for edge in self.edges:
d = edge.educts[0].depth
edges_by_depth.setdefault(d, []).append(edge)
for depth in sorted(edges_by_depth.keys()):
for edge in edges_by_depth[depth]:
if depth == 0:
bayes_probs[edge] = edge.probability
else:
predecessor_edges = [e for e in self.edges if edge.educts[0] in e.products]
if not predecessor_edges or not all(
e in bayes_probs for e in predecessor_edges
):
# Predecessor not computed yet (e.g. same-depth product),
# fall back to raw probability
bayes_probs[edge] = edge.probability
else:
predecessor_avg = sum(bayes_probs[e] for e in predecessor_edges) / len(
predecessor_edges
)
bayes_probs[edge] = predecessor_avg * edge.probability
return bayes_probs
def to_json(self): def to_json(self):
nodes = [] nodes = []
edges = [] edges = []
idx_lookup = {} idx_lookup = {}
bayes_probs = self.compute_bayes_probabilities()
for i, smiles in enumerate(self.smiles_to_node): for i, smiles in enumerate(self.smiles_to_node):
n = self.smiles_to_node[smiles] n = self.smiles_to_node[smiles]
idx_lookup[smiles] = i idx_lookup[smiles] = i
@@ -1894,6 +1865,13 @@ class SPathway(object):
"to": to_indices, "to": to_indices,
} }
if edge.rule:
e["rule"] = edge.rule.simple_json()
if edge.probability:
e["probability"] = edge.probability
e["multiGenProbability"] = bayes_probs[edge]
edges.append(e) edges.append(e)
return { return {
+7 -3
View File
@@ -99,11 +99,15 @@ class Command(BaseCommand):
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png" new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save() new_license.save()
def import_package(self, data, owner): def import_package(self, data, owner, all_envipath_user_group):
return PackageManager.import_legacy_package( p = PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
) )
PackageManager.grant_read(owner, p, all_envipath_user_group)
return p
def create_default_setting(self, owner, packages): def create_default_setting(self, owner, packages):
s = SettingManager.create_setting( s = SettingManager.create_setting(
owner, owner,
@@ -198,7 +202,7 @@ class Command(BaseCommand):
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8" s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
).read() ).read()
) )
imported_package = self.import_package(package_data, admin) imported_package = self.import_package(package_data, admin, g)
mapping[p.replace(".json", "")] = imported_package mapping[p.replace(".json", "")] = imported_package
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]]) setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
+10 -5
View File
@@ -44,15 +44,19 @@ class Command(BaseCommand):
"EPModel", "EPModel",
"ApplicabilityDomain", "ApplicabilityDomain",
"EnzymeLink", "EnzymeLink",
"AdditionalInformation",
] ]
for model in MODELS: for model in MODELS:
obj_cls = apps.get_model("epdb", model) obj_cls = apps.get_model("epdb", model)
obj_cls.objects.update(
url=Replace(F("url"), Value(options["old"]), Value(options["new"])) update_fields = {"url": Replace(F("url"), Value(options["old"]), Value(options["new"]))}
if hasattr(obj_cls, "description"):
update_fields["description"] = Replace(
F("description"), Value(options["old"]), Value(options["new"])
) )
if issubclass(obj_cls, EnviPathModel): if issubclass(obj_cls, EnviPathModel):
obj_cls.objects.update( update_fields["kv"] = Cast(
kv=Cast(
Replace( Replace(
Cast(F("kv"), output_field=TextField()), Cast(F("kv"), output_field=TextField()),
Value(options["old"]), Value(options["old"]),
@@ -60,4 +64,5 @@ class Command(BaseCommand):
), ),
output_field=JSONField(), output_field=JSONField(),
) )
)
obj_cls.objects.update(**update_fields)
@@ -0,0 +1,97 @@
import logging
from django.core.management.base import BaseCommand
from django.db import transaction
from uuid import uuid4
from epdb.models import Package, ReactionExplanation
from utilities.chem import FormatConverter
from django.utils import timezone
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--rule-package",
action="append",
default=["32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"],
type=str,
help="UUID to process. Can be specified multiple times.",
)
parser.add_argument(
"--reaction-package",
action="append",
default=[
"32de3cf4-e3e6-4168-956e-32fa5ddb0ce1", # BBD
"f05e38d8-e9b4-4c3e-b0d8-9ab29966eccf", # Sediment
"521c547a-fd2a-491c-ad5b-7eaa1577fb65", # Sludge
"5882df9c-dae1-4d80-a40e-db4724271456", # Soil
"87a49584-d937-482c-9c33-25928dcb02a8", # PFAS
],
type=str,
help="UUID to process. Can be specified multiple times.",
)
parser.add_argument(
"--dry-run",
default=False,
action="store_true",
help="Perform dry run",
)
@transaction.atomic
def handle(self, *args, **options):
RUN_UUID = uuid4()
RUN_START = timezone.now()
rule_packages = Package.objects.filter(uuid__in=options["rule_package"])
reaction_packages = Package.objects.filter(uuid__in=options["reaction_package"])
rules = []
for rule_package in rule_packages:
rules.extend(rule_package.get_applicable_rules())
reactions = []
for reaction_package in reaction_packages:
reactions.extend(reaction_package.reactions)
logger.debug(f"Collected {len(rules)} rules and {len(reactions)} reactions.")
for i, reaction in enumerate(reactions):
logger.debug(f"Reaction {i} / {len(reactions)}")
for j, rule in enumerate(rules):
reactants, products = reaction.smirks().split(">>")
if len(reactants.split(".")) > 1:
logger.debug(f"Skipping reaction {reaction.uuid} as it has multiple reactants.")
break
products = products.split(".")
# Run reaction with rule
rule_products = rule.apply(reactants)
# Check if products match (in both directions if extras are not allowed)
for product_set in rule_products:
covered, exact = FormatConverter.smiles_covered_by(
products,
product_set.product_set,
standardize=True,
canonicalize_tautomers=True,
return_exact_match=True,
)
if covered and not options["dry-run"]:
logger.debug(f"Reaction {reaction.uuid} explained by rule {rule.uuid}")
re = ReactionExplanation()
re.run_uuid = RUN_UUID
re.run_start = RUN_START
re.reaction = reaction
re.rule = rule
re.exact = exact
re.save()
# Its explained, if there are more sets skip them
break
+594
View File
@@ -0,0 +1,594 @@
# Generated by Django 5.2.1 on 2025-07-22 20:58
import datetime
import django.contrib.auth.models
import django.contrib.auth.validators
import django.contrib.postgres.fields
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Compound',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
),
migrations.CreateModel(
name='EPModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
],
),
migrations.CreateModel(
name='License',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.URLField(verbose_name='link')),
('image_link', models.URLField(verbose_name='Image link')),
],
),
migrations.CreateModel(
name='Rule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='APIToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hashed_key', models.CharField(max_length=128, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CompoundStructure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('smiles', models.TextField(verbose_name='SMILES')),
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
('normalized_structure', models.BooleanField(default=False)),
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='compound',
name='default_structure',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
),
migrations.CreateModel(
name='Edge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='EnviFormer',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='PluginModel',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='RuleBaseRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(verbose_name='Group name')),
('public', models.BooleanField(default=False, verbose_name='Public Group')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='default_group',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('depth', models.IntegerField(verbose_name='Node depth')),
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='end_nodes',
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
),
migrations.AddField(
model_name='edge',
name='start_nodes',
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='epmodel',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='compound',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='user',
name='default_package',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
),
migrations.CreateModel(
name='SequentialRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.CreateModel(
name='SimpleRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AddField(
model_name='rule',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='rule',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.CreateModel(
name='Pathway',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='node',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.AddField(
model_name='edge',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.CreateModel(
name='Reaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='edge_label',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
),
migrations.CreateModel(
name='Scenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('scenario_date', models.CharField(default='No date', max_length=256)),
('scenario_type', models.CharField(default='Not specified', max_length=256)),
('additional_information', models.JSONField(verbose_name='Additional Information')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='rule',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='reaction',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='pathway',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='node',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='edge',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compoundstructure',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compound',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.CreateModel(
name='Setting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('public', models.BooleanField(default=False)),
('global_default', models.BooleanField(default=False)),
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='pathway',
name='setting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
),
migrations.AddField(
model_name='user',
name='default_setting',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
),
migrations.CreateModel(
name='MLRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='ApplicabilityDomain',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('num_neighbours', models.FloatField(default=5)),
('reliability_threshold', models.FloatField(default=0.5)),
('local_compatibilty_threshold', models.FloatField(default=0.5)),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SimpleAmbitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('smirks', models.TextField(verbose_name='SMIRKS')),
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SimpleRDKitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SequentialRuleOrdering',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_index', models.IntegerField()),
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
],
),
migrations.AddField(
model_name='sequentialrule',
name='simple_rules',
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
),
migrations.CreateModel(
name='ParallelRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AlterUniqueTogether(
name='compound',
unique_together={('uuid', 'package')},
),
migrations.CreateModel(
name='GroupPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
],
options={
'unique_together': {('package', 'group')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('package', 'user')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserSettingPermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('setting', 'user')},
},
bases=('epdb.permission',),
),
]
@@ -0,0 +1,128 @@
# Generated by Django 5.2.1 on 2025-08-25 18:07
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('epdb', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExternalDatabase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
],
options={
'verbose_name': 'External Database',
'verbose_name_plural': 'External Databases',
'db_table': 'epdb_external_database',
'ordering': ['name'],
},
),
migrations.AlterModelOptions(
name='apitoken',
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
),
migrations.AlterModelOptions(
name='edge',
options={},
),
migrations.RemoveField(
model_name='edge',
name='polymorphic_ctype',
),
migrations.AddField(
model_name='apitoken',
name='is_active',
field=models.BooleanField(default=True, help_text='Whether this token is active'),
),
migrations.AddField(
model_name='apitoken',
name='modified',
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
),
migrations.AddField(
model_name='applicabilitydomain',
name='functional_groups',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='mlrelativereasoning',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AlterField(
model_name='apitoken',
name='created',
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
),
migrations.AlterField(
model_name='apitoken',
name='expires_at',
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
),
migrations.AlterField(
model_name='apitoken',
name='hashed_key',
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
),
migrations.AlterField(
model_name='apitoken',
name='name',
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
),
migrations.AlterField(
model_name='apitoken',
name='user',
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='applicabilitydomain',
name='num_neighbours',
field=models.IntegerField(default=5),
),
migrations.AlterModelTable(
name='apitoken',
table='epdb_api_token',
),
migrations.CreateModel(
name='ExternalIdentifier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('object_id', models.IntegerField()),
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
],
options={
'verbose_name': 'External Identifier',
'verbose_name_plural': 'External Identifiers',
'db_table': 'epdb_external_identifier',
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
},
),
]
@@ -0,0 +1,228 @@
# Generated by Django 5.2.1 on 2025-08-26 17:05
from django.db import migrations, models
def populate_url(apps, schema_editor):
MODELS = [
'User',
'Group',
'Package',
'Compound',
'CompoundStructure',
'Pathway',
'Edge',
'Node',
'Reaction',
'SimpleAmbitRule',
'SimpleRDKitRule',
'ParallelRule',
'SequentialRule',
'Scenario',
'Setting',
'MLRelativeReasoning',
'EnviFormer',
'ApplicabilityDomain',
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
for obj in obj_cls.objects.all():
obj.url = assemble_url(obj)
if obj.url is None:
raise ValueError(f"Could not assemble url for {obj}")
obj.save()
def assemble_url(obj):
from django.conf import settings as s
match obj.__class__.__name__:
case 'User':
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
case 'Group':
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
case 'Package':
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
case 'Compound':
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
case 'CompoundStructure':
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
case 'SimpleAmbitRule':
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
case 'SimpleRDKitRule':
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
case 'ParallelRule':
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
case 'SequentialRule':
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
case 'Reaction':
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
case 'Pathway':
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
case 'Node':
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
case 'Edge':
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
case 'MLRelativeReasoning':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'EnviFormer':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'ApplicabilityDomain':
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
case 'Scenario':
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
case 'Setting':
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
case _:
raise ValueError(f"Unknown model {obj.__class__.__name__}")
class Migration(migrations.Migration):
dependencies = [
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
]
operations = [
migrations.AddField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
]
@@ -0,0 +1,55 @@
# Generated by Django 5.2.1 on 2025-09-09 09:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='mlrelativereasoning',
options={},
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='data_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='eval_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='rule_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
),
migrations.CreateModel(
name='RuleBasedRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('min_count', models.IntegerField(default=10)),
('max_count', models.IntegerField(default=0)),
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.DeleteModel(
name='RuleBaseRelativeReasoning',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-09-11 06:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0004_alter_mlrelativereasoning_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='group',
name='group_member',
field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-09-18 06:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0005_alter_group_group_member'),
]
operations = [
migrations.AddField(
model_name='mlrelativereasoning',
name='multigen_eval',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='rulebasedrelativereasoning',
name='multigen_eval',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,53 @@
# Generated by Django 5.2.1 on 2025-10-07 08:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='enviformer',
options={},
),
migrations.AddField(
model_name='enviformer',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AddField(
model_name='enviformer',
name='data_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_results',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='enviformer',
name='model_status',
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
),
migrations.AddField(
model_name='enviformer',
name='multigen_eval',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='enviformer',
name='rule_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
),
]
+64
View File
@@ -0,0 +1,64 @@
# Generated by Django 5.2.7 on 2025-10-10 06:58
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
]
operations = [
migrations.CreateModel(
name="EnzymeLink",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("ec_number", models.TextField(verbose_name="EC Number")),
("classification_level", models.IntegerField(verbose_name="Classification Level")),
("linking_method", models.TextField(verbose_name="Linking Method")),
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
(
"rule",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
),
],
options={
"abstract": False,
},
),
]
+66
View File
@@ -0,0 +1,66 @@
# Generated by Django 5.2.7 on 2025-10-27 09:39
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0008_enzymelink"),
]
operations = [
migrations.CreateModel(
name="JobLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
("task_id", models.UUIDField(unique=True)),
("job_name", models.TextField()),
(
"status",
models.CharField(
choices=[
("INITIAL", "Initial"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
("REVOKED", "Revoked"),
("IGNORED", "Ignored"),
],
default="INITIAL",
max_length=20,
),
),
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
("task_result", models.TextField(blank=True, default=None, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"abstract": False,
},
),
]
+18
View File
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-11 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0009_joblog"),
]
operations = [
migrations.AddField(
model_name="license",
name="cc_string",
field=models.TextField(default="by-nc-sa", verbose_name="CC string"),
preserve_default=False,
),
]
@@ -0,0 +1,59 @@
# Generated by Django 5.2.7 on 2025-11-11 14:13
import re
from django.contrib.postgres.aggregates import ArrayAgg
from django.db import migrations
from django.db.models import Min
def set_cc(apps, schema_editor):
License = apps.get_model("epdb", "License")
# For all existing licenses extract cc_string from link
for license in License.objects.all():
pattern = r"/licenses/([^/]+)/4\.0"
match = re.search(pattern, license.link)
if match:
license.cc_string = match.group(1)
license.save()
else:
raise ValueError(f"Could not find license for {license.link}")
# Ensure we have all licenses
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
if not License.objects.filter(cc_string=cc_string).exists():
new_license = License()
new_license.cc_string = cc_string
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save()
# As we might have existing Licenses representing the same License,
# get min pk and all pks as a list
license_lookup_qs = License.objects.values("cc_string").annotate(
lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",))
)
license_lookup = {
row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs
}
Packages = apps.get_model("epdb", "Package")
for k, v in license_lookup.items():
# Set min pk to all packages pointing to any of the duplicates
Packages.objects.filter(pk__in=v[1]).update(license_id=v[0])
# remove the min pk from "other" pks as we use them for deletion
v[1].remove(v[0])
# Delete redundant License objects
License.objects.filter(pk__in=v[1]).delete()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0010_license_cc_string"),
]
operations = [migrations.RunPython(set_cc)]
@@ -0,0 +1,22 @@
# Generated by Django 5.2.7 on 2025-12-02 13:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0011_auto_20251111_1413"),
]
operations = [
migrations.AddField(
model_name="node",
name="stereo_removed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="pathway",
name="predicted",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 5.2.7 on 2025-12-14 11:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0012_node_stereo_removed_pathway_predicted"),
]
operations = [
migrations.AddField(
model_name="setting",
name="expansion_schema",
field=models.CharField(
choices=[
("BFS", "Breadth First Search"),
("DFS", "Depth First Search"),
("GREEDY", "Greedy"),
],
default="BFS",
max_length=20,
),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-12-14 16:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("epdb", "0013_setting_expansion_schema"),
]
operations = [
migrations.RenameField(
model_name="setting",
old_name="expansion_schema",
new_name="expansion_scheme",
),
]
+17
View File
@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2026-01-19 19:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0014_rename_expansion_schema_setting_expansion_scheme"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_reviewer",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,179 @@
# Generated by Django 5.2.7 on 2026-02-12 09:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0015_user_is_reviewer"),
]
operations = [
migrations.RemoveField(
model_name="enviformer",
name="model_status",
),
migrations.RemoveField(
model_name="mlrelativereasoning",
name="model_status",
),
migrations.RemoveField(
model_name="rulebasedrelativereasoning",
name="model_status",
),
migrations.AddField(
model_name="epmodel",
name="model_status",
field=models.CharField(
choices=[
("INITIAL", "Initial"),
("INITIALIZING", "Model is initializing."),
("BUILDING", "Model is building."),
(
"BUILT_NOT_EVALUATED",
"Model is built and can be used for predictions, Model is not evaluated yet.",
),
("EVALUATING", "Model is evaluating"),
("FINISHED", "Model has finished building and evaluation."),
("ERROR", "Model has failed."),
],
default="INITIAL",
),
),
migrations.AlterField(
model_name="enviformer",
name="eval_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_eval_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="enviformer",
name="rule_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_rule_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_eval_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_rule_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_eval_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_rule_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages",
),
),
migrations.CreateModel(
name="PropertyPluginModel",
fields=[
(
"epmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="epdb.epmodel",
),
),
("threshold", models.FloatField(default=0.5)),
("eval_results", models.JSONField(blank=True, default=dict, null=True)),
("multigen_eval", models.BooleanField(default=False)),
("plugin_identifier", models.CharField(max_length=255)),
(
"app_domain",
models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="epdb.applicabilitydomain",
),
),
(
"data_packages",
models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_data_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Data Packages",
),
),
(
"eval_packages",
models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_eval_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages",
),
),
(
"rule_packages",
models.ManyToManyField(
blank=True,
related_name="%(app_label)s_%(class)s_rule_packages",
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages",
),
),
],
options={
"abstract": False,
},
bases=("epdb.epmodel",),
),
migrations.AddField(
model_name="setting",
name="property_models",
field=models.ManyToManyField(
blank=True,
related_name="settings",
to="epdb.propertypluginmodel",
verbose_name="Setting Property Models",
),
),
migrations.DeleteModel(
name="PluginModel",
),
]
@@ -0,0 +1,93 @@
# Generated by Django 5.2.7 on 2026-02-20 12:02
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("epdb", "0016_remove_enviformer_model_status_and_more"),
]
operations = [
migrations.CreateModel(
name="AdditionalInformation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("type", models.TextField(verbose_name="Additional Information Type")),
("data", models.JSONField(blank=True, default=dict, null=True)),
("object_id", models.PositiveBigIntegerField(blank=True, null=True)),
(
"content_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"package",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.EPDB_PACKAGE_MODEL,
verbose_name="Package",
),
),
(
"scenario",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="scenario_additional_information",
to="epdb.scenario",
),
),
],
options={
"indexes": [
models.Index(fields=["type"], name="epdb_additi_type_394349_idx"),
models.Index(
fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx"
),
models.Index(
fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx"
),
models.Index(
fields=["scenario", "content_type", "object_id"],
name="epdb_additi_scenari_ef2bf5_idx",
),
],
"constraints": [
models.CheckConstraint(
condition=models.Q(
models.Q(("content_type__isnull", True), ("object_id__isnull", True)),
models.Q(("content_type__isnull", False), ("object_id__isnull", False)),
_connector="OR",
),
name="ck_addinfo_gfk_pair",
),
models.CheckConstraint(
condition=models.Q(
("scenario__isnull", False),
("content_type__isnull", False),
_connector="OR",
),
name="ck_addinfo_not_both_null",
),
],
},
),
]
+132
View File
@@ -0,0 +1,132 @@
# Generated by Django 5.2.7 on 2026-02-20 12:03
from django.db import migrations
def get_additional_information(scenario):
from envipy_additional_information import registry
from envipy_additional_information.parsers import TypeOfAerationParser
for k, vals in scenario.additional_information.items():
if k == "enzyme":
continue
if k == "SpikeConentration":
k = "SpikeConcentration"
if k == "AerationType":
k = "TypeOfAeration"
for v in vals:
# Per default additional fields are ignored
MAPPING = {c.__name__: c for c in registry.list_models().values()}
try:
inst = MAPPING[k](**v)
except Exception:
if k == "TypeOfAeration":
toa = TypeOfAerationParser()
inst = toa.from_string(v["type"])
# Add uuid to uniquely identify objects for manipulation
if "uuid" in v:
inst.__dict__["uuid"] = v["uuid"]
yield inst
def forward_func(apps, schema_editor):
Scenario = apps.get_model("epdb", "Scenario")
ContentType = apps.get_model("contenttypes", "ContentType")
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
bulk = []
related = []
ctype = {o.model: o for o in ContentType.objects.all()}
parents = Scenario.objects.prefetch_related(
"compound_set",
"compoundstructure_set",
"reaction_set",
"rule_set",
"pathway_set",
"node_set",
"edge_set",
).filter(parent__isnull=True)
for i, scenario in enumerate(parents):
print(f"{i + 1}/{len(parents)}", end="\r")
if scenario.parent is not None:
related.append(scenario.parent)
continue
for ai in get_additional_information(scenario):
bulk.append(
AdditionalInformation(
package=scenario.package,
scenario=scenario,
type=ai.__class__.__name__,
data=ai.model_dump(mode="json"),
)
)
print("\n", len(bulk))
related = Scenario.objects.prefetch_related(
"compound_set",
"compoundstructure_set",
"reaction_set",
"rule_set",
"pathway_set",
"node_set",
"edge_set",
).filter(parent__isnull=False)
for i, scenario in enumerate(related):
print(f"{i + 1}/{len(related)}", end="\r")
parent = scenario.parent
# Check to which objects this scenario is attached to
for ai in get_additional_information(scenario):
rel_objs = [
"compound",
"compoundstructure",
"reaction",
"rule",
"pathway",
"node",
"edge",
]
for rel_obj in rel_objs:
for o in getattr(scenario, f"{rel_obj}_set").all():
bulk.append(
AdditionalInformation(
package=scenario.package,
scenario=parent,
type=ai.__class__.__name__,
data=ai.model_dump(mode="json"),
content_type=ctype[rel_obj],
object_id=o.pk,
)
)
print("Start creating additional information objects...")
AdditionalInformation.objects.bulk_create(bulk)
print("Done!")
print(len(bulk))
Scenario.objects.filter(parent__isnull=False).delete()
# Call ai save to fix urls
ais = AdditionalInformation.objects.all()
total = ais.count()
for i, ai in enumerate(ais):
print(f"{i + 1}/{total}", end="\r")
ai.save()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0017_additionalinformation"),
]
operations = [
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
]
@@ -1,741 +1,20 @@
# Generated by Django 5.2.7 on 2026-03-06 10:51 # Generated by Django 5.2.7 on 2026-02-23 08:45
import django.contrib.auth.models from django.db import migrations
import django.contrib.auth.validators
import django.contrib.postgres.fields
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ("epdb", "0018_auto_20260220_1203"),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.RemoveField(
name='ApplicabilityDomain', model_name="scenario",
fields=[ name="additional_information",
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('num_neighbours', models.IntegerField(default=5)),
('reliability_threshold', models.FloatField(default=0.5)),
('local_compatibilty_threshold', models.FloatField(default=0.5)),
('functional_groups', models.JSONField(blank=True, default=dict, null=True)),
],
options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.RemoveField(
name='Edge', model_name="scenario",
fields=[ name="parent",
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='EPModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='ExternalDatabase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
],
options={
'verbose_name': 'External Database',
'verbose_name_plural': 'External Databases',
'db_table': 'epdb_external_database',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
],
),
migrations.CreateModel(
name='License',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cc_string', models.TextField(verbose_name='CC string')),
('link', models.URLField(verbose_name='link')),
('image_link', models.URLField(verbose_name='Image link')),
],
),
migrations.CreateModel(
name='Rule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('is_reviewer', models.BooleanField(default=False)),
('default_package', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Default Package')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='APIToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('hashed_key', models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)),
('expires_at', models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True)),
('name', models.CharField(help_text='Descriptive name for this token', max_length=100)),
('is_active', models.BooleanField(default=True, help_text='Whether this token is active')),
('user', models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'API Token',
'verbose_name_plural': 'API Tokens',
'db_table': 'epdb_api_token',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='Compound',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
],
),
migrations.CreateModel(
name='CompoundStructure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('smiles', models.TextField(verbose_name='SMILES')),
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
('normalized_structure', models.BooleanField(default=False)),
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='compound',
name='default_structure',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
),
migrations.CreateModel(
name='PropertyPluginModel',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('multigen_eval', models.BooleanField(default=False)),
('plugin_identifier', models.CharField(max_length=255)),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('name', models.TextField(verbose_name='Group name')),
('public', models.BooleanField(default=False, verbose_name='Public Group')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('group_member', models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='default_group',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
),
migrations.CreateModel(
name='JobLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('task_id', models.UUIDField(unique=True)),
('job_name', models.TextField()),
('status', models.CharField(choices=[('INITIAL', 'Initial'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKED', 'Revoked'), ('IGNORED', 'Ignored')], default='INITIAL', max_length=20)),
('done_at', models.DateTimeField(blank=True, default=None, null=True)),
('task_result', models.TextField(blank=True, default=None, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
],
options={
'swappable': 'EPDB_PACKAGE_MODEL',
},
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('depth', models.IntegerField(verbose_name='Node depth')),
('stereo_removed', models.BooleanField(default=False)),
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='end_nodes',
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
),
migrations.AddField(
model_name='edge',
name='start_nodes',
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
),
migrations.CreateModel(
name='SequentialRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.CreateModel(
name='SimpleRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.CreateModel(
name='Pathway',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('predicted', models.BooleanField(default=False)),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='node',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.AddField(
model_name='edge',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.CreateModel(
name='Reaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='EnzymeLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('ec_number', models.TextField(verbose_name='EC Number')),
('classification_level', models.IntegerField(verbose_name='Classification Level')),
('linking_method', models.TextField(verbose_name='Linking Method')),
('edge_evidence', models.ManyToManyField(to='epdb.edge')),
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.rule')),
('reaction_evidence', models.ManyToManyField(to='epdb.reaction')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='edge_label',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
),
migrations.CreateModel(
name='Scenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('scenario_date', models.CharField(default='No date', max_length=256)),
('scenario_type', models.CharField(default='Not specified', max_length=256)),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='rule',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='reaction',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='pathway',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='node',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='edge',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compoundstructure',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compound',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.CreateModel(
name='Setting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('public', models.BooleanField(default=False)),
('global_default', models.BooleanField(default=False)),
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
('expansion_scheme', models.CharField(choices=[('BFS', 'Breadth First Search'), ('DFS', 'Depth First Search'), ('GREEDY', 'Greedy')], default='BFS', max_length=20)),
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Setting Rule Packages')),
('property_models', models.ManyToManyField(blank=True, related_name='settings', to='epdb.propertypluginmodel', verbose_name='Setting Property Models')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='pathway',
name='setting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
),
migrations.AddField(
model_name='user',
name='default_setting',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
),
migrations.CreateModel(
name='EnviFormer',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('multigen_eval', models.BooleanField(default=False)),
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='MLRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('multigen_eval', models.BooleanField(default=False)),
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.AddField(
model_name='applicabilitydomain',
name='model',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning'),
),
migrations.AddField(
model_name='propertypluginmodel',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AddField(
model_name='propertypluginmodel',
name='data_packages',
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages'),
),
migrations.AddField(
model_name='propertypluginmodel',
name='eval_packages',
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages'),
),
migrations.AddField(
model_name='propertypluginmodel',
name='rule_packages',
field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages'),
),
migrations.CreateModel(
name='RuleBasedRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('multigen_eval', models.BooleanField(default=False)),
('min_count', models.IntegerField(default=10)),
('max_count', models.IntegerField(default=0)),
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='ExternalIdentifier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('object_id', models.IntegerField()),
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
],
options={
'verbose_name': 'External Identifier',
'verbose_name_plural': 'External Identifiers',
'db_table': 'epdb_external_identifier',
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
},
),
migrations.CreateModel(
name='SimpleAmbitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('smirks', models.TextField(verbose_name='SMIRKS')),
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SimpleRDKitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SequentialRuleOrdering',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_index', models.IntegerField()),
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
],
),
migrations.AddField(
model_name='sequentialrule',
name='simple_rules',
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
),
migrations.CreateModel(
name='ParallelRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AlterUniqueTogether(
name='compound',
unique_together={('uuid', 'package')},
),
migrations.CreateModel(
name='AdditionalInformation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('type', models.TextField(verbose_name='Additional Information Type')),
('data', models.JSONField(blank=True, default=dict, null=True)),
('object_id', models.PositiveBigIntegerField(blank=True, null=True)),
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')),
('scenario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scenario_additional_information', to='epdb.scenario')),
],
options={
'indexes': [models.Index(fields=['type'], name='epdb_additi_type_394349_idx'), models.Index(fields=['scenario', 'type'], name='epdb_additi_scenari_a59edf_idx'), models.Index(fields=['content_type', 'object_id'], name='epdb_additi_content_44d4b4_idx'), models.Index(fields=['scenario', 'content_type', 'object_id'], name='epdb_additi_scenari_ef2bf5_idx')],
'constraints': [models.CheckConstraint(condition=models.Q(models.Q(('content_type__isnull', True), ('object_id__isnull', True)), models.Q(('content_type__isnull', False), ('object_id__isnull', False)), _connector='OR'), name='ck_addinfo_gfk_pair'), models.CheckConstraint(condition=models.Q(('scenario__isnull', False), ('content_type__isnull', False), _connector='OR'), name='ck_addinfo_not_both_null')],
},
),
migrations.CreateModel(
name='GroupPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
],
options={
'unique_together': {('package', 'group')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('package', 'user')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserSettingPermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('setting', 'user')},
},
bases=('epdb.permission',),
), ),
] ]
@@ -1,50 +1,49 @@
# Generated by Django 6.0.3 on 2026-04-14 19:07 # Generated by Django 6.0.3 on 2026-04-21 11:43
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('epdb', '0022_alter_classifierpluginmodel_data_packages_and_more'), ("epdb", "0022_alter_classifierpluginmodel_data_packages_and_more"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='compoundstructure', name="compoundstructure",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='epmodel', name="epmodel",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='parallelrule', name="parallelrule",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='rule', name="rule",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='sequentialrule', name="sequentialrule",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='simpleambitrule', name="simpleambitrule",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='simplerdkitrule', name="simplerdkitrule",
options={}, options={},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='simplerule', name="simplerule",
options={}, options={},
), ),
migrations.AddField( migrations.AddField(
model_name='group', model_name="compoundstructure",
name='secret', name="molfile",
field=models.BooleanField(default=False, verbose_name='Secret Group'), field=models.TextField(blank=True, null=True, verbose_name="Molfile"),
), ),
] ]
+17
View 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),
),
]
@@ -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),
]
@@ -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),
]
@@ -0,0 +1,150 @@
# Generated by Django 6.0.3 on 2026-07-01 20:59
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0026_auto_20260602_1718"),
]
operations = [
migrations.AlterField(
model_name="compound",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="compound",
name="default_structure",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="compound_default_structure",
to="epdb.compoundstructure",
verbose_name="Default Structure",
),
),
migrations.AlterField(
model_name="compound",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="compoundstructure",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="compoundstructure",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="edge",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="edge",
name="edge_label",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="epdb.reaction",
verbose_name="Edge label",
),
),
migrations.AlterField(
model_name="edge",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="node",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="node",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="pathway",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="pathway",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="reaction",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="reaction",
name="medline_references",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(),
blank=True,
null=True,
verbose_name="Medline References",
),
),
migrations.AlterField(
model_name="reaction",
name="rules",
field=models.ManyToManyField(
blank=True, related_name="reaction_rule", to="epdb.rule", verbose_name="Rule"
),
),
migrations.AlterField(
model_name="reaction",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="rule",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="rule",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
]
+113
View File
@@ -0,0 +1,113 @@
# Generated by Django 6.0.3 on 2026-08-12 09:02
from django.conf import settings as s
from django.db import migrations
from envipy_additional_information import Likelihood, RuleLikelihood
NEW_RULE = {
"parent": "bt0005",
"name": "bt0005-3667",
"description": "vic-unsubstituted Aromatic > vic-Dihydroxyaromatic",
"smirks": "[#8:7]([H])-[#6:1]([H])-1-[#6:2]=[#6:3]-[#6:4]=[#6:5]-[#6:6]([H])-1-[#8:8]([H])>>[#8:7]([H])-[#6:1]=1-[#6:2]=[#6:3]-[#6:4]=[#6:5]-[#6:6]=1-[#8:8]([H])",
"scenario_name": "bt0005-3667 aerobic likelihood",
"scenario_aerobic_likelihood": RuleLikelihood(likelihood=Likelihood.LIKELY),
}
RULE_FIXES = {
"bt0005-4282": "[c:1]([H])1:[c:2]([H]):[#6,#7;a:3]:[c:4]:[c:5]:[c:6]1>>[c:1]([#8])1:[c:2]([#8]):[#6,#7;a:3]:[c:4]:[c:5]:[c:6]1",
"bt0014-4215": "[c:1]([H])1[c:8][#6,#7;a:7][c:6][c:5][c:4]1[#8;!$([OH]c:[#6,#7;a:7]([OH])):9]([H])>>[#8:9]([H])[c:4]1:[c:5]:[c:6]:[#6,#7;a:7]:[c:8]:[c:1]1[#8]([H])",
# "bt0063-3938": "[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([H:3])(=[#1,#6:7]).[#6;A:2]=O",
# CN1C=NC2=C1C(=O)N(C)C(=O)N2 not working anymore with bt0063-3938 if change above is applied
"bt0063-3938": "[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O",
"bt0068-3564": "[#7:4]!@-[#6:2](!@-[#7:1])=[O:5]>>[#7:4]-[#6:2](-[O+0H1])=[O:5].[#7H1:1]",
"bt0180-2844": "[H][C:2]([#6:5]([H])([H])([H]))([#1,#6:4])!@-[#6:1]([H])([H])-[#6:3](-[#8-:8])=[O:6]>>[#6:5]([H])([H])([H])\\[#6:2](-[#1,#6:4])=[#6H:1]\\[#6:3](-[#8-:8])=[O:6]",
"bt0181-1278": "[#8-:1]-[#6:2](=[O:11])-[#6:7]=[#6:8]-[#6:3](-[H])=[#6:5](-Cl)-[#6:6](-[#8-:10])=[O:9]>>[O+0H1:10]-[#6:6](=[O:9])-[#6:5]=[#6:3]-1-[O+0:1]-[#6:2](=[O:11])-[#6:7]=[#6:8]-1",
"bt0298-3335": "[#6:1][N+:2]#[C:3]>>[#6:1]-[#7H2:2]-[#6:3]=O",
"bt0322-3393": "[H:10]\\[#6:6](=[#6:9](/[#6:1]([H])([H])([H]))-[#6:11]-[#6:12]-[#6:13]=[#6:14])-[#6:5](-[#16:7])=[O:8]>>[H:10]\\[#6:6](-[#6:5](-[#16:7])=[O:8])=[#6:9](\\[#6:11]-[#6:12]-[#6:13]=[#6:14])-[#6:1]-[#6](-[#8-])=O",
"bt0343-2675": "[#8-]-[#6](=O)-[c:1]1[c:6][cH:7][c:8](-[#7H2,#8H1:9])[cH:10][c:11]1>>[#8H][c:1]1[c:6][c:7][c:8]([*:9])[c:10][c:11]1",
"bt0350-3319": "[#6:6][#7:3][#6;!R:2]=[#7;!R:1][#6:5]>>[#6:5][#7:1][#6:2]=O.[#6:6][#7:3]", # Trig before 5 -> all of them shouldn't
"bt0374-4081": "[cH:4]1[c:16][c:15][c:14][c:13][c:3]1[#7,#8:2][c:1]1[c:8][c:9][c:10][c:11][c:12]1>>[#7,#8:2]-[c:1]1[c:12][c:11][c:10][c:9][c:8]1[c:13]1[c:14][c:15][c:16][c:4](-[#8])[c:3]1-[#8]",
"bt0378-3188": "[#8-:7][c:1]1[c:6]([#7+]([#8-])=O)[c:5][c:4]([#7+:9]([#8-])=O)[c:3][c:2]1([#7+:8]([#8-])=O)>>[#8+0:7]=[#6:1]1-[#6:6]-[#6:5]-[#6:4]([#7+:9]([#8-])=O)-[#6:3]-[#6:2]1([#7+:8]([#8-])=O)",
"bt0379-3190": "[#9,#17,#35,#53]-[#6:1](-[H])-1-[#6:5]-,=[#6:6]-[#6:7]-,=[#6:8]-[#6:2](-[H])-1-[#9,#17,#35,#53]>>[#6:6]~1-[#6:7]~[#6:8]-[#6:2]=[#6:1]-[#6:5]~1",
"bt0393-3367": "[#6:5]-[#6:1](-[#7:2](-[H])(-[H]))=[S+:3]-[#8-:6]>>[#6:5]-[#6:1](=[#7H1:2])-[S+0:3](=[O])-[#8+0H1:6]",
}
def forward_func(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
pkg_class = s.EPDB_PACKAGE_MODEL
if len(pkg_class.split(".")) != 2:
raise ValueError(
f"EPDB_PACKAGE_MODEL must be of the form 'app_label.model_name', got {pkg_class}"
)
app_label, model_name = pkg_class.split(".")
Package = apps.get_model(app_label, model_name)
SimpleAmbitRule = apps.get_model("epdb", "SimpleAmbitRule")
ParallelRule = apps.get_model("epdb", "ParallelRule")
Scenario = apps.get_model("epdb", "Scenario")
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
simple_ambit_rule_ct = ContentType.objects.get_for_model(SimpleAmbitRule)
if Package.objects.filter(name="EAWAG-BBD").exists():
p = Package.objects.get(name="EAWAG-BBD")
if not SimpleAmbitRule.objects.filter(package=p, name=NEW_RULE["name"]).exists():
# Create Missing Rule
new_sr = SimpleAmbitRule()
new_sr.polymorphic_ctype = simple_ambit_rule_ct
new_sr.package = p
new_sr.name = NEW_RULE["name"]
new_sr.description = NEW_RULE["description"]
new_sr.smirks = NEW_RULE["smirks"]
new_sr.save()
new_sr.url = "{}/simple-ambit-rule/{}".format(new_sr.package.url, new_sr.uuid)
new_sr.save()
# Add likelihood
new_scen = Scenario()
new_scen.package = p
new_scen.name = NEW_RULE["scenario_name"]
new_scen.save()
new_scen.url = "{}/scenario/{}".format(new_scen.package.url, new_scen.uuid)
new_scen.save()
ai = NEW_RULE["scenario_aerobic_likelihood"]
new_add_inf = AdditionalInformation()
new_add_inf.package = p
new_add_inf.type = ai.__class__.__name__
new_add_inf.data = ai.model_dump(mode="json")
new_add_inf.scenario = new_scen
new_add_inf.save()
new_add_inf.url = "{}/additional-information/{}".format(
new_add_inf.scenario.url, new_add_inf.uuid
)
new_add_inf.save()
# Link Scenario
new_sr.scenarios.add(new_scen)
# Link to bt0005
pr = ParallelRule.objects.get(package=p, name="bt0005")
pr.simple_rules.add(new_sr)
# Update others
for rule_name, smirks in RULE_FIXES.items():
sr = SimpleAmbitRule.objects.get(package=p, name=rule_name)
sr.smirks = smirks
sr.save()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0027_alter_compound_aliases_and_more"),
]
operations = [
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
]
@@ -0,0 +1,63 @@
# Generated by Django 6.0.3 on 2026-08-13 09:58
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0028_auto_20260812_0902"),
]
operations = [
migrations.CreateModel(
name="ReactionExplanation",
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"
),
),
("run_uuid", models.UUIDField()),
("run_start", models.DateTimeField()),
("exact", models.BooleanField(default=False)),
(
"reaction",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="epdb.reaction"
),
),
(
"rule",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="reaction",
name="explained_by",
field=models.ManyToManyField(
related_name="explained_reactions",
through="epdb.ReactionExplanation",
to="epdb.rule",
),
),
]
@@ -0,0 +1,37 @@
# Generated by Django 6.0.3 on 2026-08-14 07:41
from django.db import migrations
def forward_func(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
models = {}
for c in ContentType.objects.all():
try:
models[(c.app_label, c.model)] = apps.get_model(c.app_label, c.model)
except Exception:
pass
for ai in AdditionalInformation.objects.all():
if ai.url is None:
if ai.content_type is None:
ai.url = "{}/additional-information/{}".format(ai.scenario.url, ai.uuid)
else:
model = models[(ai.content_type.app_label, ai.content_type.model)]
obj = model.objects.get(pk=ai.object_id)
ai.url = "{}/additional-information/{}".format(obj.url, ai.uuid)
ai.save()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0029_reactionexplanation_reaction_explained_by"),
]
operations = [
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
]
+486 -100
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -477,8 +477,7 @@ def batch_predict(
limit=None, limit=None,
setting_overrides={ setting_overrides={
"max_nodes": num_tps, "max_nodes": num_tps,
"max_depth": num_tps, "model_threshold": 0.0,
"model_threshold": 0.001,
}, },
) )
+111 -58
View File
@@ -19,6 +19,7 @@ from sentry_sdk import capture_exception
from utilities.chem import FormatConverter, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
from .exceptions import InvalidMolfileException, InvalidSMILESException
from .logic import ( from .logic import (
EPDBURLParser, EPDBURLParser,
@@ -522,6 +523,7 @@ def batch_predict_pathway(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Batch Predict Pathway" context["title"] = "enviPath - Batch Predict Pathway"
context["meta"]["current_package"] = context["meta"]["user"].default_package context["meta"]["current_package"] = context["meta"]["user"].default_package
context["batch_predict_max_compounds"] = s.BATCH_PREDICT_MAX_COMPOUNDS
return render(request, "batch_predict_pathway.html", context) return render(request, "batch_predict_pathway.html", context)
@@ -587,38 +589,10 @@ def packages(request):
"package-description", s.DEFAULT_VALUES["description"] "package-description", s.DEFAULT_VALUES["description"]
) )
# EDIT START
data_pool = None
package_classification = request.POST.get("package-classification")
classification = Package.Classification(int(package_classification))
# For SECRET we'll need a data pool which will be an additional perm check later
if classification == Package.Classification.SECRET:
package_data_pool = request.POST.get("package-data-pool")
if package_data_pool is None:
return error(request, "Invalid data pool.", "Data Pool is required!")
data_pool = GroupManager.get_group_by_url(current_user, package_data_pool)
if data_pool is None:
return error(request, "Invalid data pool.", "Data Pool does not exist or no access!")
if not data_pool.secret:
return error(request, "Invalid data pool.", "Data Pool is not a secret group!")
created_package = PackageManager.create_package( created_package = PackageManager.create_package(
current_user, package_name, package_description current_user, package_name, package_description
) )
created_package.classification_level = classification
# Set previously determined data pool
if classification == Package.Classification.SECRET:
created_package.data_pool = data_pool
created_package.save()
# EDIT END
return redirect(created_package.url) return redirect(created_package.url)
elif request.method == "OPTIONS": elif request.method == "OPTIONS":
@@ -782,6 +756,11 @@ def models(request):
{"Model": s.SERVER_URL + "/model"}, {"Model": s.SERVER_URL + "/model"},
] ]
context["entity_type"] = "model"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
# Keep model_types for potential modal/action use # Keep model_types for potential modal/action use
context["model_types"] = { context["model_types"] = {
"ML Relative Reasoning": { "ML Relative Reasoning": {
@@ -794,11 +773,13 @@ def models(request):
"requires_rule_packages": True, "requires_rule_packages": True,
"requires_data_packages": True, "requires_data_packages": True,
}, },
"EnviFormer": { }
if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = {
"type": "enviformer", "type": "enviformer",
"requires_rule_packages": False, "requires_rule_packages": False,
"requires_data_packages": True, "requires_data_packages": True,
},
} }
if s.FLAGS.get("PLUGINS", False): if s.FLAGS.get("PLUGINS", False):
@@ -807,6 +788,9 @@ def models(request):
"type": k, "type": k,
"requires_rule_packages": v.requires_rule_packages(), "requires_rule_packages": v.requires_rule_packages(),
"requires_data_packages": v.requires_data_packages(), "requires_data_packages": v.requires_data_packages(),
"additional_parameters": v.Config.__name__.lower()
if v.Config.__name__ != ""
else None,
} }
for k, v in s.PROPERTY_PLUGINS.items(): for k, v in s.PROPERTY_PLUGINS.items():
context["model_types"][v.display()] = { context["model_types"][v.display()] = {
@@ -815,12 +799,6 @@ def models(request):
"requires_data_packages": v.requires_data_packages(), "requires_data_packages": v.requires_data_packages(),
} }
# Context for paginated template
context["entity_type"] = "model"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
return render(request, "collections/models_paginated.html", context) return render(request, "collections/models_paginated.html", context)
elif request.method == "POST": elif request.method == "POST":
@@ -934,11 +912,12 @@ def package_models(request, package_uuid):
"requires_rule_packages": True, "requires_rule_packages": True,
"requires_data_packages": True, "requires_data_packages": True,
}, },
"EnviFormer": { }
if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = {
"type": "enviformer", "type": "enviformer",
"requires_rule_packages": False, "requires_rule_packages": False,
"requires_data_packages": True, "requires_data_packages": True,
},
} }
if s.FLAGS.get("PLUGINS", False): if s.FLAGS.get("PLUGINS", False):
@@ -1101,13 +1080,17 @@ def package_model(request, package_uuid, model_uuid):
for pr in pred_res: for pr in pred_res:
if len(pr) > 0: if len(pr) > 0:
products = [] products = []
for prod_set in pr.product_sets: for prod_set in pr.product_sets:
logger.debug(f"Checking {prod_set}") logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set])) products.append(tuple([x for x in prod_set]))
products = list(set(products))
for prod in products:
res["pred"].append( res["pred"].append(
{ {
"products": list(set(products)), "products": list(prod),
"probability": pr.probability, "probability": pr.probability,
"btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]} "btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]}
if pr.rule is not None if pr.rule is not None
@@ -1115,6 +1098,11 @@ def package_model(request, package_uuid, model_uuid):
} }
) )
# Sort data by prob desc
res["pred"] = sorted(
res["pred"], key=lambda x: x["probability"], reverse=True
)
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
elif half_life: elif half_life:
@@ -1218,9 +1206,7 @@ def package(request, package_uuid):
if request.method == "GET": if request.method == "GET":
if request.GET.get("export", False) == "true": if request.GET.get("export", False) == "true":
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json" filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
pack_json = PackageManager.export_package( pack_json = PackageManager.export_package(current_package)
current_package, include_models=False, include_external_identifiers=False
)
response = JsonResponse(pack_json, content_type="application/json") response = JsonResponse(pack_json, content_type="application/json")
response["Content-Disposition"] = f'attachment; filename="{filename}"' response["Content-Disposition"] = f'attachment; filename="{filename}"'
@@ -1403,12 +1389,18 @@ def package_compounds(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
compound_name = request.POST.get("compound-name") compound_name = request.POST.get("compound-name")
compound_smiles = request.POST.get("compound-smiles") compound_smiles = request.POST.get("compound-smiles")
compound_molfile = request.POST.get("compound-molfile")
compound_description = request.POST.get("compound-description") compound_description = request.POST.get("compound-description")
try: try:
c = Compound.create( c = Compound.create(
current_package, compound_smiles, compound_name, compound_description current_package,
compound_smiles,
molfile=compound_molfile,
name=compound_name,
description=compound_description,
) )
except ValueError as e: except (InvalidSMILESException, InvalidMolfileException) as e:
raise BadRequest(str(e)) raise BadRequest(str(e))
return redirect(c.url) return redirect(c.url)
@@ -1534,11 +1526,15 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST": elif request.method == "POST":
structure_name = request.POST.get("structure-name") structure_name = request.POST.get("structure-name")
structure_smiles = request.POST.get("structure-smiles") structure_smiles = request.POST.get("structure-smiles")
structure_molfile = request.POST.get("structure-molfile")
structure_description = request.POST.get("structure-description") structure_description = request.POST.get("structure-description")
try: try:
cs = current_compound.add_structure( cs = current_compound.add_structure(
structure_smiles, structure_name, structure_description structure_smiles,
molfile=structure_molfile,
name=structure_name,
description=structure_description,
) )
except ValueError: except ValueError:
return error( return error(
@@ -1926,9 +1922,24 @@ def package_reactions(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
reaction_name = request.POST.get("reaction-name") reaction_name = request.POST.get("reaction-name")
reaction_description = request.POST.get("reaction-description") reaction_description = request.POST.get("reaction-description")
reactions_smirks = request.POST.get("reaction-smirks") reaction_smiles = request.POST.get("reaction-smiles")
educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".") if reaction_smiles is None or reaction_smiles.strip() == "":
return error(
request,
"Reaction SMILES is empty / missing",
"No reaction SMILES provided. Please provide a SMILES for the reaction.",
)
if not FormatConverter.is_valid_smirks(reaction_smiles):
return error(
request,
"Reaction SMILES is invalid",
f"The provided reactions SMILES {reaction_smiles} is invalid",
)
educts = reaction_smiles.split(">>")[0].split(".")
products = reaction_smiles.split(">>")[1].split(".")
r = Reaction.create( r = Reaction.create(
current_package, current_package,
@@ -2109,12 +2120,14 @@ def package_pathways(request, package_uuid):
else: else:
prediction_setting = current_user.prediction_settings() prediction_setting = current_user.prediction_settings()
is_predict_mode = pw_mode in {"predict", "incremental"}
pw = Pathway.create( pw = Pathway.create(
current_package, current_package,
stand_smiles, stand_smiles if is_predict_mode else smiles,
name=name, name=name,
description=description, description=description,
predicted=pw_mode in {"predict", "incremental"}, predicted=is_predict_mode,
) )
# set mode # set mode
@@ -2355,8 +2368,17 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
node_name = request.POST.get("node-name") node_name = request.POST.get("node-name")
node_description = request.POST.get("node-description") node_description = request.POST.get("node-description")
node_smiles = request.POST.get("node-smiles").strip() node_smiles = request.POST.get("node-smiles")
current_pathway.add_node(node_smiles, name=node_name, description=node_description) node_molfile = request.POST.get("node-molfile")
try:
current_pathway.add_node(
node_smiles, molfile=node_molfile, name=node_name, description=node_description
)
except InvalidSMILESException:
return error(
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
)
return redirect(current_pathway.url) return redirect(current_pathway.url)
@@ -2464,7 +2486,26 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
return JsonResponse({"success": current_node.url}) return JsonResponse({"success": current_node.url})
return HttpResponseBadRequest() new_node_name = request.POST.get("node-name")
new_node_description = request.POST.get("node-description")
if any([new_node_name, new_node_description]):
if new_node_name is not None and new_node_name.strip() != "":
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
current_node.name = new_node_name
if new_node_description is not None and new_node_description.strip() != "":
new_node_description = nh3.clean(
new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
).strip()
current_node.description = new_node_description
current_node.save()
return redirect(current_node.url)
return error(request, "Node update failed!", "No changes were made to the node")
else: else:
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])
@@ -2534,6 +2575,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
substrate_nodes, product_nodes, name=edge_name, description=edge_description substrate_nodes, product_nodes, name=edge_name, description=edge_description
) )
# Update depths as sideeffect of above operation
current_pathway.update_depths()
return redirect(current_pathway.url) return redirect(current_pathway.url)
else: else:
@@ -3105,12 +3149,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")
+12 -8
View File
@@ -120,13 +120,6 @@ class PathwayMapper:
) )
bundle.reference_substances.append(ref_sub) bundle.reference_substances.append(ref_sub)
sub = IUCLIDSubstanceData(
uuid=sub_uuid,
name=compound.name,
reference_substance_uuid=ref_sub_uuid,
)
bundle.substances.append(sub)
if not export.compounds: if not export.compounds:
return bundle return bundle
@@ -145,6 +138,16 @@ class PathwayMapper:
if not root_compound_pks: if not root_compound_pks:
return bundle return bundle
for root_pk in root_compound_pks:
root_sub_uuid, root_ref_uuid = seen_compounds[root_pk]
bundle.substances.append(
IUCLIDSubstanceData(
uuid=root_sub_uuid,
name=compound_names[root_pk],
reference_substance_uuid=root_ref_uuid,
)
)
edge_templates: list[tuple[UUID, frozenset[int], tuple[int, ...], tuple[UUID, ...]]] = [] edge_templates: list[tuple[UUID, frozenset[int], tuple[int, ...], tuple[UUID, ...]]] = []
for edge in sorted(export.edges, key=lambda item: str(item.edge_uuid)): for edge in sorted(export.edges, key=lambda item: str(item.edge_uuid)):
parent_compound_pks = sorted( parent_compound_pks = sorted(
@@ -348,7 +351,8 @@ class PathwayMapper:
props = SoilPropertiesData() props = SoilPropertiesData()
for ai in ai_list: for ai_obj in ai_list:
ai = ai_obj.get()
if isinstance(ai, SoilTexture1) and props.soil_type is None: if isinstance(ai, SoilTexture1) and props.soil_type is None:
props.soil_type = ai.type.value props.soil_type = ai.type.value
elif isinstance(ai, SoilTexture2): elif isinstance(ai, SoilTexture2):
+1 -2
View File
@@ -70,8 +70,7 @@ class IUCLIDExportAPITest(TestCase):
names = zf.namelist() names = zf.namelist()
self.assertIn("manifest.xml", names) self.assertIn("manifest.xml", names)
i6d_files = [n for n in names if n.endswith(".i6d")] i6d_files = [n for n in names if n.endswith(".i6d")]
# 2 substances + 2 ref substances + 1 ESR = 5 i6d files self.assertEqual(len(i6d_files), 4)
self.assertEqual(len(i6d_files), 5)
def test_anonymous_returns_401(self): def test_anonymous_returns_401(self):
self.client.logout() self.client.logout()
+46
View File
@@ -7,6 +7,11 @@ from uuid import uuid4
from django.test import SimpleTestCase, tag from django.test import SimpleTestCase, tag
from epapi.v1.interfaces.iuclid.dto import (
PathwayCompoundDTO,
PathwayEdgeDTO,
PathwayExportDTO,
)
from epiuclid.serializers.i6z import I6ZSerializer from epiuclid.serializers.i6z import I6ZSerializer
from epiuclid.serializers.pathway_mapper import ( from epiuclid.serializers.pathway_mapper import (
IUCLIDDocumentBundle, IUCLIDDocumentBundle,
@@ -14,9 +19,24 @@ from epiuclid.serializers.pathway_mapper import (
IUCLIDReferenceSubstanceData, IUCLIDReferenceSubstanceData,
IUCLIDSubstanceData, IUCLIDSubstanceData,
IUCLIDTransformationProductEntry, IUCLIDTransformationProductEntry,
PathwayMapper,
) )
def _unlinked_documents(manifest_xml: str) -> list[tuple[str | None, str]]:
ns = "http://iuclid6.echa.europa.eu/namespaces/manifest/v1"
root = ET.fromstring(manifest_xml)
base = root.findtext(f"{{{ns}}}base-document-uuid")
linked_targets: set[str | None] = {base}
docs: dict[str, str | None] = {}
for doc in root.findall(f".//{{{ns}}}document"):
uuid = doc.findtext(f"{{{ns}}}uuid")
docs[uuid] = doc.findtext(f"{{{ns}}}type")
for link in doc.findall(f"{{{ns}}}links/{{{ns}}}link"):
linked_targets.add(link.findtext(f"{{{ns}}}ref-uuid"))
return [(doc_type, uuid) for uuid, doc_type in docs.items() if uuid not in linked_targets]
def _make_bundle() -> IUCLIDDocumentBundle: def _make_bundle() -> IUCLIDDocumentBundle:
ref_uuid = uuid4() ref_uuid = uuid4()
sub_uuid = uuid4() sub_uuid = uuid4()
@@ -197,3 +217,29 @@ class I6ZSerializerTest(SimpleTestCase):
} }
self.assertIn(parent_ref_key, reference_links) self.assertIn(parent_ref_key, reference_links)
self.assertIn(product_ref_key, reference_links) self.assertIn(product_ref_key, reference_links)
def test_multi_compound_pathway_has_no_unlinked_documents(self):
compounds = [
PathwayCompoundDTO(pk=1, name="Root", smiles="c1ccccc1"),
PathwayCompoundDTO(pk=2, name="P1", smiles="CCO"),
PathwayCompoundDTO(pk=3, name="P2", smiles="CCN"),
PathwayCompoundDTO(pk=4, name="P3", smiles="CCC"),
]
export = PathwayExportDTO(
pathway_uuid=uuid4(),
pathway_name="Regression Pathway",
compounds=compounds,
edges=[
PathwayEdgeDTO(edge_uuid=uuid4(), start_compound_pks=[1], end_compound_pks=[2]),
PathwayEdgeDTO(edge_uuid=uuid4(), start_compound_pks=[1], end_compound_pks=[3]),
PathwayEdgeDTO(edge_uuid=uuid4(), start_compound_pks=[2], end_compound_pks=[4]),
],
root_compound_pks=[1],
)
bundle = PathwayMapper().map(export)
data = I6ZSerializer().serialize(bundle)
with zipfile.ZipFile(io.BytesIO(data)) as zf:
manifest_xml = zf.read("manifest.xml").decode("utf-8")
self.assertEqual(_unlinked_documents(manifest_xml), [])
+2 -3
View File
@@ -31,7 +31,7 @@ class PathwayMapperTest(SimpleTestCase):
) )
bundle = PathwayMapper().map(export) bundle = PathwayMapper().map(export)
self.assertEqual(len(bundle.substances), 2) self.assertEqual(len(bundle.substances), 1)
self.assertEqual(len(bundle.reference_substances), 2) self.assertEqual(len(bundle.reference_substances), 2)
self.assertEqual(len(bundle.endpoint_study_records), 1) self.assertEqual(len(bundle.endpoint_study_records), 1)
@@ -49,8 +49,7 @@ class PathwayMapperTest(SimpleTestCase):
) )
bundle = PathwayMapper().map(export) bundle = PathwayMapper().map(export)
# 2 unique compounds -> 2 substances, 2 ref substances self.assertEqual(len(bundle.substances), 1)
self.assertEqual(len(bundle.substances), 2)
self.assertEqual(len(bundle.reference_substances), 2) self.assertEqual(len(bundle.reference_substances), 2)
# One endpoint study record per pathway # One endpoint study record per pathway
self.assertEqual(len(bundle.endpoint_study_records), 1) self.assertEqual(len(bundle.endpoint_study_records), 1)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+30 -2
View File
@@ -65,6 +65,25 @@ def run_both_engines(SMILES, SMIRKS):
def migration(request): def migration(request):
accepted_diffs = [
"bt0055-3469.1",
"bt0056-2685",
"bt0193-4263",
"bt0231-1871.1",
"bt0242-3803",
"bt0254-4224.1",
"bt0254-4224.2",
"bt0291-1129",
"bt0337-3543",
"bt0391-4285",
"bt0402-3576",
"bt0404-3928",
"bt0416-4269",
"bt0432-4254",
"bt0337-4117",
"bt0322-3393",
]
if request.method == "GET": if request.method == "GET":
context = get_base_context(request) context = get_base_context(request)
@@ -109,11 +128,11 @@ def migration(request):
), ),
"id": str(r.uuid), "id": str(r.uuid),
"url": r.url, "url": r.url,
"status": res, "status": res or r.name in accepted_diffs,
} }
) )
if res: if res or r.name in accepted_diffs:
success += 1 success += 1
else: else:
error += 1 error += 1
@@ -135,7 +154,16 @@ def migration(request):
for r in migration_status["results"]: for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL) r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
if r["name"] in accepted_diffs:
r["status"] = True
migration_status["results"] = sorted(
migration_status["results"], key=lambda x: (x["status"], x["name"])
)
num_success = sum([int(x["status"]) for x in migration_status["results"]])
migration_status["success"] = num_success
migration_status["error"] = migration_status["total"] - num_success
context.update(**migration_status) context.update(**migration_status)
return render(request, "migration.html", context) return render(request, "migration.html", context)
+6
View File
@@ -21,5 +21,11 @@
"django", "django",
"tailwindcss", "tailwindcss",
"daisyui" "daisyui"
],
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@tailwindcss/oxide"
] ]
} }
}
+4 -4
View File
@@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
import matplotlib.patches as mpatches import matplotlib.patches as mpatches
import numpy as np import numpy as np
from matplotlib import pyplot as plt from matplotlib.figure import Figure
from scipy import stats from scipy import stats
""" """
@@ -101,7 +101,8 @@ class PepperPrediction(PropertyPrediction):
mask_red = x > vp mask_red = x > vp
# Plot # Plot
fig, ax = plt.subplots(figsize=(9, 5.5)) fig = Figure(figsize=(9, 5.5))
ax = fig.subplots()
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF") ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
if np.any(mask_green): if np.any(mask_green):
@@ -146,13 +147,12 @@ class PepperPrediction(PropertyPrediction):
] ]
ax.legend(handles=patches, frameon=True) ax.legend(handles=patches, frameon=True)
plt.tight_layout() fig.tight_layout()
# --- Export to SVG string --- # --- Export to SVG string ---
buf = io.StringIO() buf = io.StringIO()
fig.savefig(buf, format="svg", bbox_inches="tight") fig.savefig(buf, format="svg", bbox_inches="tight")
svg = buf.getvalue() svg = buf.getvalue()
plt.close(fig)
buf.close() buf.close()
return svg return svg
+3 -2
View File
@@ -187,8 +187,9 @@ class Pepper:
groups = [group for group in dataset.group_by("structure_id")] groups = [group for group in dataset.group_by("structure_id")]
# Unless explicitly set compute everything serial # Unless explicitly set compute everything serial
if os.environ.get("N_PEPPER_THREADS", 1) > 1: n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])( if n_threads > 1:
results = Parallel(n_jobs=n_threads)(
delayed(compute_bayes_per_group)(group[1]) delayed(compute_bayes_per_group)(group[1])
for group in dataset.group_by("structure_id") for group in dataset.group_by("structure_id")
) )
+2
View File
@@ -1,3 +1,5 @@
allowBuilds:
'@parcel/watcher': true
onlyBuiltDependencies: onlyBuiltDependencies:
- '@parcel/watcher' - '@parcel/watcher'
- '@tailwindcss/oxide' - '@tailwindcss/oxide'
+9
View File
@@ -34,3 +34,12 @@
} }
@import "./daisyui-theme.css"; @import "./daisyui-theme.css";
select.select[multiple] {
display: block;
white-space: normal;
}
p a {
@apply underline;
}
+31
View File
@@ -59,6 +59,9 @@ document.addEventListener("alpine:init", () => {
get isEditMode() { get isEditMode() {
return this.mode === "edit"; return this.mode === "edit";
}, },
get isRequired() {
return (this.schema.required || []).indexOf(this.fieldName) > -1
}
}); });
// Text widget // Text widget
@@ -293,6 +296,34 @@ document.addEventListener("alpine:init", () => {
}), }),
); );
// PubMed link widget
Alpine.data(
"doiWidget",
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
...baseWidget(
fieldName,
data,
schema,
uiSchema,
mode,
debugErrors,
context,
),
get value() {
return this.data[this.fieldName] || "";
},
set value(v) {
this.data[this.fieldName] = v;
},
get doiUrl() {
return this.value
? `https://dx.doi.org/${this.value}`
: null;
},
}),
);
// Compound link widget // Compound link widget
Alpine.data( Alpine.data(
"compoundWidget", "compoundWidget",
+120
View File
@@ -5,6 +5,126 @@
*/ */
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
const basePagination = (
items,
currentPage,
totalPages,
totalItems,
perPage,
isReviewed,
instanceId
) => ({
items,
currentPage,
totalPages,
totalItems,
perPage,
isReviewed,
instanceId,
get paginatedItems() {
return this.items;
},
get showingStart() {
if (this.totalItems === 0) return 0;
return (this.currentPage - 1) * this.perPage + 1;
},
get showingEnd() {
if (this.totalItems === 0) return 0;
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.fetchPage(this.currentPage + 1);
}
},
prevPage() {
if (this.currentPage > 1) {
this.fetchPage(this.currentPage - 1);
}
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.fetchPage(page);
}
},
get pageNumbers() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
if (total === 0) {
return pages;
}
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
} else {
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
let rangeStart;
let rangeEnd;
if (current <= 4) {
rangeStart = 2;
rangeEnd = 5;
} else if (current >= total - 3) {
rangeStart = total - 4;
rangeEnd = total - 1;
} else {
rangeStart = current - 1;
rangeEnd = current + 1;
}
if (rangeStart > 2) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
}
for (let i = rangeStart; i <= rangeEnd; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
if (rangeEnd < total - 1) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
}
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
}
return pages;
}
});
Alpine.data('paginatedList',
(items, options = {}) => ({
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
init() {
this.fetchPage(1);
},
async fetchPage(page) {
this.totalItems = this.items.length;
this.totalPages = Math.ceil(this.items.length / this.perPage);
this.currentPage = page
const start = page * this.perPage - this.perPage;
const end = page * this.perPage;
return this.items.slice(start, end);
}
})
);
Alpine.data('remotePaginatedList', (options = {}) => ({ Alpine.data('remotePaginatedList', (options = {}) => ({
items: [], items: [],
currentPage: 1, currentPage: 1,
+34
View File
@@ -186,6 +186,40 @@ window.AdditionalInformationApi = {
return this._handleResponse(response, "createItem"); return this._handleResponse(response, "createItem");
}, },
/**
* Create new additional information and attach it to an object.
* @param {string} modelName - Name/type of the additional information model
* @param {Object} data - Data for the new item
* @param {string} attachObjectUrl - UUID of the object this data should be attached to
* @param {string} scenarioUuid - UUID of the scenario
* @returns {Promise<{status: string, uuid: string}>}
*/
async createItemOnNonScenarioObject(modelName, data, attachObjectUrl, scenarioUuid) {
const sanitizedData = this.sanitizePayload(data);
this._log("createItemOnNonScenarioObject", { modelName, data: sanitizedData, attachObjectUrl, scenarioUuid });
sanitizedData.attach_obj_url = attachObjectUrl;
if (scenarioUuid) {
sanitizedData.scenario_uuid = scenarioUuid;
}
// Normalize model name to lowercase
const normalizedName = modelName.toLowerCase();
const response = await fetch(
`/api/v1/information/${normalizedName}/`,
{
method: "POST",
headers: this._buildHeaders(),
body: JSON.stringify(sanitizedData),
},
);
return this._handleResponse(response, "createItemOnNonScenarioObject");
},
/** /**
* Delete additional information from a scenario * Delete additional information from a scenario
* @param {string} scenarioUuid - UUID of the scenario * @param {string} scenarioUuid - UUID of the scenario
-57
View File
@@ -1,57 +0,0 @@
## Prerequisites
Stable [Node.js](https://nodejs.org) version
## Build instructions
npm install
npm start
For production build:
npm run build
You could also build only the style with command
npm run style
## Indigo Service
Ketcher uses Indigo Service for server operations.
You can use `--api-path` parameter to start with it:
npm start -- --api-path=<server-url>
For production build:
npm run build -- --api-path=<server-url>
You can find the instruction for service installation
[here](http://lifescience.opensource.epam.com/indigo/service/index.html).
## Tests instructions
You can start tests for input/output `.mol`-files and render.
npm test
Tests are started for all structures in `test/fixtures` directory.
To start the tests separately:
npm run test-io
npm run test-render
#### Parameters
You can use following parameters to start the tests:
- `--fixtures` - for the choice of a specific directory with molecules
- `--headless` - for start of the browser in headless mode
```
npm run test-render -- --fixtures=fixtures/super --headless
```
If you have added new structures for testing to the `test/fixtures` directory
you have to generate `svg` from them for correct render-test with:
npm run generate-svg
-184
View File
@@ -1,184 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 EPAM Systems
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-5
View File
@@ -1,5 +0,0 @@
Ketcher version 1 was released under GNU Affero General Public License v3.0
Ketcher version 2 was re-licensed under Apache License, Version 2.
Current version is distributed by the terms of the Apache License, Version 2.
which is included in the file LICENSE, found at the root of the Ketcher source tree.
-19
View File
@@ -1,19 +0,0 @@
Ketcher
Copyright (C) 2017 EPAM Systems
This product includes software developed at EPAM Systems, Inc.
In addition, this product contains dependencies on files licensed under:
The FreeBSD Documentation License https://www.freebsd.org/copyright/freebsd-doc-license.html
The MIT License https://opensource.org/licenses/MIT
X11 License http://www.xfree86.org/3.3.6/COPYRIGHT2.html
Academic Free License https://opensource.org/licenses/AFL-3.0
Apache License, Version 1.0 http://www.apache.org/licenses/LICENSE-1.0
Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
The 2-Clause BSD License https://opensource.org/licenses/BSD-2-Clause
The 3-Clause BSD License https://opensource.org/licenses/BSD-3-Clause
ISC License (ISC) https://opensource.org/licenses/ISC
GNU Lesser General Public License version 2.1 https://opensource.org/licenses/LGPL-2.1
The Mozilla Public License https://opensource.org/licenses/MPL-1.0
Public Domain https://wiki.creativecommons.org/wiki/Public_domain
Unlicense http://unlicense.org/
-35
View File
@@ -1,35 +0,0 @@
# EPAM Ketcher projects
Copyright (c) 2017 EPAM Systems, Inc
Ketcher is an open-source web-based chemical structure editor incorporating high performance, good portability, light weight, and ability to easily integrate into a custom web-application. Ketcher is designed for chemists, laboratory scientists and technicians who draw structures and reactions.
## KEY FEATURES
* Fast 2D structure representation that satisfies common chemical drawing standards
* 3D structure visualization
* Draw and edit structures using major tools: Atom Tool, Bond Tool, and Template Tool
* Template library (including custom and user's templates)
* Add atom and bond basic properties and query features, add aliases and Generic groups
* Select, modify, and erase connected and unconnected atoms and bonds using Selection Tool, or using Shift key
* Simple Structure Clean up Tool (checks bonds length, angles and spatial arrangement of atoms) and Advanced Structure Clean up Tool (+ stereochemistry checking and structure layout)
* Aromatize/De-aromatize Tool
* Calculate CIP Descriptors Tool
* Structure Check Tool
* MW and Structure Parameters Calculate Tool
* Stereochemistry support during editing, loading, and saving chemical structures
* Storing history of actions, with the ability to rollback to previous state
* Ability to load and save structures and reactions in MDL Molfile or RXN file format, InChI String, ChemAxon Extended SMILES, ChemAxon Extended CML file formats
* Easy to use R-Group and S-Group tools (Generic, Multiple group, SRU polymer, peratom, Data S-Group)
* Reaction Tool (reaction generating, manual and automatic atom-to-atom mapping)
* Flip/Rotate Tool
* Zoom in/out, hotkeys, cut/copy/paste
* OCR - ability to recognize structures at pictures (image files) and reproduce them
* Copy and paste between different chemical editors
* Settings support (Rendering, Displaying, Debugging)
* Use of SVG to achieve best quality in-browser chemical structure rendering
* Languages: JavaScript with third-party libraries
## Build instructions
Please read [DEVNOTES.md](DEVNOTES.md) for details.
## License
Please read [LICENSE](LICENSE) and [NOTICE](NOTICE) for details.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Some files were not shown because too many files have changed in this diff Show More