forked from enviPath/enviPy
Compare commits
23 Commits
e3876ac945
...
develop-ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 520fb9f510 | |||
| 8ed3b506cc | |||
| ac297b2e25 | |||
| e2da59634b | |||
| f8d01e4477 | |||
| d381effdaf | |||
| 19d90b51eb | |||
| cca121af21 | |||
| 74489094c9 | |||
| 14cfc1e4d7 | |||
| 868bbf5c05 | |||
| be5ee1d1d7 | |||
| 20fd949dfd | |||
| c9b643fe6e | |||
| 1a9f1cf9af | |||
| c7c7e17e43 | |||
| 674e10c7fa | |||
| 8079b80d57 | |||
| 76e63fda2c | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e |
13
Dockerfile
13
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}"
|
||||
|
||||
@ -232,5 +232,6 @@ class PESStructure(CompoundStructure):
|
||||
"is_pes": True,
|
||||
"pes_link": self.pes_link,
|
||||
# Will overwrite image from Node
|
||||
"image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}"
|
||||
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
||||
"image_type": "png",
|
||||
}
|
||||
|
||||
@ -75,6 +75,10 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
@ -83,6 +87,10 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure)
|
||||
if node_qs.exists():
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
n = Node()
|
||||
n.stereo_removed = False
|
||||
n.pathway = current_pathway
|
||||
|
||||
@ -491,3 +491,5 @@ BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID")
|
||||
BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID")
|
||||
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
||||
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")
|
||||
|
||||
os.environ["NO_PROXY"] = "localhost,127.0.0.1,epbiotransformer3"
|
||||
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})
|
||||
|
||||
@ -439,23 +439,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):
|
||||
@ -872,6 +899,27 @@ def delete_compound(request, package_uuid, compound_uuid):
|
||||
}
|
||||
|
||||
|
||||
class CreateCompoundStructure(Schema):
|
||||
smiles: str
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
inchi: str | None = None
|
||||
molfile: str | None = None
|
||||
|
||||
|
||||
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
|
||||
def create_package_compound_structure(
|
||||
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
|
||||
):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
||||
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
|
||||
return redirect(cs.url)
|
||||
except ValueError as e:
|
||||
return 400, {"message": str(e)}
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||
)
|
||||
@ -1398,6 +1446,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"
|
||||
@ -1541,28 +1590,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}")
|
||||
@ -1613,6 +1690,7 @@ class PathwayNode(Schema):
|
||||
name: str = Field(None, alias="name")
|
||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
|
||||
@staticmethod
|
||||
def resolve_atom_count(obj: Node):
|
||||
@ -1766,6 +1844,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:
|
||||
@ -1879,30 +1980,42 @@ class CreateNode(Schema):
|
||||
|
||||
@router.post(
|
||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||
response={200: str | Any, 403: Error},
|
||||
response={200: str | Any, 400: Error, 403: Error},
|
||||
)
|
||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||
|
||||
# TODO Code Dup from bayer.views
|
||||
|
||||
if n.pesLink:
|
||||
from bayer.views import fetch_pes
|
||||
from bayer.models import PESCompound
|
||||
|
||||
try:
|
||||
pes_data = fetch_pes(request, c.pesLink)
|
||||
pes_data = fetch_pes(request, n.pesLink)
|
||||
except ValueError as e:
|
||||
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
||||
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if p.classification_level != Package.Classification.SECRET:
|
||||
return 400, "Cannot create PESs for non-secret packages."
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
||||
return 400, {
|
||||
"messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"
|
||||
}
|
||||
|
||||
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||
c = PESCompound.create(p, pes_data, n.nodeName, n.nodeReason)
|
||||
|
||||
node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure)
|
||||
if node_qs.exists():
|
||||
return redirect(pw.url)
|
||||
|
||||
node = Node()
|
||||
node.stereo_removed = False
|
||||
@ -2065,6 +2178,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,
|
||||
@ -2072,8 +2189,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!"}
|
||||
|
||||
@ -1065,52 +1065,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
|
||||
|
||||
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),
|
||||
]
|
||||
@ -1703,7 +1703,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
|
||||
@ -1781,7 +1781,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):
|
||||
@ -1894,6 +1894,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)
|
||||
@ -2186,6 +2189,50 @@ 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()
|
||||
|
||||
for n in self.root_nodes:
|
||||
depth_map[0].append(n)
|
||||
|
||||
# At most depth len(nodes) is possible
|
||||
for i in range(self.nodes.count()):
|
||||
level_nodes = depth_map.get(i, [])
|
||||
|
||||
if len(level_nodes) == 0:
|
||||
break
|
||||
|
||||
unique_next_level = set()
|
||||
for n in level_nodes:
|
||||
processed.add(n)
|
||||
for e in self.edges:
|
||||
if n in e.start_nodes.all():
|
||||
for p in e.end_nodes.all():
|
||||
if p not in processed:
|
||||
unique_next_level.add(p)
|
||||
|
||||
if len(unique_next_level) > 0:
|
||||
depth_map[i + 1] = list(unique_next_level)
|
||||
|
||||
for depth, nodes in depth_map.items():
|
||||
for n in nodes:
|
||||
if n.depth != depth and depth != 0:
|
||||
n.depth = depth
|
||||
n.save()
|
||||
|
||||
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
@ -2413,6 +2460,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
|
||||
@ -2442,7 +2491,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
|
||||
@ -4450,18 +4499,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:
|
||||
|
||||
@ -2539,6 +2539,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:
|
||||
@ -3110,12 +3113,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")
|
||||
|
||||
@ -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'
|
||||
@ -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",
|
||||
|
||||
@ -463,7 +463,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>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -581,11 +581,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));
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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>
|
||||
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 %}
|
||||
@ -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) ===== #}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -56,8 +56,8 @@
|
||||
<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"
|
||||
>{{ um.username }}
|
||||
<a href="{% if not user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
|
||||
>{{ um.username }}
|
||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||
>
|
||||
</li>
|
||||
|
||||
@ -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,22 @@
|
||||
</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);
|
||||
|
||||
@ -110,8 +110,6 @@
|
||||
<div
|
||||
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
||||
>
|
||||
<a href="/legal" class="link link-hover">Legal</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/terms" class="link link-hover">Terms of Use</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
||||
|
||||
@ -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!")
|
||||
|
||||
|
||||
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