forked from enviPath/enviPy
Compare commits
21 Commits
e3876ac945
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc9f86ff1 | |||
| dba6514013 | |||
| a092d4a558 | |||
| 2502c020f7 | |||
| 6ab9180291 | |||
| 3657d14659 | |||
| ef6091d416 | |||
| 14cfc1e4d7 | |||
| 868bbf5c05 | |||
| be5ee1d1d7 | |||
| 20fd949dfd | |||
| c9b643fe6e | |||
| 1a9f1cf9af | |||
| c7c7e17e43 | |||
| 674e10c7fa | |||
| 8079b80d57 | |||
| 76e63fda2c | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e |
15
Dockerfile
15
Dockerfile
@ -6,18 +6,23 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
curl \
|
||||
openssh-client \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
# Install Node 22 + pnpm
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@latest --activate \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
@ -32,7 +37,7 @@ 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:
|
||||
# docker build --ssh default -t envipath/envipy:1.0 .
|
||||
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
|
||||
# Ensure .dockerignore is reasonable
|
||||
|
||||
@ -254,7 +254,14 @@ class Classifier(Plugin):
|
||||
def parse_config(cls, data: dict | None = None) -> EnviPyModel | None:
|
||||
if cls.Config is 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
|
||||
def create(cls, data: dict | None = None):
|
||||
|
||||
26
epapi/v1/endpoints/joblogs.py
Normal file
26
epapi/v1/endpoints/joblogs.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.conf import settings as s
|
||||
from ninja import Router
|
||||
from ninja_extra.pagination import paginate
|
||||
|
||||
from epdb.models import JobLog
|
||||
from ..pagination import EnhancedPageNumberPagination
|
||||
from ..schemas import JobLogOutSchema
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
|
||||
@paginate(
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
)
|
||||
def list_all_joblogs(request):
|
||||
"""
|
||||
List all JobLogs from reviewed packages.
|
||||
"""
|
||||
current_user = request.user
|
||||
|
||||
if current_user.is_superuser:
|
||||
return JobLog.objects.all().order_by("-created")
|
||||
else:
|
||||
return JobLog.objects.filter(user=current_user).order_by("-created")
|
||||
@ -15,6 +15,7 @@ from .endpoints import (
|
||||
additional_information,
|
||||
settings,
|
||||
groups,
|
||||
joblogs,
|
||||
)
|
||||
|
||||
# Main router with authentication
|
||||
@ -37,6 +38,7 @@ router.add_router("", structure.router)
|
||||
router.add_router("", additional_information.router)
|
||||
router.add_router("", settings.router)
|
||||
router.add_router("", groups.router)
|
||||
router.add_router("", joblogs.router)
|
||||
|
||||
if s.IUCLID_EXPORT_ENABLED:
|
||||
from epiuclid.api import router as iuclid_router
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from ninja import FilterSchema, FilterLookup, Schema
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from django.urls import reverse
|
||||
from ninja import Field, FilterSchema, FilterLookup, Schema
|
||||
|
||||
|
||||
# Filter schema for query parameters
|
||||
class ReviewStatusFilter(FilterSchema):
|
||||
@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
|
||||
url: str = ""
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class SimpleUserOutSchema(Schema):
|
||||
uuid: UUID
|
||||
url: str
|
||||
name: str = Field(alias="username")
|
||||
|
||||
|
||||
class JobLogOutSchema(Schema):
|
||||
user: SimpleUserOutSchema
|
||||
id: UUID = Field(alias="task_id")
|
||||
url: str
|
||||
name: str = Field(alias="job_name")
|
||||
created: datetime = Field(alias="created")
|
||||
status: str = Field(alias="status")
|
||||
done: Optional[datetime] = Field(None, alias="done_at")
|
||||
|
||||
@staticmethod
|
||||
def resolve_url(obj):
|
||||
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
|
||||
|
||||
2
epdb/exceptions.py
Normal file
2
epdb/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class InvalidSMILESException(Exception):
|
||||
pass
|
||||
@ -386,23 +386,50 @@ class PackageSchema(Schema):
|
||||
|
||||
@staticmethod
|
||||
def resolve_readers(obj: Package):
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.READ[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
readers = []
|
||||
|
||||
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
|
||||
def resolve_writers(obj: Package):
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.WRITE[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
writers = []
|
||||
|
||||
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
|
||||
def resolve_review_comment(obj):
|
||||
@ -799,6 +826,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(
|
||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||
)
|
||||
@ -1325,6 +1373,7 @@ class ScenarioSchema(Schema):
|
||||
aliases: List[str] = Field([], alias="aliases")
|
||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||
collectionID: Optional[str] = None
|
||||
date: str = Field(None, alias="scenario_date")
|
||||
description: str = Field(None, alias="description")
|
||||
id: str = Field(None, alias="url")
|
||||
identifier: str = "scenario"
|
||||
@ -1468,28 +1517,56 @@ def create_package_additional_information(request, package_uuid):
|
||||
scen = request.POST.get("scenario")
|
||||
scenario = Scenario.objects.get(package=p, url=scen)
|
||||
|
||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||
attach_obj = url_parser.get_object()
|
||||
if request.POST.get("adInfoTypes[]"):
|
||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||
attach_obj = url_parser.get_object()
|
||||
|
||||
if not hasattr(attach_obj, "additional_information"):
|
||||
raise ValueError("Can't attach additional information to this object!")
|
||||
if not 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!"
|
||||
)
|
||||
if not attach_obj.url.startswith(p.url):
|
||||
raise ValueError(
|
||||
"Additional Information can only be set to objects stored in the same package!"
|
||||
)
|
||||
|
||||
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||
|
||||
for t in types:
|
||||
ai = build_additional_information_from_request(request, t)
|
||||
for t in types:
|
||||
ai = build_additional_information_from_request(request, t)
|
||||
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
ai,
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
ai,
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
|
||||
elif request.POST.get("ais"):
|
||||
import json
|
||||
|
||||
parsed_ais = json.loads(request.POST.get("ais"))
|
||||
|
||||
for ai_type, ais in parsed_ais.items():
|
||||
for ai in ais:
|
||||
attach_obj = None
|
||||
if ai.get("related"):
|
||||
url_parser = EPDBURLParser(ai.get("related").get("url"))
|
||||
attach_obj = url_parser.get_object()
|
||||
|
||||
if not hasattr(attach_obj, "additional_information"):
|
||||
raise ValueError("Can't attach additional information to this object!")
|
||||
|
||||
if not attach_obj.url.startswith(p.url):
|
||||
raise ValueError(
|
||||
"Additional Information can only be set to objects stored in the same package!"
|
||||
)
|
||||
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
AdditionalInformation.from_dict(ai_type, ai),
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
|
||||
# TODO implement additional information endpoint ?
|
||||
return redirect(f"{scenario.url}")
|
||||
@ -1533,13 +1610,14 @@ class PathwayNode(Schema):
|
||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||
id: str = Field(None, alias="url")
|
||||
idcomp: str = Field(None, alias="default_node_label.url")
|
||||
idreact: str = Field(None, alias="default_node_label.url")
|
||||
idcomp: str = Field(None, alias="node_label_id")
|
||||
idreact: str = Field(None, alias="node_label_id")
|
||||
image: str = Field(None, alias="image")
|
||||
imageSize: int = Field(None, alias="image_size")
|
||||
name: str = Field(None, alias="name")
|
||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
smiles: str = Field(None, alias="smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
|
||||
@staticmethod
|
||||
def resolve_atom_count(obj: Node):
|
||||
@ -1693,6 +1771,29 @@ def create_package_pathway(
|
||||
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}")
|
||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||
try:
|
||||
@ -1813,7 +1914,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||
|
||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||
node_depth = int(n.nodeDepth)
|
||||
node_depth = int(float(n.nodeDepth))
|
||||
else:
|
||||
node_depth = -1
|
||||
|
||||
@ -1958,6 +2059,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
for pr in e.products.split(","):
|
||||
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(
|
||||
pathway=pw,
|
||||
start_nodes=educts,
|
||||
@ -1965,8 +2070,12 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
rule=None,
|
||||
name=None,
|
||||
description=e.edgeReason,
|
||||
multi_step=multi_step,
|
||||
)
|
||||
|
||||
# Update depths as sideeffect of above operation
|
||||
pw.update_depths()
|
||||
|
||||
return redirect(new_e.url)
|
||||
except ValueError:
|
||||
return 403, {"message": "Adding Edge failed!"}
|
||||
|
||||
@ -995,52 +995,9 @@ class PackageManager(object):
|
||||
|
||||
print("Fixing Node depths...")
|
||||
total_pws = Pathway.objects.filter(package=pack).count()
|
||||
|
||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||
in_count = defaultdict(lambda: 0)
|
||||
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)
|
||||
|
||||
pw.update_depths()
|
||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||
|
||||
return pack
|
||||
|
||||
@ -99,11 +99,15 @@ class Command(BaseCommand):
|
||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||
new_license.save()
|
||||
|
||||
def import_package(self, data, owner):
|
||||
return PackageManager.import_legacy_package(
|
||||
def import_package(self, data, owner, all_envipath_user_group):
|
||||
p = PackageManager.import_legacy_package(
|
||||
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
||||
)
|
||||
|
||||
PackageManager.grant_read(owner, p, all_envipath_user_group)
|
||||
|
||||
return p
|
||||
|
||||
def create_default_setting(self, owner, packages):
|
||||
s = SettingManager.create_setting(
|
||||
owner,
|
||||
@ -198,7 +202,7 @@ class Command(BaseCommand):
|
||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||
).read()
|
||||
)
|
||||
imported_package = self.import_package(package_data, admin)
|
||||
imported_package = self.import_package(package_data, admin, g)
|
||||
mapping[p.replace(".json", "")] = imported_package
|
||||
|
||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||
|
||||
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
||||
|
||||
from django.db import migrations
|
||||
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
||||
|
||||
MAPPING = {
|
||||
"": HalfLifeModel.OTHER,
|
||||
"HS-SFO": HalfLifeModel.HS_SFO,
|
||||
"FOMC": HalfLifeModel.FOMC,
|
||||
"FOTC": HalfLifeModel.DFOP,
|
||||
"FMOC": HalfLifeModel.FOMC,
|
||||
"DFOP": HalfLifeModel.DFOP,
|
||||
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
||||
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
||||
"first order kinetics": HalfLifeModel.SFO,
|
||||
"SFO²": HalfLifeModel.SFO,
|
||||
"HS": HalfLifeModel.HS,
|
||||
"top down": HalfLifeModel.OTHER,
|
||||
"SFO": HalfLifeModel.SFO,
|
||||
"First Order": HalfLifeModel.SFO,
|
||||
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
||||
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
||||
"true": HalfLifeModel.SFO,
|
||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||
}
|
||||
|
||||
|
||||
def forward_func(apps, schema_editor):
|
||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||
|
||||
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
||||
|
||||
for hl in hls:
|
||||
data = hl.data
|
||||
data["model"] = MAPPING[data["model"]].value
|
||||
hl.data = HalfLife(**data).model_dump(mode="json")
|
||||
hl.save()
|
||||
|
||||
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
||||
|
||||
for hl in hlws:
|
||||
data = hl.data
|
||||
data["model"] = MAPPING[data["model"]].value
|
||||
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
||||
hl.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0024_user_contacted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 6.0.3 on 2026-06-02 17:18
|
||||
|
||||
from django.db import migrations
|
||||
from envipy_additional_information import DOI
|
||||
|
||||
|
||||
def forward_func(apps, schema_editor):
|
||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||
|
||||
refs = AdditionalInformation.objects.filter(type="Reference")
|
||||
|
||||
remaining = []
|
||||
|
||||
for ref in refs:
|
||||
r = ref.data["reference"]
|
||||
try:
|
||||
# PubMed IDs are plain ints, try parsing
|
||||
_ = int(r)
|
||||
# Nothing to do
|
||||
except ValueError:
|
||||
DOMAINS = [
|
||||
"http://dx.doi.org/",
|
||||
"https://dx.doi.org/",
|
||||
"http://doi.org/",
|
||||
"https://doi.org/",
|
||||
]
|
||||
for d in DOMAINS:
|
||||
r = r.replace(d, "")
|
||||
|
||||
if r.startswith("10."):
|
||||
ref.type = DOI.__name__
|
||||
ref.data = {"doi": r}
|
||||
ref.save()
|
||||
else:
|
||||
remaining.append(ref)
|
||||
|
||||
if len(remaining) > 0:
|
||||
raise ValueError(f"Could not parse {len(remaining)} references")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0025_auto_20260511_2025"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
131
epdb/models.py
131
epdb/models.py
@ -31,6 +31,7 @@ from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
from bridge.contracts import Property
|
||||
from bridge.dto import RunResult, PropertyPrediction
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||
from utilities.ml import (
|
||||
ApplicabilityDomainPCA,
|
||||
@ -631,7 +632,7 @@ class EnviPathModel(TimeStampedModel):
|
||||
|
||||
class AliasMixin(models.Model):
|
||||
aliases = ArrayField(
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
@ -654,7 +655,9 @@ class AliasMixin(models.Model):
|
||||
|
||||
|
||||
class ScenarioMixin(models.Model):
|
||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
||||
scenarios = models.ManyToManyField(
|
||||
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||
@ -780,7 +783,7 @@ class Compound(
|
||||
"CompoundStructure",
|
||||
verbose_name="Default Structure",
|
||||
related_name="compound_default_structure",
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
|
||||
@ -857,13 +860,13 @@ class Compound(
|
||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
) -> "Compound":
|
||||
if smiles is None or smiles.strip() == "":
|
||||
raise ValueError("SMILES is required")
|
||||
raise InvalidSMILESException("SMILES is required")
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise ValueError("Given SMILES is invalid")
|
||||
raise InvalidSMILESException("Given SMILES is invalid")
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
|
||||
@ -1174,6 +1177,8 @@ class CompoundStructure(
|
||||
|
||||
@property
|
||||
def as_svg(self, width: int = 800, height: int = 400):
|
||||
if self.molfile is not None and self.molfile.strip() != "":
|
||||
return IndigoUtils.mol_to_svg(self.molfile, width=width, height=height)
|
||||
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
||||
|
||||
@property
|
||||
@ -1610,10 +1615,15 @@ class Reaction(
|
||||
products = models.ManyToManyField(
|
||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||
)
|
||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
||||
rules = models.ManyToManyField(
|
||||
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
||||
)
|
||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||
medline_references = ArrayField(
|
||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
||||
models.TextField(blank=False, null=False),
|
||||
null=True,
|
||||
verbose_name="Medline References",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||
@ -1630,7 +1640,7 @@ class Reaction(
|
||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||
products: Union[List[str], List[CompoundStructure]] = None,
|
||||
rules: Union[Rule | List[Rule]] = None,
|
||||
multi_step: bool = True,
|
||||
multi_step: bool = False,
|
||||
):
|
||||
_educts = []
|
||||
_products = []
|
||||
@ -1695,7 +1705,7 @@ class Reaction(
|
||||
if name is not None and name.strip() != "":
|
||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if description is not None and name.strip() != "":
|
||||
if description is not None and description.strip() != "":
|
||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
r.multi_step = multi_step
|
||||
@ -1773,7 +1783,7 @@ class Reaction(
|
||||
return new_reaction
|
||||
|
||||
def smirks(self):
|
||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
||||
return f"{'.'.join([cs.smiles for cs in self.educts.all().order_by('-pk')])}>>{'.'.join([cs.smiles for cs in self.products.all().order_by('-pk')])}"
|
||||
|
||||
@property
|
||||
def as_svg(self):
|
||||
@ -1886,6 +1896,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
if n not in queue:
|
||||
queue.append(n)
|
||||
|
||||
for i in queue:
|
||||
processed.add(i)
|
||||
|
||||
while len(queue):
|
||||
current = queue.pop()
|
||||
processed.add(current)
|
||||
@ -2163,7 +2176,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
smiles: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
depth: Optional[int] = 0,
|
||||
depth: Optional[int] = -1,
|
||||
):
|
||||
return Node.create(self, smiles, depth, name=name, description=description)
|
||||
|
||||
@ -2178,6 +2191,68 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
):
|
||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||
|
||||
def update_depths(self):
|
||||
# Collect number of in and out links per node
|
||||
in_count = defaultdict(lambda: 0)
|
||||
out_count = defaultdict(lambda: 0)
|
||||
|
||||
for e in self.edges:
|
||||
for react in e.start_nodes.all():
|
||||
out_count[str(react.uuid)] += 1
|
||||
|
||||
for prod in e.end_nodes.all():
|
||||
in_count[str(prod.uuid)] += 1
|
||||
|
||||
depth_map = {}
|
||||
depth_map[0] = list()
|
||||
processed = set()
|
||||
|
||||
data_driven_root_nodes = (
|
||||
self.node_set.all()
|
||||
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts"))
|
||||
.filter(prod_cnt=0, educt_cnt__gt=0)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# Eval QuerySet
|
||||
root_nodes_by_depth = list(self.root_nodes)
|
||||
|
||||
data_driven_root_nodes.update(depth=0)
|
||||
root_nodes = [n for n in data_driven_root_nodes]
|
||||
|
||||
for n in root_nodes_by_depth:
|
||||
if n not in root_nodes:
|
||||
if len(n.edge_products.all()) == 0:
|
||||
root_nodes.append(n)
|
||||
|
||||
for n in root_nodes:
|
||||
depth_map[0].append(n)
|
||||
|
||||
# At most depth len(nodes) is possible
|
||||
for i in range(self.nodes.count()):
|
||||
level_nodes = depth_map.get(i, [])
|
||||
|
||||
if len(level_nodes) == 0:
|
||||
break
|
||||
|
||||
unique_next_level = set()
|
||||
for n in level_nodes:
|
||||
processed.add(n)
|
||||
for e in self.edges:
|
||||
if n in e.start_nodes.all():
|
||||
for p in e.end_nodes.all():
|
||||
if p not in processed:
|
||||
unique_next_level.add(p)
|
||||
|
||||
if len(unique_next_level) > 0:
|
||||
depth_map[i + 1] = list(unique_next_level)
|
||||
|
||||
for depth, nodes in depth_map.items():
|
||||
for n in nodes:
|
||||
if n.depth != depth and depth != 0:
|
||||
n.depth = depth
|
||||
n.save()
|
||||
|
||||
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
@ -2230,7 +2305,12 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
"node_label_id": self.default_node_label.url,
|
||||
"image": f"{self.url}?image=svg",
|
||||
"image_svg": IndigoUtils.mol_to_svg(
|
||||
self.default_node_label.smiles, width=40, height=40
|
||||
self.default_node_label.molfile
|
||||
if self.default_node_label.molfile is not None
|
||||
and self.default_node_label.molfile.strip()
|
||||
else self.default_node_label.smiles,
|
||||
width=40,
|
||||
height=40,
|
||||
),
|
||||
"image_type": "svg",
|
||||
"name": self.get_name(),
|
||||
@ -2284,12 +2364,17 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
@property
|
||||
def as_svg(self):
|
||||
if (
|
||||
self.default_node_label.molfile is not None
|
||||
and self.default_node_label.molfile.strip() != ""
|
||||
):
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||
|
||||
def get_timeseries_data(self):
|
||||
for ai in self.additional_information.all():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
if ai.type == "OECD301FTimeSeries":
|
||||
return ai.get().model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
@ -2326,7 +2411,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
edge_label = models.ForeignKey(
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.CASCADE
|
||||
)
|
||||
start_nodes = models.ManyToManyField(
|
||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||
@ -2405,6 +2490,8 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
rule: Optional[Rule] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
e = Edge()
|
||||
e.pathway = pathway
|
||||
@ -2434,7 +2521,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||
rules=rule,
|
||||
multi_step=False,
|
||||
multi_step=kwargs.get("multi_step", False),
|
||||
)
|
||||
|
||||
e.edge_label = r
|
||||
@ -4442,18 +4529,22 @@ class AdditionalInformation(models.Model):
|
||||
|
||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
@staticmethod
|
||||
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
||||
from envipy_additional_information import registry
|
||||
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[self.type](**self.data)
|
||||
inst = MAPPING[ai_type](**ai_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading {self.type}: {e}")
|
||||
print(f"Error loading {ai_type}: {e}")
|
||||
raise e
|
||||
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
return inst
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
inst = AdditionalInformation.from_dict(self.type, self.data)
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
return inst
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@ -19,6 +19,7 @@ from sentry_sdk import capture_exception
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .exceptions import InvalidSMILESException
|
||||
|
||||
from .logic import (
|
||||
EPDBURLParser,
|
||||
@ -754,6 +755,11 @@ def models(request):
|
||||
{"Model": s.SERVER_URL + "/model"},
|
||||
]
|
||||
|
||||
context["entity_type"] = "model"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "models"
|
||||
|
||||
# Keep model_types for potential modal/action use
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": {
|
||||
@ -766,19 +772,26 @@ def models(request):
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = (
|
||||
{
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
)
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v.display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": v.requires_rule_packages(),
|
||||
"requires_data_packages": v.requires_data_packages(),
|
||||
"additional_parameters": v.Config.__name__.lower()
|
||||
if v.Config.__name__ != ""
|
||||
else None,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v.display()] = {
|
||||
@ -787,12 +800,6 @@ def models(request):
|
||||
"requires_data_packages": v.requires_data_packages(),
|
||||
}
|
||||
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "model"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "models"
|
||||
|
||||
return render(request, "collections/models_paginated.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
@ -906,12 +913,13 @@ def package_models(request, package_uuid):
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
}
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
@ -1087,6 +1095,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)
|
||||
|
||||
elif half_life:
|
||||
@ -1190,9 +1203,7 @@ def package(request, package_uuid):
|
||||
if request.method == "GET":
|
||||
if request.GET.get("export", False) == "true":
|
||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||
pack_json = PackageManager.export_package(
|
||||
current_package, include_models=False, include_external_identifiers=False
|
||||
)
|
||||
pack_json = PackageManager.export_package(current_package)
|
||||
response = JsonResponse(pack_json, content_type="application/json")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
|
||||
@ -2328,7 +2339,13 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
||||
node_description = request.POST.get("node-description")
|
||||
|
||||
node_smiles = request.POST.get("node-smiles").strip()
|
||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||
|
||||
try:
|
||||
current_pathway.add_node(node_smiles, 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)
|
||||
|
||||
@ -2506,6 +2523,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
||||
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)
|
||||
|
||||
else:
|
||||
@ -3077,12 +3097,21 @@ def jobs(request):
|
||||
{"Home": s.SERVER_URL},
|
||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||
]
|
||||
if current_user.is_superuser:
|
||||
context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||
else:
|
||||
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||
# if current_user.is_superuser:
|
||||
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||
# else:
|
||||
# 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":
|
||||
job_name = request.POST.get("job-name")
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -65,6 +65,25 @@ def run_both_engines(SMILES, SMIRKS):
|
||||
|
||||
|
||||
def migration(request):
|
||||
accepted_diffs = [
|
||||
"bt0055-3469.1",
|
||||
"bt0056-2685",
|
||||
"bt0193-4263",
|
||||
"bt0231-1871.1",
|
||||
"bt0242-3803",
|
||||
"bt0254-4224.1",
|
||||
"bt0254-4224.2",
|
||||
"bt0291-1129",
|
||||
"bt0337-3543",
|
||||
"bt0391-4285",
|
||||
"bt0402-3576",
|
||||
"bt0404-3928",
|
||||
"bt0416-4269",
|
||||
"bt0432-4254",
|
||||
"bt0337-4117",
|
||||
"bt0322-3393",
|
||||
]
|
||||
|
||||
if request.method == "GET":
|
||||
context = get_base_context(request)
|
||||
|
||||
@ -109,11 +128,11 @@ def migration(request):
|
||||
),
|
||||
"id": str(r.uuid),
|
||||
"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
|
||||
else:
|
||||
error += 1
|
||||
@ -135,7 +154,16 @@ def migration(request):
|
||||
|
||||
for r in migration_status["results"]:
|
||||
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
||||
if r["name"] in accepted_diffs:
|
||||
r["status"] = True
|
||||
|
||||
migration_status["results"] = sorted(
|
||||
migration_status["results"], key=lambda x: (x["status"], x["name"])
|
||||
)
|
||||
|
||||
num_success = sum([int(x["status"]) for x in migration_status["results"]])
|
||||
migration_status["success"] = num_success
|
||||
migration_status["error"] = migration_status["total"] - num_success
|
||||
context.update(**migration_status)
|
||||
|
||||
return render(request, "migration.html", context)
|
||||
|
||||
@ -21,5 +21,11 @@
|
||||
"django",
|
||||
"tailwindcss",
|
||||
"daisyui"
|
||||
]
|
||||
],
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@tailwindcss/oxide"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
|
||||
|
||||
import matplotlib.patches as mpatches
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.figure import Figure
|
||||
from scipy import stats
|
||||
|
||||
"""
|
||||
@ -101,7 +101,8 @@ class PepperPrediction(PropertyPrediction):
|
||||
mask_red = x > vp
|
||||
|
||||
# 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")
|
||||
|
||||
if np.any(mask_green):
|
||||
@ -146,13 +147,12 @@ class PepperPrediction(PropertyPrediction):
|
||||
]
|
||||
ax.legend(handles=patches, frameon=True)
|
||||
|
||||
plt.tight_layout()
|
||||
fig.tight_layout()
|
||||
|
||||
# --- Export to SVG string ---
|
||||
buf = io.StringIO()
|
||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||
svg = buf.getvalue()
|
||||
plt.close(fig)
|
||||
buf.close()
|
||||
|
||||
return svg
|
||||
|
||||
@ -187,8 +187,9 @@ class Pepper:
|
||||
groups = [group for group in dataset.group_by("structure_id")]
|
||||
|
||||
# Unless explicitly set compute everything serial
|
||||
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
||||
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
||||
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
|
||||
if n_threads > 1:
|
||||
results = Parallel(n_jobs=n_threads)(
|
||||
delayed(compute_bayes_per_group)(group[1])
|
||||
for group in dataset.group_by("structure_id")
|
||||
)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
|
||||
@ -34,3 +34,8 @@
|
||||
}
|
||||
|
||||
@import "./daisyui-theme.css";
|
||||
|
||||
select.select[multiple] {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@ -59,6 +59,9 @@ document.addEventListener("alpine:init", () => {
|
||||
get isEditMode() {
|
||||
return this.mode === "edit";
|
||||
},
|
||||
get isRequired() {
|
||||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
Alpine.data(
|
||||
"compoundWidget",
|
||||
|
||||
148
static/js/pw.js
148
static/js/pw.js
@ -1,5 +1,37 @@
|
||||
console.log("loaded pw.js")
|
||||
|
||||
function findPaths(source_idx, links) {
|
||||
const resultLinks = new Set();
|
||||
const visited = new Set();
|
||||
// Helper function for depth-first search
|
||||
function dfs(current) {
|
||||
|
||||
if (visited.has(current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (links.filter(link => link.target.id === current).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(current);
|
||||
links.forEach(link => {
|
||||
if (
|
||||
link.target.id === current &&
|
||||
!visited.has(link.source.id) &&
|
||||
link.source.id !== link.target.id // Avoid self-loops
|
||||
) {
|
||||
resultLinks.add(link); // Add link to result
|
||||
dfs(link.source.id);
|
||||
}
|
||||
});
|
||||
visited.delete(current);
|
||||
}
|
||||
// Start DFS
|
||||
dfs(source_idx);
|
||||
return Array.from(resultLinks);
|
||||
}
|
||||
|
||||
function predictFromNode(url) {
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
@ -23,6 +55,7 @@ function predictFromNode(url) {
|
||||
// elem = 'vizdiv'
|
||||
function draw(pathway, elem) {
|
||||
|
||||
const initialzoom = 2.5
|
||||
const nodeRadius = 20;
|
||||
const linkDistance = 100;
|
||||
const chargeStrength = -200;
|
||||
@ -63,7 +96,7 @@ function draw(pathway, elem) {
|
||||
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
||||
}
|
||||
|
||||
node.fy = node.depth * levelSpacing + 50;
|
||||
node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
|
||||
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||
});
|
||||
}
|
||||
@ -101,10 +134,24 @@ function draw(pathway, elem) {
|
||||
// Update pseudo node positions first
|
||||
updatePseudoNodePositions();
|
||||
|
||||
link.attr("x1", d => d.source.x)
|
||||
.attr("y1", d => d.source.y)
|
||||
.attr("x2", d => d.target.x)
|
||||
.attr("y2", d => d.target.y);
|
||||
link.attr("d", d => {
|
||||
// Check if it's a self-loop (source equals target)
|
||||
if (d.source.id === d.target.id) {
|
||||
// Create a bezier curve for self-loops
|
||||
const x = d.source.x;
|
||||
const y = d.source.y;
|
||||
const loopRadius = nodeRadius * 2; // Adjust size of the loop
|
||||
|
||||
// Create a circular path to the left of the node
|
||||
return `M ${x},${y - nodeRadius}
|
||||
C ${x - loopRadius},${y - nodeRadius - loopRadius}
|
||||
${x - loopRadius},${y + nodeRadius + loopRadius}
|
||||
${x},${y + nodeRadius}`;
|
||||
} else {
|
||||
// Regular straight line for normal edges
|
||||
return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
|
||||
}
|
||||
});
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
}
|
||||
@ -463,7 +510,7 @@ function draw(pathway, elem) {
|
||||
// TODO needs to be generic once we store it as AddInf
|
||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||
if (s["mean"] != null) {
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -563,6 +610,8 @@ function draw(pathway, elem) {
|
||||
// Apply zoom to the SVG element - this enables wheel zoom
|
||||
svg.call(zoom);
|
||||
|
||||
svg.call(zoom.scaleBy, initialzoom);
|
||||
|
||||
// Also apply zoom to container to catch events that might not reach SVG
|
||||
// This ensures drag-to-pan works even when clicking on empty space
|
||||
container.call(zoom);
|
||||
@ -581,11 +630,11 @@ function draw(pathway, elem) {
|
||||
for (idx in parents) {
|
||||
p = nodes[parents[idx]]
|
||||
// console.log(p.depth)
|
||||
if (p.depth >= n.depth) {
|
||||
// keep the .5 steps for pseudo nodes
|
||||
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||
}
|
||||
// if (p.depth >= n.depth) {
|
||||
// // keep the .5 steps for pseudo nodes
|
||||
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
@ -599,13 +648,45 @@ function draw(pathway, elem) {
|
||||
|
||||
// Kanten zeichnen
|
||||
const link = zoomable.append("g")
|
||||
.selectAll("line")
|
||||
.selectAll("path")
|
||||
.data(links)
|
||||
.enter().append("line")
|
||||
.enter().append("path")
|
||||
// Check if target is pseudo and draw marker only if not pseudo
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
|
||||
.attr("marker-end", d => {
|
||||
if (d.target.pseudo) return '';
|
||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||
})
|
||||
.attr("fill", "none")
|
||||
.on("click", function(event, d) {
|
||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
if (!wasHighlighted) {
|
||||
const toHighlight = [];
|
||||
toHighlight.push(d.el);
|
||||
|
||||
if (d.source.pseudo || d.target.pseudo) {
|
||||
if (d.target.pseudo) {
|
||||
d3.selectAll("path").each(e => {
|
||||
if (e !== undefined && e.source.id === d.target.id) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3.selectAll("path").each(e => {
|
||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of toHighlight) {
|
||||
d3.select(e).classed("highlighted", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add element to links array
|
||||
link.each(function (d) {
|
||||
@ -622,10 +703,41 @@ function draw(pathway, elem) {
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended))
|
||||
.on("end", dragended)
|
||||
)
|
||||
.on("click", function (event, d) {
|
||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||
d3.selectAll("path").classed("inedge", false);
|
||||
d3.selectAll("path").classed("outedge", false);
|
||||
|
||||
if (!wasHighlighted) {
|
||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||
})
|
||||
const inEdges = findPaths(d.id, links);
|
||||
const outEdges = []
|
||||
// Colorize out edges green
|
||||
for (const l of links) {
|
||||
if (l.source.id === d.id) {
|
||||
outEdges.push(l);
|
||||
if (l.target.pseudo) {
|
||||
for (const l2 of links) {
|
||||
if (l.target.id === l2.source.id) {
|
||||
outEdges.push(l2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of inEdges) {
|
||||
d3.select(e.el).classed("inedge", true);
|
||||
}
|
||||
|
||||
for (const e of outEdges) {
|
||||
d3.select(e.el).classed("outedge", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Kreise für die Knoten hinzufügen
|
||||
node.append("circle")
|
||||
@ -699,14 +811,14 @@ function draw(pathway, elem) {
|
||||
|
||||
function serializeSVG(svgElement) {
|
||||
|
||||
svgElement.querySelectorAll("line.link").forEach(line => {
|
||||
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||
const style = getComputedStyle(line);
|
||||
line.setAttribute("stroke", style.stroke);
|
||||
line.setAttribute("stroke-width", style.strokeWidth);
|
||||
line.setAttribute("fill", style.fill);
|
||||
});
|
||||
|
||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
||||
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
|
||||
const style = getComputedStyle(line);
|
||||
line.setAttribute("stroke", style.stroke);
|
||||
line.setAttribute("stroke-width", style.strokeWidth);
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
{% for tpl in action_button_templates %}
|
||||
{% include tpl %}
|
||||
{% endfor %}
|
||||
<li role="separator" class="divider"></li>
|
||||
<li role="separator" class="divider h-px"></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a
|
||||
@ -74,7 +74,7 @@
|
||||
Rules</a
|
||||
>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li role="separator" class="divider h-px"></li>
|
||||
<li>
|
||||
<a
|
||||
class="button"
|
||||
@ -99,11 +99,16 @@
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
||||
>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li role="separator" class="divider h-px"></li>
|
||||
<li>
|
||||
<a
|
||||
class="button"
|
||||
onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
|
||||
onclick="
|
||||
const modal = document.getElementById('delete_pathway_node_modal');
|
||||
modal.showModal();
|
||||
window.dispatchEvent(new Event('modal-opened'));
|
||||
return false;
|
||||
"
|
||||
>
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
||||
>
|
||||
@ -111,7 +116,12 @@
|
||||
<li>
|
||||
<a
|
||||
class="button"
|
||||
onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
|
||||
onclick="
|
||||
const modal = document.getElementById('delete_pathway_edge_modal');
|
||||
modal.showModal();
|
||||
window.dispatchEvent(new Event('modal-opened'));
|
||||
return false;
|
||||
"
|
||||
>
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
||||
>
|
||||
|
||||
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
@ -0,0 +1,102 @@
|
||||
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
|
||||
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
||||
{% load envipytags %}
|
||||
{# Loading state #}
|
||||
<div
|
||||
x-show="isLoading"
|
||||
class="mx-auto flex h-32 w-32 items-center justify-center"
|
||||
>
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
|
||||
{# Error state #}
|
||||
<div
|
||||
x-show="!isLoading && error"
|
||||
class="alert alert-error/50 text-sm"
|
||||
x-text="error"
|
||||
></div>
|
||||
|
||||
{# Content #}
|
||||
<template x-if="!isLoading && !error">
|
||||
<div>
|
||||
{# Empty state #}
|
||||
<div
|
||||
x-show="totalItems === 0"
|
||||
class="text-base-content/70 py-8 text-center"
|
||||
>
|
||||
<p>No {{ empty_text|default:"items" }} found.</p>
|
||||
</div>
|
||||
|
||||
{# Items list #}
|
||||
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Queued At</th>
|
||||
<th>Done At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="obj in paginatedItems" :key="obj.url">
|
||||
<tr>
|
||||
<td>
|
||||
<a :href="obj.user.url"><span x-text="obj.user.name"></span></a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a :href="obj.url"><span x-text="obj.id"></span></a>
|
||||
</td>
|
||||
<td><span x-text="obj.name"></span></td>
|
||||
<td><span x-text="obj.status"></span></td>
|
||||
<td><span x-text="obj.created"></span></td>
|
||||
<td><span x-text="obj.done"></span></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</ul>
|
||||
|
||||
{# Pagination controls #}
|
||||
<div
|
||||
x-show="totalPages > 1"
|
||||
class="mt-4 flex items-center justify-between px-2"
|
||||
>
|
||||
<span class="text-base-content/70 text-sm">
|
||||
Showing <span x-text="showingStart"></span>-<span
|
||||
x-text="showingEnd"
|
||||
></span>
|
||||
of <span x-text="totalItems"></span>
|
||||
</span>
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === 1"
|
||||
@click="prevPage()"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<template x-for="item in pageNumbers" :key="item.key">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:class="{ 'btn-active': item.page === currentPage }"
|
||||
:disabled="item.isEllipsis"
|
||||
@click="!item.isEllipsis && goToPage(item.page)"
|
||||
x-text="item.page"
|
||||
></button>
|
||||
</template>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="nextPage()"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
{% block action_button %}
|
||||
<div class="flex items-center gap-2">
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
13
templates/collections/joblog_paginated.html
Normal file
13
templates/collections/joblog_paginated.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "collections/paginated_base.html" %}
|
||||
|
||||
{% block page_title %}Jobs{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block description %}
|
||||
<p>List of Jobs submitted.</p>
|
||||
{% endblock description %}
|
||||
@ -3,14 +3,16 @@
|
||||
{% block page_title %}Models{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick="document.getElementById('new_model_modal').showModal(); return false;"
|
||||
>
|
||||
New Model
|
||||
</button>
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
{% if meta.enabled_features.MODEL_BUILDING %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick="document.getElementById('new_model_modal').showModal(); return false;"
|
||||
>
|
||||
New Model
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock action_button %}
|
||||
|
||||
|
||||
@ -3,75 +3,73 @@
|
||||
{% block page_title %}Packages{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
id="new-package-button"
|
||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
id="new-package-button"
|
||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
||||
>
|
||||
<path d="M12 10v6" />
|
||||
<path d="M9 13h6" />
|
||||
<path
|
||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm">
|
||||
Import
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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-folder-plus-icon lucide-folder-plus"
|
||||
class="lucide lucide-chevron-down ml-1"
|
||||
>
|
||||
<path d="M12 10v6" />
|
||||
<path d="M9 13h6" />
|
||||
<path
|
||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||
/>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm">
|
||||
Import
|
||||
<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-chevron-down ml-1"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from JSON
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from legacy JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from JSON
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from legacy JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
|
||||
@ -37,7 +37,11 @@
|
||||
perPage: {{ per_page|default:50 }}
|
||||
})"
|
||||
>
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% if entity_type == 'joblog' %}
|
||||
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% else %}
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{% block page_title %}Pathways{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{% block page_title %}Reactions{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{% block page_title %}Rules{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -123,6 +123,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DOI link widget -->
|
||||
<template
|
||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'doi-link'"
|
||||
>
|
||||
<div
|
||||
x-data="doiWidget(fieldName, data, schema, uiSchema, mode, debugErrors, context)"
|
||||
>
|
||||
{% include "components/widgets/doi_link_widget.html" %}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Compound link widget -->
|
||||
<template
|
||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
||||
|
||||
69
templates/components/widgets/doi_link_widget.html
Normal file
69
templates/components/widgets/doi_link_widget.html
Normal file
@ -0,0 +1,69 @@
|
||||
{# DOI link widget - pure HTML template #}
|
||||
<div class="form-control">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
|
||||
<!-- Label -->
|
||||
<label class="label sm:w-48 sm:shrink-0">
|
||||
<span
|
||||
class="label-text"
|
||||
:class="{
|
||||
'text-error': $store.validationErrors.hasError(fieldName, context),
|
||||
'text-sm text-base-content/60': isViewMode
|
||||
}"
|
||||
x-text="label"
|
||||
></span>
|
||||
</label>
|
||||
|
||||
<!-- Input column -->
|
||||
<div class="flex-1">
|
||||
<!-- Help text -->
|
||||
<template x-if="helpText">
|
||||
<div class="label">
|
||||
<span
|
||||
class="label-text-alt text-base-content/60"
|
||||
x-text="helpText"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- View mode: display as link -->
|
||||
<template x-if="isViewMode">
|
||||
<div class="mt-1">
|
||||
<template x-if="value && doiUrl">
|
||||
<a
|
||||
:href="doiUrl"
|
||||
class="link link-primary"
|
||||
target="_blank"
|
||||
x-text="value"
|
||||
></a>
|
||||
</template>
|
||||
<template x-if="!value">
|
||||
<span class="text-base-content/50">—</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<template x-if="isEditMode">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
:class="{ 'input-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||
placeholder="DOI e.g. 10.1016/j.jhazmat.2016.08.036"
|
||||
x-model="value"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Errors -->
|
||||
<template x-if="$store.validationErrors.hasError(fieldName, context)">
|
||||
<div class="label">
|
||||
<template
|
||||
x-for="errMsg in $store.validationErrors.getErrors(fieldName, context)"
|
||||
:key="errMsg"
|
||||
>
|
||||
<span class="label-text-alt text-error" x-text="errMsg"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,6 +44,7 @@
|
||||
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||
x-model="value"
|
||||
:multiple="multiple"
|
||||
:required="isRequired"
|
||||
>
|
||||
<option value="" :selected="!value">Select...</option>
|
||||
|
||||
|
||||
@ -65,11 +65,11 @@
|
||||
},
|
||||
|
||||
get showMlrr() {
|
||||
return this.selectedType === 'mlrr';
|
||||
return this.selectedType === 'ml-relative-reasoning';
|
||||
},
|
||||
|
||||
get showRbrr() {
|
||||
return this.selectedType === 'rbrr';
|
||||
return this.selectedType === 'rule-based-relative-reasoning';
|
||||
},
|
||||
|
||||
get showEnviformer() {
|
||||
|
||||
@ -203,11 +203,11 @@
|
||||
id="model-based-prediction-setting-threshold"
|
||||
name="model-based-prediction-setting-threshold"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="0.25"
|
||||
value="0.25"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -4,6 +4,22 @@
|
||||
id="delete_pathway_edge_modal"
|
||||
class="modal"
|
||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||
@modal-opened.window="
|
||||
const links = d3.selectAll('path.highlighted');
|
||||
if (!links.empty()) {
|
||||
const el = links.node();
|
||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||
console.log(el);
|
||||
console.log(el.__data__);
|
||||
for (let option of selectElement.options) {
|
||||
if (option.value === el.__data__.url) {
|
||||
option.selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
}
|
||||
"
|
||||
@close="reset()"
|
||||
>
|
||||
<div class="modal-box">
|
||||
|
||||
@ -4,6 +4,19 @@
|
||||
id="delete_pathway_node_modal"
|
||||
class="modal"
|
||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||
@modal-opened.window="
|
||||
const el = d3.select('circle.highlighted').node();
|
||||
if (el !== null) {
|
||||
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
||||
for (let option of selectElement.options) {
|
||||
if (option.value === el.__data__.url) {
|
||||
option.selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
}
|
||||
"
|
||||
@close="reset()"
|
||||
>
|
||||
<div class="modal-box">
|
||||
|
||||
@ -92,7 +92,7 @@
|
||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||
{% for educt in edge.start_nodes.all %}
|
||||
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
||||
>{{ educt.name }}</a
|
||||
>{{ educt.get_name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<svg
|
||||
@ -112,7 +112,7 @@
|
||||
</svg>
|
||||
{% for product in edge.end_nodes.all %}
|
||||
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
||||
>{{ product.name }}</a
|
||||
>{{ product.get_name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@ -56,7 +56,9 @@
|
||||
<ul class="menu bg-base-200 rounded-box">
|
||||
{% for um in group.user_member.all %}
|
||||
<li>
|
||||
<a href="{{ um.url }}" class="hover:bg-base-300"
|
||||
<a
|
||||
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
||||
class="hover:bg-base-300"
|
||||
>{{ um.username }}
|
||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||
>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{% extends "framework_modern.html" %}
|
||||
{% load envipytags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -54,6 +55,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||
|
||||
{% for tpl in viz_templates %}
|
||||
{% include tpl %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Image Representation -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
|
||||
@ -72,6 +72,15 @@
|
||||
stroke: red;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.inedge {
|
||||
stroke: red;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.outedge {
|
||||
stroke: green;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
</style>
|
||||
<script src="{% static 'js/pw.js' %}"></script>
|
||||
|
||||
@ -106,7 +115,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Graphical Representation -->
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<div class="collapse-arrow bg-base-200 collapse overflow-y-auto">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Graphical Representation
|
||||
@ -140,7 +149,7 @@
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-96 p-2"
|
||||
>
|
||||
{% include "actions/objects/pathway.html" %}
|
||||
</ul>
|
||||
@ -367,6 +376,18 @@
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||
</marker>
|
||||
<marker
|
||||
id="curve-arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="10"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||
</marker>
|
||||
<marker
|
||||
id="doublearrow"
|
||||
viewBox="0 0 20 30"
|
||||
|
||||
@ -105,7 +105,7 @@
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<label class="select mb-8 w-full">
|
||||
<label class="select mb-8 w-full" id="prediction-setting-label">
|
||||
<span class="label">Predictor</span>
|
||||
<select id="prediction-setting" name="prediction-setting">
|
||||
<option disabled>Select a Setting</option>
|
||||
@ -148,6 +148,21 @@
|
||||
</div>
|
||||
{# prettier-ignore-start #}
|
||||
<script>
|
||||
// Hide predictor selection and update button text if mode is "build"
|
||||
function radioChange(event) {
|
||||
if (event.target.value === "build") {
|
||||
document.getElementById("prediction-setting-label").hidden = true;
|
||||
document.getElementById("predict-submit-button").innerText = "Build";
|
||||
} else {
|
||||
document.getElementById("prediction-setting-label").hidden = false;
|
||||
document.getElementById("predict-submit-button").innerText = "Predict";
|
||||
}
|
||||
}
|
||||
const radioButtons = document.querySelectorAll('input[name="predict"]');
|
||||
radioButtons.forEach(radio => {
|
||||
radio.addEventListener('change', radioChange);
|
||||
});
|
||||
|
||||
// Helper function to safely get Ketcher instance from iframe
|
||||
function getKetcherInstance(iframeId) {
|
||||
const ketcherFrame = document.getElementById(iframeId);
|
||||
@ -197,7 +212,13 @@
|
||||
e.preventDefault();
|
||||
const button = this;
|
||||
button.disabled = true;
|
||||
button.textContent = "Predicting...";
|
||||
|
||||
// Set text depending on mode
|
||||
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
||||
button.textContent = "Building...";
|
||||
} else {
|
||||
button.textContent = "Predicting...";
|
||||
}
|
||||
|
||||
// Get SMILES from either input or Ketcher
|
||||
const smilesInput = document.getElementById("predict-smiles");
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from django.conf import settings as s
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, CompoundStructure
|
||||
|
||||
@ -33,13 +35,13 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(c.description, "No Desc")
|
||||
|
||||
def test_missing_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||
|
||||
def test_smiles_are_trimmed(self):
|
||||
@ -96,7 +98,7 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(len(self.package.compounds), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(
|
||||
self.package,
|
||||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django.conf import settings as s
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, Reaction, Rule
|
||||
|
||||
@ -163,7 +164,7 @@ class ReactionTest(TestCase):
|
||||
self.assertEqual(len(self.package.reactions), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Reaction.create(
|
||||
package=self.package,
|
||||
name="Eawag BBD reaction r0001",
|
||||
|
||||
@ -46,6 +46,14 @@ class ModelViewTest(TestCase):
|
||||
)
|
||||
|
||||
expected = [
|
||||
{
|
||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||
"probability": 0.75,
|
||||
"btrule": {
|
||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||
"name": "bt0001-3568",
|
||||
},
|
||||
},
|
||||
{
|
||||
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
||||
"probability": 0.25,
|
||||
@ -62,14 +70,6 @@ class ModelViewTest(TestCase):
|
||||
"name": "bt0243-4301",
|
||||
},
|
||||
},
|
||||
{
|
||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||
"probability": 0.75,
|
||||
"btrule": {
|
||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||
"name": "bt0001-3568",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
actual = response.json()["pred"]
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from envipy_additional_information import HalfLife, HalfLifeWS
|
||||
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel
|
||||
from envipy_additional_information.information import Interval
|
||||
from envipy_additional_information.parsers import (
|
||||
AcidityParser,
|
||||
@ -125,6 +125,7 @@ from envipy_additional_information.parsers import (
|
||||
LiquidMatrixSourceParser,
|
||||
OxygenUptakeRateParser,
|
||||
InitiatingOrganismParser,
|
||||
PFASConfidenceParser,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -141,7 +142,7 @@ def get_parameter(request, paramname):
|
||||
res = request.POST.get(paramname)
|
||||
if res is not None and res.strip() != "":
|
||||
return res
|
||||
return ValueError("Not all parameters are set!")
|
||||
raise ValueError("Not all parameters are set!")
|
||||
|
||||
|
||||
def get_parameter_or_empty_string(request, paramname):
|
||||
@ -473,17 +474,12 @@ def build_additional_information_from_request(request, type_):
|
||||
|
||||
comment = get_parameter_or_empty_string(request, "comment")
|
||||
source = get_parameter_or_empty_string(request, "source")
|
||||
first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||
# first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||
model = get_parameter_or_empty_string(request, "model")
|
||||
fit = get_parameter_or_empty_string(request, "fit")
|
||||
|
||||
if first_order != "":
|
||||
if model != "":
|
||||
raise ValueError("not both, model and firstOrder can be set!")
|
||||
if first_order == "true":
|
||||
model = "SFO"
|
||||
else:
|
||||
logger.info("firstOrder is set to false which is not meaningful")
|
||||
if model:
|
||||
model = HalfLifeModel(model.upper())
|
||||
|
||||
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
||||
|
||||
@ -508,6 +504,10 @@ def build_additional_information_from_request(request, type_):
|
||||
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
||||
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
||||
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
||||
|
||||
if model_ws:
|
||||
model_ws = HalfLifeModel(model_ws.upper())
|
||||
|
||||
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
||||
|
||||
dt50_total = IntervalParser.from_string(hl_ws_total)
|
||||
@ -674,7 +674,8 @@ def build_additional_information_from_request(request, type_):
|
||||
elif type_ == "studywst":
|
||||
# study_wst_cond = get_parameter(request, "studywstcond")
|
||||
raise ValueError("studywstcond is not yet implemented")
|
||||
|
||||
elif type_ == "pfasconfidence":
|
||||
return PFASConfidenceParser.from_string(get_parameter(request, "level"))
|
||||
else:
|
||||
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")
|
||||
|
||||
|
||||
@ -6,12 +6,15 @@ import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, TYPE_CHECKING
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from django.conf import settings as s
|
||||
from django.db import transaction
|
||||
from ninja import Schema
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from epdb.models import (
|
||||
AdditionalInformation,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
Edge,
|
||||
@ -28,7 +31,6 @@ from epdb.models import (
|
||||
Rule,
|
||||
RuleBasedRelativeReasoning,
|
||||
Scenario,
|
||||
SequentialRule,
|
||||
Setting,
|
||||
SimpleAmbitRule,
|
||||
SimpleRDKitRule,
|
||||
@ -43,408 +45,235 @@ if TYPE_CHECKING:
|
||||
from epdb.logic import SPathway
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
def __init__(
|
||||
self,
|
||||
package: Package,
|
||||
include_models: bool = False,
|
||||
include_external_identifiers: bool = True,
|
||||
):
|
||||
self._raw_package = package
|
||||
self.include_modes = include_models
|
||||
self.include_external_identifiers = include_external_identifiers
|
||||
class LicenseExportSchema(Schema):
|
||||
cc_string: str
|
||||
link: HttpUrl
|
||||
image_link: HttpUrl
|
||||
|
||||
def do_export(self):
|
||||
return PackageExporter._export_package_as_json(
|
||||
self._raw_package, self.include_modes, self.include_external_identifiers
|
||||
)
|
||||
|
||||
##############
|
||||
# RefSchemas #
|
||||
##############
|
||||
class RefExportSchema(Schema):
|
||||
uuid: str
|
||||
url: str
|
||||
|
||||
@staticmethod
|
||||
def _export_package_as_json(
|
||||
package: Package, include_models: bool = False, include_external_identifiers: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
def resolve_uuid(obj):
|
||||
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
||||
return str(value)
|
||||
|
||||
|
||||
class RefCompoundExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefCompoundStructureExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefReactionExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefRuleExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefNodeExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefEdgeExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefPathwayExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
class RefScenarioExportSchema(RefExportSchema): ...
|
||||
|
||||
|
||||
############
|
||||
# Compound #
|
||||
############
|
||||
class CompoundExportSchema(RefCompoundExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
default_structure: RefCompoundStructureExportSchema
|
||||
structures: List["CompoundStructureExportSchema"]
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
class CompoundStructureExportSchema(RefCompoundStructureExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
smiles: str
|
||||
molfile: Optional[str]
|
||||
normalized_structure: bool
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
############
|
||||
# Reaction #
|
||||
############
|
||||
class ReactionExportSchema(RefReactionExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
educts: List[RefCompoundStructureExportSchema]
|
||||
products: List[RefCompoundStructureExportSchema]
|
||||
rules: List[RefRuleExportSchema]
|
||||
multi_step: bool
|
||||
medline_references: List[str] | None
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
#########
|
||||
# Rules #
|
||||
#########
|
||||
class RuleExportSchema(RefRuleExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
smirks: str
|
||||
reactant_filter_smarts: Optional[str]
|
||||
product_filter_smarts: Optional[str]
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
class ParallelRuleExportSchema(RefRuleExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
simple_rules: List[RefRuleExportSchema]
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
###########################
|
||||
# Pathway / Nodes / Edges #
|
||||
###########################
|
||||
class NodeExportSchema(RefNodeExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
default_node_label: RefCompoundStructureExportSchema
|
||||
node_labels: List[RefCompoundStructureExportSchema]
|
||||
depth: int
|
||||
stereo_removed: bool
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
class EdgeExportSchema(RefEdgeExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
edge_label: RefReactionExportSchema
|
||||
start_nodes: List[RefNodeExportSchema]
|
||||
end_nodes: List[RefNodeExportSchema]
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
class PathwayExportSchema(RefPathwayExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
aliases: List[str]
|
||||
predicted: bool
|
||||
nodes: List[NodeExportSchema]
|
||||
edges: List[EdgeExportSchema]
|
||||
scenarios: List[RefScenarioExportSchema]
|
||||
|
||||
|
||||
class ScenarioExportSchema(RefScenarioExportSchema):
|
||||
name: str
|
||||
description: str
|
||||
scenario_date: str
|
||||
scenario_type: str
|
||||
|
||||
|
||||
class AdditionalInformationExportSchema(RefExportSchema):
|
||||
type: str
|
||||
data: dict
|
||||
scenario: RefScenarioExportSchema | None = None
|
||||
attach_object: RefExportSchema | None = None
|
||||
|
||||
@staticmethod
|
||||
def resolve_attach_object(obj):
|
||||
if isinstance(obj, dict):
|
||||
if obj.get("attach_object") is None:
|
||||
return None
|
||||
return RefExportSchema.model_validate(obj["attach_object"])
|
||||
return obj.content_object
|
||||
|
||||
|
||||
###########
|
||||
# Package #
|
||||
###########
|
||||
class PackageExportSchema(Schema):
|
||||
name: str
|
||||
description: str
|
||||
uuid: str
|
||||
reviewed: bool
|
||||
license: LicenseExportSchema | None
|
||||
compounds: List[CompoundExportSchema]
|
||||
reactions: List[ReactionExportSchema]
|
||||
simple_rules: List[RuleExportSchema]
|
||||
composite_rules: List[ParallelRuleExportSchema]
|
||||
pathways: List[PathwayExportSchema]
|
||||
scenarios: List[ScenarioExportSchema]
|
||||
additional_information: List[AdditionalInformationExportSchema]
|
||||
|
||||
@staticmethod
|
||||
def resolve_uuid(obj):
|
||||
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def resolve_simple_rules(obj):
|
||||
if isinstance(obj, dict):
|
||||
result = []
|
||||
for r in obj.get("simple_rules", []):
|
||||
result.append(RuleExportSchema.model_validate(r))
|
||||
return result
|
||||
return SimpleAmbitRule.objects.filter(package=obj)
|
||||
|
||||
@staticmethod
|
||||
def resolve_composite_rules(obj):
|
||||
if isinstance(obj, dict):
|
||||
result = []
|
||||
for r in obj.get("composite_rules", []):
|
||||
result.append(ParallelRuleExportSchema.model_validate(r))
|
||||
return result
|
||||
return ParallelRule.objects.filter(package=obj)
|
||||
|
||||
@staticmethod
|
||||
def resolve_additional_information(obj):
|
||||
if isinstance(obj, dict):
|
||||
result = []
|
||||
for ai in obj.get("additional_information", []):
|
||||
result.append(AdditionalInformationExportSchema.model_validate(ai))
|
||||
return result
|
||||
return AdditionalInformation.objects.filter(package=obj)
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
def __init__(self, package: Package):
|
||||
self._raw_package = package
|
||||
|
||||
def do_export(self):
|
||||
return PackageExporter._export_package_as_json(self._raw_package)
|
||||
|
||||
@staticmethod
|
||||
def _export_package_as_json(package: Package) -> Dict[str, Any]:
|
||||
"""
|
||||
Dumps a Package and all its related objects as JSON.
|
||||
|
||||
Args:
|
||||
package: The Package instance to dump
|
||||
include_models: Whether to include EPModel objects
|
||||
include_external_identifiers: Whether to include external identifiers
|
||||
|
||||
Returns:
|
||||
Dict containing the complete package data as JSON-serializable structure
|
||||
"""
|
||||
|
||||
def serialize_base_object(
|
||||
obj, include_aliases: bool = True, include_scenarios: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Serialize common EnviPathModel fields"""
|
||||
base_dict = {
|
||||
"uuid": str(obj.uuid),
|
||||
"name": obj.name,
|
||||
"description": obj.description,
|
||||
"url": obj.url,
|
||||
"kv": obj.kv,
|
||||
}
|
||||
data = PackageExportSchema.from_orm(package)
|
||||
|
||||
# Add aliases if the object has them
|
||||
if include_aliases and hasattr(obj, "aliases"):
|
||||
base_dict["aliases"] = obj.aliases
|
||||
|
||||
# Add scenarios if the object has them
|
||||
if include_scenarios and hasattr(obj, "scenarios"):
|
||||
base_dict["scenarios"] = [
|
||||
{"uuid": str(s.uuid), "url": s.url} for s in obj.scenarios.all()
|
||||
]
|
||||
|
||||
return base_dict
|
||||
|
||||
def serialize_external_identifiers(obj) -> List[Dict[str, Any]]:
|
||||
"""Serialize external identifiers for an object"""
|
||||
if not include_external_identifiers or not hasattr(obj, "external_identifiers"):
|
||||
return []
|
||||
|
||||
identifiers = []
|
||||
for ext_id in obj.external_identifiers.all():
|
||||
identifier_dict = {
|
||||
"uuid": str(ext_id.uuid),
|
||||
"database": {
|
||||
"uuid": str(ext_id.database.uuid),
|
||||
"name": ext_id.database.name,
|
||||
"base_url": ext_id.database.base_url,
|
||||
},
|
||||
"identifier_value": ext_id.identifier_value,
|
||||
"url": ext_id.url,
|
||||
"is_primary": ext_id.is_primary,
|
||||
}
|
||||
identifiers.append(identifier_dict)
|
||||
return identifiers
|
||||
|
||||
# Start with the package itself
|
||||
result = serialize_base_object(package, include_aliases=True, include_scenarios=True)
|
||||
result["reviewed"] = package.reviewed
|
||||
|
||||
# # Add license information
|
||||
# if package.license:
|
||||
# result['license'] = {
|
||||
# 'uuid': str(package.license.uuid),
|
||||
# 'name': package.license.name,
|
||||
# 'link': package.license.link,
|
||||
# 'image_link': package.license.image_link
|
||||
# }
|
||||
# else:
|
||||
# result['license'] = None
|
||||
|
||||
# Initialize collections
|
||||
result.update(
|
||||
{
|
||||
"compounds": [],
|
||||
"structures": [],
|
||||
"rules": {"simple_rules": [], "parallel_rules": [], "sequential_rules": []},
|
||||
"reactions": [],
|
||||
"pathways": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"scenarios": [],
|
||||
"models": [],
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Exporting package: {package.name}")
|
||||
|
||||
# Export compounds
|
||||
print("Exporting compounds...")
|
||||
for compound in package.compounds.prefetch_related("default_structure").order_by("url"):
|
||||
compound_dict = serialize_base_object(
|
||||
compound, include_aliases=True, include_scenarios=True
|
||||
)
|
||||
|
||||
if compound.default_structure:
|
||||
compound_dict["default_structure"] = {
|
||||
"uuid": str(compound.default_structure.uuid),
|
||||
"url": compound.default_structure.url,
|
||||
}
|
||||
else:
|
||||
compound_dict["default_structure"] = None
|
||||
|
||||
compound_dict["external_identifiers"] = serialize_external_identifiers(compound)
|
||||
result["compounds"].append(compound_dict)
|
||||
|
||||
# Export compound structures
|
||||
print("Exporting compound structures...")
|
||||
compound_structures = (
|
||||
CompoundStructure.objects.filter(compound__package=package)
|
||||
.select_related("compound")
|
||||
.order_by("url")
|
||||
)
|
||||
|
||||
for structure in compound_structures:
|
||||
structure_dict = serialize_base_object(
|
||||
structure, include_aliases=True, include_scenarios=True
|
||||
)
|
||||
structure_dict.update(
|
||||
{
|
||||
"compound": {
|
||||
"uuid": str(structure.compound.uuid),
|
||||
"url": structure.compound.url,
|
||||
},
|
||||
"smiles": structure.smiles,
|
||||
"canonical_smiles": structure.canonical_smiles,
|
||||
"inchikey": structure.inchikey,
|
||||
"normalized_structure": structure.normalized_structure,
|
||||
"external_identifiers": serialize_external_identifiers(structure),
|
||||
}
|
||||
)
|
||||
result["structures"].append(structure_dict)
|
||||
|
||||
# Export rules
|
||||
print("Exporting rules...")
|
||||
|
||||
# Simple rules (including SimpleAmbitRule and SimpleRDKitRule)
|
||||
for rule in SimpleRule.objects.filter(package=package).order_by("url"):
|
||||
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
|
||||
|
||||
# Add specific fields for SimpleAmbitRule
|
||||
if isinstance(rule, SimpleAmbitRule):
|
||||
rule_dict.update(
|
||||
{
|
||||
"rule_type": "SimpleAmbitRule",
|
||||
"smirks": rule.smirks,
|
||||
"reactant_filter_smarts": rule.reactant_filter_smarts or "",
|
||||
"product_filter_smarts": rule.product_filter_smarts or "",
|
||||
}
|
||||
)
|
||||
elif isinstance(rule, SimpleRDKitRule):
|
||||
rule_dict.update(
|
||||
{"rule_type": "SimpleRDKitRule", "reaction_smarts": rule.reaction_smarts}
|
||||
)
|
||||
else:
|
||||
rule_dict["rule_type"] = "SimpleRule"
|
||||
|
||||
result["rules"]["simple_rules"].append(rule_dict)
|
||||
|
||||
# Parallel rules
|
||||
for rule in (
|
||||
ParallelRule.objects.filter(package=package)
|
||||
.prefetch_related("simple_rules")
|
||||
.order_by("url")
|
||||
):
|
||||
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
|
||||
rule_dict["rule_type"] = "ParallelRule"
|
||||
rule_dict["simple_rules"] = [
|
||||
{"uuid": str(sr.uuid), "url": sr.url} for sr in rule.simple_rules.all()
|
||||
]
|
||||
result["rules"]["parallel_rules"].append(rule_dict)
|
||||
|
||||
# Sequential rules
|
||||
for rule in (
|
||||
SequentialRule.objects.filter(package=package)
|
||||
.prefetch_related("simple_rules")
|
||||
.order_by("url")
|
||||
):
|
||||
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
|
||||
rule_dict["rule_type"] = "SequentialRule"
|
||||
rule_dict["simple_rules"] = [
|
||||
{
|
||||
"uuid": str(sr.uuid),
|
||||
"url": sr.url,
|
||||
"order_index": sr.sequentialruleordering_set.get(
|
||||
sequential_rule=rule
|
||||
).order_index,
|
||||
}
|
||||
for sr in rule.simple_rules.all()
|
||||
]
|
||||
result["rules"]["sequential_rules"].append(rule_dict)
|
||||
|
||||
# Export reactions
|
||||
print("Exporting reactions...")
|
||||
for reaction in package.reactions.prefetch_related("educts", "products", "rules").order_by(
|
||||
"url"
|
||||
):
|
||||
reaction_dict = serialize_base_object(
|
||||
reaction, include_aliases=True, include_scenarios=True
|
||||
)
|
||||
reaction_dict.update(
|
||||
{
|
||||
"educts": [{"uuid": str(e.uuid), "url": e.url} for e in reaction.educts.all()],
|
||||
"products": [
|
||||
{"uuid": str(p.uuid), "url": p.url} for p in reaction.products.all()
|
||||
],
|
||||
"rules": [{"uuid": str(r.uuid), "url": r.url} for r in reaction.rules.all()],
|
||||
"multi_step": reaction.multi_step,
|
||||
"medline_references": reaction.medline_references,
|
||||
"external_identifiers": serialize_external_identifiers(reaction),
|
||||
}
|
||||
)
|
||||
result["reactions"].append(reaction_dict)
|
||||
|
||||
# Export pathways
|
||||
print("Exporting pathways...")
|
||||
for pathway in package.pathways.order_by("url"):
|
||||
pathway_dict = serialize_base_object(
|
||||
pathway, include_aliases=True, include_scenarios=True
|
||||
)
|
||||
|
||||
# Add setting reference if exists
|
||||
if hasattr(pathway, "setting") and pathway.setting:
|
||||
pathway_dict["setting"] = {
|
||||
"uuid": str(pathway.setting.uuid),
|
||||
"url": pathway.setting.url,
|
||||
}
|
||||
else:
|
||||
pathway_dict["setting"] = None
|
||||
|
||||
result["pathways"].append(pathway_dict)
|
||||
|
||||
# Export nodes
|
||||
print("Exporting nodes...")
|
||||
pathway_nodes = (
|
||||
Node.objects.filter(pathway__package=package)
|
||||
.select_related("pathway", "default_node_label")
|
||||
.prefetch_related("node_labels", "out_edges")
|
||||
.order_by("url")
|
||||
)
|
||||
|
||||
for node in pathway_nodes:
|
||||
node_dict = serialize_base_object(node, include_aliases=True, include_scenarios=True)
|
||||
node_dict.update(
|
||||
{
|
||||
"pathway": {"uuid": str(node.pathway.uuid), "url": node.pathway.url},
|
||||
"default_node_label": {
|
||||
"uuid": str(node.default_node_label.uuid),
|
||||
"url": node.default_node_label.url,
|
||||
},
|
||||
"node_labels": [
|
||||
{"uuid": str(label.uuid), "url": label.url}
|
||||
for label in node.node_labels.all()
|
||||
],
|
||||
"out_edges": [
|
||||
{"uuid": str(edge.uuid), "url": edge.url} for edge in node.out_edges.all()
|
||||
],
|
||||
"depth": node.depth,
|
||||
}
|
||||
)
|
||||
result["nodes"].append(node_dict)
|
||||
|
||||
# Export edges
|
||||
print("Exporting edges...")
|
||||
pathway_edges = (
|
||||
Edge.objects.filter(pathway__package=package)
|
||||
.select_related("pathway", "edge_label")
|
||||
.prefetch_related("start_nodes", "end_nodes")
|
||||
.order_by("url")
|
||||
)
|
||||
|
||||
for edge in pathway_edges:
|
||||
edge_dict = serialize_base_object(edge, include_aliases=True, include_scenarios=True)
|
||||
edge_dict.update(
|
||||
{
|
||||
"pathway": {"uuid": str(edge.pathway.uuid), "url": edge.pathway.url},
|
||||
"edge_label": {"uuid": str(edge.edge_label.uuid), "url": edge.edge_label.url},
|
||||
"start_nodes": [
|
||||
{"uuid": str(node.uuid), "url": node.url} for node in edge.start_nodes.all()
|
||||
],
|
||||
"end_nodes": [
|
||||
{"uuid": str(node.uuid), "url": node.url} for node in edge.end_nodes.all()
|
||||
],
|
||||
}
|
||||
)
|
||||
result["edges"].append(edge_dict)
|
||||
|
||||
# Export scenarios
|
||||
print("Exporting scenarios...")
|
||||
for scenario in package.scenarios.order_by("url"):
|
||||
scenario_dict = serialize_base_object(
|
||||
scenario, include_aliases=False, include_scenarios=False
|
||||
)
|
||||
scenario_dict.update(
|
||||
{
|
||||
"scenario_date": scenario.scenario_date,
|
||||
"scenario_type": scenario.scenario_type,
|
||||
"parent": {"uuid": str(scenario.parent.uuid), "url": scenario.parent.url}
|
||||
if scenario.parent
|
||||
else None,
|
||||
"additional_information": scenario.additional_information,
|
||||
}
|
||||
)
|
||||
result["scenarios"].append(scenario_dict)
|
||||
|
||||
# Export models
|
||||
if include_models:
|
||||
print("Exporting models...")
|
||||
package_models = (
|
||||
package.models.select_related("app_domain")
|
||||
.prefetch_related("rule_packages", "data_packages", "eval_packages")
|
||||
.order_by("url")
|
||||
)
|
||||
|
||||
for model in package_models:
|
||||
model_dict = serialize_base_object(
|
||||
model, include_aliases=True, include_scenarios=False
|
||||
)
|
||||
|
||||
# Common fields for PackageBasedModel
|
||||
if hasattr(model, "rule_packages"):
|
||||
model_dict.update(
|
||||
{
|
||||
"rule_packages": [
|
||||
{"uuid": str(p.uuid), "url": p.url}
|
||||
for p in model.rule_packages.all()
|
||||
],
|
||||
"data_packages": [
|
||||
{"uuid": str(p.uuid), "url": p.url}
|
||||
for p in model.data_packages.all()
|
||||
],
|
||||
"eval_packages": [
|
||||
{"uuid": str(p.uuid), "url": p.url}
|
||||
for p in model.eval_packages.all()
|
||||
],
|
||||
"threshold": model.threshold,
|
||||
"eval_results": model.eval_results,
|
||||
"model_status": model.model_status,
|
||||
}
|
||||
)
|
||||
|
||||
if model.app_domain:
|
||||
model_dict["app_domain"] = {
|
||||
"uuid": str(model.app_domain.uuid),
|
||||
"url": model.app_domain.url,
|
||||
}
|
||||
else:
|
||||
model_dict["app_domain"] = None
|
||||
|
||||
# Specific fields for different model types
|
||||
if isinstance(model, RuleBasedRelativeReasoning):
|
||||
model_dict.update(
|
||||
{
|
||||
"model_type": "RuleBasedRelativeReasoning",
|
||||
"min_count": model.min_count,
|
||||
"max_count": model.max_count,
|
||||
}
|
||||
)
|
||||
elif isinstance(model, MLRelativeReasoning):
|
||||
model_dict["model_type"] = "MLRelativeReasoning"
|
||||
elif isinstance(model, EnviFormer):
|
||||
model_dict["model_type"] = "EnviFormer"
|
||||
else:
|
||||
model_dict["model_type"] = "EPModel"
|
||||
|
||||
result["models"].append(model_dict)
|
||||
|
||||
print(f"Export completed for package: {package.name}")
|
||||
print(f"- Compounds: {len(result['compounds'])}")
|
||||
print(f"- Structures: {len(result['structures'])}")
|
||||
print(f"- Simple rules: {len(result['rules']['simple_rules'])}")
|
||||
print(f"- Parallel rules: {len(result['rules']['parallel_rules'])}")
|
||||
print(f"- Sequential rules: {len(result['rules']['sequential_rules'])}")
|
||||
print(f"- Reactions: {len(result['reactions'])}")
|
||||
print(f"- Pathways: {len(result['pathways'])}")
|
||||
print(f"- Nodes: {len(result['nodes'])}")
|
||||
print(f"- Edges: {len(result['edges'])}")
|
||||
print(f"- Scenarios: {len(result['scenarios'])}")
|
||||
print(f"- Models: {len(result['models'])}")
|
||||
|
||||
return result
|
||||
return data.model_dump(mode="json")
|
||||
|
||||
|
||||
class PackageImporter:
|
||||
|
||||
34
uv.lock
generated
34
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.4.2"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#0a608c85c73a6ef5c38afea87d2b57fb43f01a70" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
@ -2763,9 +2763,9 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54", upload-time = "2025-10-01T23:35:50Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58", upload-time = "2025-10-01T23:35:52Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2785,19 +2785,19 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5", upload-time = "2025-10-01T23:33:41Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d", upload-time = "2025-10-01T23:33:45Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e", upload-time = "2025-10-01T23:33:48Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d", upload-time = "2025-10-01T23:33:52Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434", upload-time = "2025-10-01T23:34:10Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d", upload-time = "2025-10-01T23:34:15Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25", upload-time = "2025-10-01T23:34:19Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de", upload-time = "2025-10-01T23:34:23Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856", upload-time = "2025-10-01T23:34:28Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88", upload-time = "2025-10-01T23:34:45Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041", upload-time = "2025-10-01T23:34:50Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab", upload-time = "2025-10-01T23:34:53Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user