forked from enviPath/enviPy
Compare commits
5 Commits
develop-ba
...
3b5d299128
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b5d299128 | |||
| 38e901a51e | |||
| c92fccaf8e | |||
| 5eb3ebac89 | |||
| d9530ce755 |
13
Dockerfile
13
Dockerfile
@ -6,23 +6,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
curl \
|
||||
openssh-client \
|
||||
git \
|
||||
ca-certificates \
|
||||
nodejs \
|
||||
npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node 22 + pnpm
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@latest --activate \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
import enum
|
||||
from typing import Optional
|
||||
|
||||
from envipy_additional_information import GroupEnum as G
|
||||
from envipy_additional_information import SubcategoryEnum as S
|
||||
from envipy_additional_information import (
|
||||
register,
|
||||
register_parser_command,
|
||||
EnviPyModel,
|
||||
# EnviPyModelParser,
|
||||
Interval,
|
||||
UIConfig,
|
||||
IntervalConfig,
|
||||
WidgetType,
|
||||
registry,
|
||||
)
|
||||
|
||||
|
||||
@register(keyname="compoundlabel", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
||||
class CompoundLabel(EnviPyModel):
|
||||
label: str
|
||||
|
||||
class UI:
|
||||
title = "Compound Label"
|
||||
label = UIConfig(widget=WidgetType.TEXT, label="Label", order=1)
|
||||
|
||||
|
||||
# TODO Expose EnviPyModelParser in lib and subclass
|
||||
@register_parser_command("compoundlabel")
|
||||
class CompoundLabelParser:
|
||||
@staticmethod
|
||||
def from_string(data: str) -> CompoundLabel:
|
||||
return CompoundLabel(label=data)
|
||||
|
||||
|
||||
@register(keyname="studywaterstoragecapacity", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
||||
class StudyWaterStorageCapacity(EnviPyModel):
|
||||
capacity: str
|
||||
|
||||
class UI:
|
||||
title = "Study Water Storage Capacity"
|
||||
capacity = UIConfig(widget=WidgetType.TEXT, label="Study Water Storage Capacity", order=1)
|
||||
|
||||
|
||||
# TODO Expose EnviPyModelParser in lib and subclass
|
||||
@register_parser_command("studywst")
|
||||
class StudyWaterStorageCapacityParser:
|
||||
@staticmethod
|
||||
def from_string(data: str) -> StudyWaterStorageCapacity:
|
||||
return StudyWaterStorageCapacity(capacity=data)
|
||||
|
||||
|
||||
class ObservationType(enum.Enum):
|
||||
OBSERVED = "observed"
|
||||
APPLIED = "applied"
|
||||
NA = 'NA'
|
||||
|
||||
|
||||
@register(keyname="observation", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
||||
class Observation(EnviPyModel):
|
||||
type: ObservationType
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
|
||||
class UI:
|
||||
title = "Observation"
|
||||
type = UIConfig(widget=WidgetType.SELECT, label="Observed or Applied", order=1)
|
||||
min_value = UIConfig(widget=WidgetType.NUMBER, label="Min Value", order=2)
|
||||
max_value = UIConfig(widget=WidgetType.NUMBER, label="Max Value", order=3)
|
||||
|
||||
|
||||
# TODO Expose EnviPyModelParser in lib and subclass
|
||||
@register_parser_command("observation")
|
||||
class ObservationParser:
|
||||
@staticmethod
|
||||
def from_string(data: str) -> Observation:
|
||||
parts = data.split(";")
|
||||
|
||||
observation_type = ObservationType(parts[0])
|
||||
min_value = None
|
||||
if parts[1]:
|
||||
try:
|
||||
min_value = float(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
max_value = None
|
||||
if parts[2]:
|
||||
try:
|
||||
max_value = float(parts[2])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return Observation(type=observation_type, min_value=min_value, max_value=max_value)
|
||||
|
||||
|
||||
@register(keyname="kinetics", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL])
|
||||
class Kinetics(EnviPyModel):
|
||||
dt50: Interval[float]
|
||||
normalized_dt50: bool
|
||||
chi2err: Optional[float] = None
|
||||
t_test: Optional[float] = None
|
||||
swarc: Optional[float] = None
|
||||
visual_fit: Optional[int] = None
|
||||
comment: str
|
||||
source: str
|
||||
kinetic_model: str
|
||||
k1: Optional[float] = None
|
||||
k2: Optional[float] = None
|
||||
g: Optional[float] = None
|
||||
tb: Optional[float] = None
|
||||
alpha: Optional[float] = None
|
||||
beta: Optional[float] = None
|
||||
|
||||
class UI:
|
||||
title = "Kinetics"
|
||||
|
||||
# Field config
|
||||
dt50 = IntervalConfig(label="DT50 Range", order=1, unit="d")
|
||||
normalized_dt50 = UIConfig(widget=WidgetType.CHECKBOX, label="Normalized DT50", order=2)
|
||||
chi2err = UIConfig(widget=WidgetType.NUMBER, label="Chi2err", order=3)
|
||||
t_test = UIConfig(widget=WidgetType.NUMBER, label="T-Test", order=4)
|
||||
swarc = UIConfig(widget=WidgetType.NUMBER, label="SWARC", order=5)
|
||||
visual_fit = UIConfig(widget=WidgetType.NUMBER, label="Visual Fit", order=6)
|
||||
comment = UIConfig(widget=WidgetType.TEXTAREA, label="Comments", order=7)
|
||||
source = UIConfig(widget=WidgetType.TEXT, label="Source", order=8)
|
||||
kinetic_model = UIConfig(widget=WidgetType.SELECT, label="Kinetic Model", order=9)
|
||||
k1 = UIConfig(widget=WidgetType.NUMBER, label="K1", order=10)
|
||||
k2 = UIConfig(widget=WidgetType.NUMBER, label="K2", order=11)
|
||||
g = UIConfig(widget=WidgetType.NUMBER, label="G", order=12)
|
||||
tb = UIConfig(widget=WidgetType.NUMBER, label="TB", order=13)
|
||||
alpha = UIConfig(widget=WidgetType.NUMBER, label="Alpha", order=14)
|
||||
beta = UIConfig(widget=WidgetType.NUMBER, label="Beta", order=15)
|
||||
|
||||
|
||||
# TODO Expose EnviPyModelParser in lib and subclass
|
||||
@register_parser_command("kineticevaluation")
|
||||
class KinecticsParser:
|
||||
@staticmethod
|
||||
def from_string(data: str) -> Kinetics:
|
||||
parts = data.split(";")
|
||||
dt50 = registry.get_parser("interval").from_string(parts[0])
|
||||
normalized_dt50 = parts[1] == "true"
|
||||
chi2err = float(parts[2]) if parts[2] else None
|
||||
t_test = float(parts[3]) if parts[3] else None
|
||||
swarc = float(parts[4]) if parts[4] else None
|
||||
visual_fit = int(parts[5]) if parts[5] else None
|
||||
comment = parts[6]
|
||||
source = parts[7]
|
||||
kinetic_model = parts[8]
|
||||
k1 = float(parts[9]) if parts[9] else None
|
||||
k2 = float(parts[10]) if parts[10] else None
|
||||
g = float(parts[11]) if parts[11] else None
|
||||
tb = float(parts[12]) if parts[12] else None
|
||||
alpha = float(parts[13]) if parts[13] else None
|
||||
beta = float(parts[14]) if parts[14] else None
|
||||
|
||||
return Kinetics(
|
||||
dt50=dt50,
|
||||
normalized_dt50=normalized_dt50,
|
||||
chi2err=chi2err,
|
||||
t_test=t_test,
|
||||
swarc=swarc,
|
||||
visual_fit=visual_fit,
|
||||
comment=comment,
|
||||
source=source,
|
||||
kinetic_model=kinetic_model,
|
||||
k1=k1,
|
||||
k2=k2,
|
||||
g=g,
|
||||
tb=tb,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(KinecticsParser.from_string("187.0 - 187.0;false;;;;;;;AFO;;;;;;"))
|
||||
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
from bayer import additional_information # noqa: F401
|
||||
from epdb.template_registry import register_template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -37,4 +36,4 @@ register_template(
|
||||
register_template(
|
||||
"epdb.objects.node.viz",
|
||||
"objects/node_viz.html",
|
||||
)
|
||||
)
|
||||
@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
|
||||
def create(
|
||||
compound: Compound,
|
||||
pes_link: str,
|
||||
molfile: str,
|
||||
mol_file: str,
|
||||
smiles: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
cs.smiles = smiles
|
||||
cs.molfile = molfile
|
||||
cs.mol_file = mol_file
|
||||
cs.pes_link = pes_link
|
||||
cs.compound = compound
|
||||
|
||||
@ -232,6 +232,5 @@ 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_type": "png",
|
||||
"image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}"
|
||||
}
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -2,13 +2,14 @@ import base64
|
||||
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from bayer.models import PESCompound
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Pathway, Node
|
||||
from epdb.views import _anonymous_or_real, error
|
||||
from epdb.views import _anonymous_or_real
|
||||
from utilities.decorators import package_permission_required
|
||||
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
@ -22,11 +23,7 @@ def create_pes(request, package_uuid):
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
return error(
|
||||
request,
|
||||
f'Creation of PESs for package {current_package.name} failed!',
|
||||
"Creating PESs for internal packages is not allowed.",
|
||||
)
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
|
||||
compound_name = request.POST.get('compound-name')
|
||||
compound_description = request.POST.get('compound-description')
|
||||
@ -36,25 +33,21 @@ def create_pes(request, package_uuid):
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return HttpResponseBadRequest(
|
||||
return BadRequest(
|
||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
return redirect(pes.url)
|
||||
else:
|
||||
return HttpResponseBadRequest("Please provide a PES link.")
|
||||
return BadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
@ -68,11 +61,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
return error(
|
||||
request,
|
||||
f'Creation of PESs for package {current_package.name} failed!',
|
||||
"Creating PESs for internal packages is not allowed.",
|
||||
)
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
|
||||
compound_name = request.POST.get('compound-name')
|
||||
compound_description = request.POST.get('compound-description')
|
||||
@ -82,26 +71,18 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return HttpResponseBadRequest(
|
||||
return BadRequest(
|
||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure)
|
||||
if node_qs.exists():
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
n = Node()
|
||||
n.stereo_removed = False
|
||||
n.pathway = current_pathway
|
||||
@ -116,7 +97,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest("Please provide a PES link.")
|
||||
return BadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
@ -125,9 +106,6 @@ def fetch_pes(request, pes_url) -> dict:
|
||||
from epauth.views import get_access_token_from_request
|
||||
token = get_access_token_from_request(request)
|
||||
|
||||
if token is None:
|
||||
token = pes_url.split('/')[-1] == 'dummy'
|
||||
|
||||
if token:
|
||||
for k, v in s.PES_API_MAPPING.items():
|
||||
if pes_url.startswith(k):
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
import enum
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from envipy_additional_information import register, EnviPyModel, UIConfig, WidgetType
|
||||
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
|
||||
from envipy_additional_information import register
|
||||
|
||||
from bridge.contracts import Classifier # noqa: I001
|
||||
from bridge.dto import (
|
||||
@ -18,9 +17,6 @@ from bridge.dto import (
|
||||
TransformationProductPrediction,
|
||||
) # noqa: I001
|
||||
|
||||
logger = logging.getLogger("epdb")
|
||||
|
||||
|
||||
class SamplingAlgorithm(enum.Enum):
|
||||
EXACT = "exact"
|
||||
|
||||
@ -89,13 +85,14 @@ class BB4G(Classifier):
|
||||
}
|
||||
|
||||
started = False
|
||||
|
||||
while not started:
|
||||
retries = 0
|
||||
while not started and retries < 5:
|
||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
||||
logger.info(f"Starting BB4G: {res.status_code}")
|
||||
|
||||
if res.status_code == 200:
|
||||
started = True
|
||||
elif res.status_code in [500, 502]:
|
||||
retries += 1
|
||||
import time
|
||||
time.sleep(5)
|
||||
else:
|
||||
@ -169,30 +166,18 @@ class BB4G(Classifier):
|
||||
"cutoff": self.config.cutoff,
|
||||
}
|
||||
|
||||
retries = 0
|
||||
while retries < 100:
|
||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
|
||||
proxies=s.PROXIES or None)
|
||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
|
||||
|
||||
if resp.status_code == 418:
|
||||
retries += 1
|
||||
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
|
||||
import time
|
||||
time.sleep(3)
|
||||
continue
|
||||
resp.raise_for_status()
|
||||
|
||||
resp.raise_for_status()
|
||||
for substrate, predictions in resp.json().items():
|
||||
preds = {}
|
||||
|
||||
for substrate, predictions in resp.json().items():
|
||||
preds = {}
|
||||
for pred in predictions:
|
||||
prod = pred["prediction"]
|
||||
prob = math.exp(pred["log_likelihood"])
|
||||
preds[prod] = prob
|
||||
|
||||
for pred in predictions:
|
||||
prod = pred["prediction"]
|
||||
prob = math.exp(pred["log_likelihood"])
|
||||
preds[prod] = prob
|
||||
|
||||
result[substrate] = preds
|
||||
|
||||
break
|
||||
result[substrate] = preds
|
||||
|
||||
return result
|
||||
|
||||
@ -261,6 +261,7 @@ class Classifier(Plugin):
|
||||
for k, v in data.items():
|
||||
if v != "":
|
||||
cpy[k] = v
|
||||
|
||||
return cls.Config(**cpy)
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -25,14 +25,25 @@ services:
|
||||
- ep_bayer_redis_data:/data
|
||||
|
||||
biotransformer3:
|
||||
image: git.envipath.com/envipath/biotransformer3:1.0
|
||||
image: envipath/biotransformer3:1.0
|
||||
container_name: epbiotransformer3
|
||||
|
||||
# web:
|
||||
# image: envipath/envipy-bayer:1.0
|
||||
# container_name: epdjango
|
||||
# ports:
|
||||
# - "127.0.0.1:8000:8000"
|
||||
# env_file:
|
||||
# - .env
|
||||
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
# volumes:
|
||||
# - ep_bayer_data:/opt/enviPy/
|
||||
|
||||
celery_worker:
|
||||
image: git.envipath.com/envipath/envipy-bayer:1.2
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epcelery
|
||||
env_file:
|
||||
- .env
|
||||
- .env.dev
|
||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||
volumes:
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
@ -275,12 +275,6 @@ LOGGING = {
|
||||
"filename": os.path.join(LOG_DIR, "debug.log"),
|
||||
"formatter": "simple",
|
||||
},
|
||||
"auth_file": {
|
||||
"level": "INFO", # Or higher
|
||||
"class": "logging.FileHandler",
|
||||
"filename": os.path.join(LOG_DIR, "auth.log"),
|
||||
"formatter": "simple",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
# For everything under epdb/ loaded via getlogger(__name__)
|
||||
@ -301,11 +295,6 @@ LOGGING = {
|
||||
"propagate": True,
|
||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||
},
|
||||
"auth": {
|
||||
"handlers": ["auth_file"],
|
||||
"propagate": True,
|
||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -354,7 +343,7 @@ DEFAULT_MODEL_PARAMS = {
|
||||
"num_chains": 10,
|
||||
}
|
||||
|
||||
DEFAULT_MAX_NUMBER_OF_NODES = 9999
|
||||
DEFAULT_MAX_NUMBER_OF_NODES = 50
|
||||
DEFAULT_MAX_DEPTH = 8
|
||||
DEFAULT_MODEL_THRESHOLD = 0.25
|
||||
|
||||
@ -502,5 +491,3 @@ 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"
|
||||
@ -1,26 +0,0 @@
|
||||
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,7 +15,6 @@ from .endpoints import (
|
||||
additional_information,
|
||||
settings,
|
||||
groups,
|
||||
joblogs,
|
||||
)
|
||||
|
||||
# Main router with authentication
|
||||
@ -38,7 +37,6 @@ 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,10 +1,7 @@
|
||||
from datetime import datetime
|
||||
from ninja import FilterSchema, FilterLookup, Schema
|
||||
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):
|
||||
@ -136,23 +133,3 @@ 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})
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
class InvalidSMILESException(Exception):
|
||||
pass
|
||||
|
||||
class PackageImportException(Exception):
|
||||
pass
|
||||
@ -439,50 +439,23 @@ class PackageSchema(Schema):
|
||||
|
||||
@staticmethod
|
||||
def resolve_readers(obj: Package):
|
||||
readers = []
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.READ[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
|
||||
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
|
||||
return [{u.id: u.get_name()} for u in users]
|
||||
|
||||
@staticmethod
|
||||
def resolve_writers(obj: Package):
|
||||
writers = []
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.WRITE[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
|
||||
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
|
||||
return [{u.id: u.get_name()} for u in users]
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_comment(obj):
|
||||
@ -633,14 +606,9 @@ class CompoundSchema(Schema):
|
||||
reviewStatus: str = Field(False, alias="review_status")
|
||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||
structures: List["CompoundStructureSchema"] = []
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
|
||||
@staticmethod
|
||||
def resolve_pes_link(obj: Compound):
|
||||
return getattr(obj.default_structure, "pes_link", None)
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_status(obj: Compound):
|
||||
def resolve_review_status(obj: CompoundStructure):
|
||||
return "reviewed" if obj.package.reviewed else "unreviewed"
|
||||
|
||||
@staticmethod
|
||||
@ -714,7 +682,6 @@ class CompoundStructureSchema(Schema):
|
||||
reviewStatus: str = Field(None, alias="review_status")
|
||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||
smiles: str = Field(None, alias="smiles")
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_status(obj: CompoundStructure):
|
||||
@ -877,10 +844,6 @@ def create_package_compound(
|
||||
|
||||
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:
|
||||
@ -909,27 +872,6 @@ 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}"
|
||||
)
|
||||
@ -1456,7 +1398,6 @@ 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"
|
||||
@ -1600,56 +1541,28 @@ def create_package_additional_information(request, package_uuid):
|
||||
scen = request.POST.get("scenario")
|
||||
scenario = Scenario.objects.get(package=p, url=scen)
|
||||
|
||||
if request.POST.get("adInfoTypes[]"):
|
||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||
attach_obj = url_parser.get_object()
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
ai,
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
|
||||
# TODO implement additional information endpoint ?
|
||||
return redirect(f"{scenario.url}")
|
||||
@ -1693,15 +1606,13 @@ 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="node_label_id")
|
||||
idreact: str = Field(None, alias="node_label_id")
|
||||
idcomp: str = Field(None, alias="default_node_label.url")
|
||||
idreact: str = Field(None, alias="default_node_label.url")
|
||||
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="smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
|
||||
@staticmethod
|
||||
def resolve_atom_count(obj: Node):
|
||||
@ -1714,10 +1625,24 @@ class PathwayNode(Schema):
|
||||
# TODO
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def resolve_engineered_intermediate(obj: Node):
|
||||
# TODO
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def resolve_image(obj: Node):
|
||||
return f"{obj.default_node_label.url}?image=svg"
|
||||
|
||||
@staticmethod
|
||||
def resolve_image_size(obj: Node):
|
||||
return 400
|
||||
|
||||
@staticmethod
|
||||
def resolve_proposed_intermediate(obj: Node):
|
||||
# TODO
|
||||
return []
|
||||
|
||||
|
||||
class PathwaySchema(Schema):
|
||||
aliases: List[str] = Field([], alias="aliases")
|
||||
@ -1841,29 +1766,6 @@ 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:
|
||||
@ -1977,42 +1879,30 @@ class CreateNode(Schema):
|
||||
|
||||
@router.post(
|
||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||
response={200: str | Any, 400: Error, 403: Error},
|
||||
response={200: str | Any, 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, n.pesLink)
|
||||
pes_data = fetch_pes(request, c.pesLink)
|
||||
except ValueError as e:
|
||||
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
||||
return 400, {"message": f"Could not fetch PES data for {c.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, 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)
|
||||
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||
|
||||
node = Node()
|
||||
node.stereo_removed = False
|
||||
@ -2175,10 +2065,6 @@ 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,
|
||||
@ -2186,7 +2072,6 @@ 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
|
||||
@ -2385,31 +2270,3 @@ def get_setting(request, setting_uuid):
|
||||
return 403, {
|
||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
|
||||
########
|
||||
# Util #
|
||||
########
|
||||
class NonPersistent(Schema):
|
||||
smiles: str
|
||||
setting_url: str = Field(..., alias="settingUri")
|
||||
|
||||
|
||||
@router.post("/util", response={200: Any, 403: Error})
|
||||
def predict(request, np: Form[NonPersistent]):
|
||||
try:
|
||||
print(request.user)
|
||||
print(np.setting_url)
|
||||
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||
|
||||
from epdb.logic import SPathway
|
||||
|
||||
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||
spw.predict()
|
||||
|
||||
return spw.to_json()
|
||||
|
||||
except ValueError:
|
||||
return 403, {
|
||||
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
@ -1078,8 +1078,10 @@ class PackageManager(object):
|
||||
data: Dict[str, Any],
|
||||
owner: User,
|
||||
preserve_uuids=False,
|
||||
add_import_timestamp=True,
|
||||
trust_reviewed=False,
|
||||
) -> Package:
|
||||
importer = PackageImporter(data, preserve_uuids)
|
||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
||||
imported_package = importer.do_import()
|
||||
|
||||
up = UserPackagePermission()
|
||||
@ -1896,12 +1898,6 @@ class SPathway(object):
|
||||
"to": to_indices,
|
||||
}
|
||||
|
||||
if edge.rule:
|
||||
e["rule"] = edge.rule.simple_json()
|
||||
|
||||
if edge.probability:
|
||||
e["probability"] = edge.probability
|
||||
|
||||
edges.append(e)
|
||||
|
||||
return {
|
||||
|
||||
@ -99,15 +99,11 @@ 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, all_envipath_user_group):
|
||||
p = PackageManager.import_legacy_package(
|
||||
def import_package(self, data, owner):
|
||||
return 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,
|
||||
@ -202,7 +198,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, g)
|
||||
imported_package = self.import_package(package_data, admin)
|
||||
mapping[p.replace(".json", "")] = imported_package
|
||||
|
||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||
|
||||
@ -23,7 +23,6 @@ MAPPING = {
|
||||
"true": HalfLifeModel.SFO,
|
||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||
"other": HalfLifeModel.OTHER,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
# 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),
|
||||
]
|
||||
@ -1,150 +0,0 @@
|
||||
# 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"
|
||||
),
|
||||
),
|
||||
]
|
||||
209
epdb/models.py
209
epdb/models.py
@ -31,7 +31,6 @@ 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,
|
||||
@ -577,10 +576,6 @@ class ReactionIdentifierMixin(ExternalIdentifierMixin):
|
||||
def get_uniprot_identifiers(self):
|
||||
return self.get_external_identifier("UniProt")
|
||||
|
||||
def contains_pes(self):
|
||||
from bayer.models import PESStructure
|
||||
return any([isinstance(o, PESStructure) for o in self.educts.all()]) or any(
|
||||
[isinstance(o, PESStructure) for o in self.products.all()])
|
||||
|
||||
##############
|
||||
# EP Objects #
|
||||
@ -637,7 +632,7 @@ class EnviPathModel(TimeStampedModel):
|
||||
|
||||
class AliasMixin(models.Model):
|
||||
aliases = ArrayField(
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
@ -660,9 +655,7 @@ class AliasMixin(models.Model):
|
||||
|
||||
|
||||
class ScenarioMixin(models.Model):
|
||||
scenarios = models.ManyToManyField(
|
||||
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||
)
|
||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
||||
|
||||
@transaction.atomic
|
||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||
@ -788,7 +781,7 @@ class Compound(
|
||||
"CompoundStructure",
|
||||
verbose_name="Default Structure",
|
||||
related_name="compound_default_structure",
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
)
|
||||
|
||||
@ -865,17 +858,13 @@ class Compound(
|
||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
) -> "Compound":
|
||||
if smiles is None or smiles.strip() == "":
|
||||
raise InvalidSMILESException("SMILES is required")
|
||||
raise ValueError("SMILES is required")
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise InvalidSMILESException("Given SMILES is invalid")
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
raise ValueError("Given SMILES is invalid")
|
||||
|
||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
|
||||
@ -887,14 +876,7 @@ class Compound(
|
||||
|
||||
# Check if we find a direct match for a given SMILES
|
||||
if qs.exists():
|
||||
found_structure = qs.first()
|
||||
found_compound = found_structure.compound
|
||||
|
||||
if name:
|
||||
found_structure.add_alias(name)
|
||||
found_compound.add_alias(name)
|
||||
|
||||
return found_compound
|
||||
return qs.first().compound
|
||||
|
||||
|
||||
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||
@ -904,19 +886,16 @@ class Compound(
|
||||
# Check if we can find the standardized one
|
||||
if qs.exists():
|
||||
# TODO should we add a structure?
|
||||
found_structure = qs.first()
|
||||
found_compound = found_structure.compound
|
||||
|
||||
if name:
|
||||
found_structure.add_alias(name)
|
||||
found_compound.add_alias(name)
|
||||
|
||||
return found_compound
|
||||
return qs.first().compound
|
||||
|
||||
# Generate Compound
|
||||
c = Compound()
|
||||
c.package = package
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1167,27 +1146,18 @@ class CompoundStructure(
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
compound: Compound, smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
|
||||
compound: Compound, smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
):
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
||||
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
|
||||
if name:
|
||||
found_cs.add_alias(name)
|
||||
|
||||
return found_cs
|
||||
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
|
||||
if compound.pk is None:
|
||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||
|
||||
cs = CompoundStructure()
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
cs.name = name
|
||||
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if description is not None:
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
@ -1195,10 +1165,6 @@ class CompoundStructure(
|
||||
cs.smiles = smiles
|
||||
cs.compound = compound
|
||||
|
||||
# Check if molfile is present and valid
|
||||
if molfile is not None and FormatConverter.from_molfile(molfile) is not None:
|
||||
cs.molfile = molfile
|
||||
|
||||
if "normalized_structure" in kwargs:
|
||||
cs.normalized_structure = kwargs["normalized_structure"]
|
||||
|
||||
@ -1216,8 +1182,6 @@ 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
|
||||
@ -1415,9 +1379,6 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if not FormatConverter.is_valid_smirks(smirks):
|
||||
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
|
||||
|
||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
||||
@ -1429,17 +1390,14 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if query.exists():
|
||||
if query.count() > 1:
|
||||
logger.error(f"More than one rule matched this one! {query}")
|
||||
|
||||
found_rule = query.first()
|
||||
|
||||
if name:
|
||||
found_rule.add_alias(name)
|
||||
|
||||
return found_rule
|
||||
return query.first()
|
||||
|
||||
r = SimpleAmbitRule()
|
||||
r.package = package
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1660,15 +1618,10 @@ 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", blank=True
|
||||
)
|
||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||
medline_references = ArrayField(
|
||||
models.TextField(blank=False, null=False),
|
||||
null=True,
|
||||
verbose_name="Medline References",
|
||||
blank=True,
|
||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
||||
)
|
||||
|
||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||
@ -1685,13 +1638,8 @@ 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 = False,
|
||||
multi_step: bool = True,
|
||||
):
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None and name.strip() != "":
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
_educts = []
|
||||
_products = []
|
||||
|
||||
@ -1746,21 +1694,16 @@ class Reaction(
|
||||
logger.error(
|
||||
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
||||
)
|
||||
|
||||
found_reaction = existing_reaction_qs.first()
|
||||
|
||||
if name:
|
||||
found_reaction.add_alias(name)
|
||||
|
||||
return found_reaction
|
||||
return existing_reaction_qs.first()
|
||||
|
||||
r = Reaction()
|
||||
r.package = package
|
||||
|
||||
if r is not None:
|
||||
r.name = name
|
||||
# Clean for potential XSS
|
||||
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 description.strip() != "":
|
||||
if description is not None and name.strip() != "":
|
||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
r.multi_step = multi_step
|
||||
@ -1838,7 +1781,7 @@ class Reaction(
|
||||
return new_reaction
|
||||
|
||||
def smirks(self):
|
||||
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')])}"
|
||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
||||
|
||||
@property
|
||||
def as_svg(self):
|
||||
@ -1951,9 +1894,6 @@ 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)
|
||||
@ -2231,7 +2171,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
smiles: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
depth: Optional[int] = -1,
|
||||
depth: Optional[int] = 0,
|
||||
):
|
||||
return Node.create(self, smiles, depth, name=name, description=description)
|
||||
|
||||
@ -2260,28 +2200,18 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
|
||||
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()
|
||||
)
|
||||
for n in self.nodes:
|
||||
num_parents = in_count[str(n.uuid)]
|
||||
if num_parents == 0:
|
||||
# must be a root node or unconnected node
|
||||
if n.depth != 0:
|
||||
n.depth = 0
|
||||
n.save()
|
||||
|
||||
# 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)
|
||||
# Only root node may have children
|
||||
if out_count[str(n.uuid)] > 0:
|
||||
depth_map[0].append(n)
|
||||
|
||||
# At most depth len(nodes) is possible
|
||||
for i in range(self.nodes.count()):
|
||||
@ -2292,19 +2222,17 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
|
||||
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)
|
||||
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:
|
||||
if n.depth != depth:
|
||||
n.depth = depth
|
||||
n.save()
|
||||
|
||||
@ -2360,12 +2288,7 @@ 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.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,
|
||||
self.default_node_label.smiles, width=40, height=40
|
||||
),
|
||||
"image_type": "svg",
|
||||
"name": self.get_name(),
|
||||
@ -2379,7 +2302,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
},
|
||||
"predicted_properties": predicted_properties,
|
||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||
"proposed": self.is_proposed_intermediate(),
|
||||
"timeseries": self.get_timeseries_data(),
|
||||
**structure_data,
|
||||
}
|
||||
@ -2420,17 +2342,12 @@ 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.type == "OECD301FTimeSeries":
|
||||
return ai.get().model_dump(mode="json")
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
@ -2453,26 +2370,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
return data
|
||||
|
||||
def is_proposed_intermediate(self):
|
||||
collected = defaultdict(dict)
|
||||
for ai in self.additional_information.filter(
|
||||
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"]
|
||||
, scenario__isnull=False
|
||||
):
|
||||
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||
|
||||
if ai.type == "ProposedIntermediate":
|
||||
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||
|
||||
if ai.type == "Confidence":
|
||||
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||
|
||||
if ai.type == "TransformationProductImportance":
|
||||
collected[str(ai.scenario.uuid)]["Transformation product importance"] = ai.get().importance.value
|
||||
|
||||
return list(collected.values())
|
||||
|
||||
def simple_json(self, include_description=False):
|
||||
res = super().simple_json()
|
||||
name = res.get("name", None)
|
||||
@ -2487,7 +2384,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.CASCADE
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
start_nodes = models.ManyToManyField(
|
||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||
@ -2566,8 +2463,6 @@ 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
|
||||
@ -2597,7 +2492,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=kwargs.get("multi_step", False),
|
||||
multi_step=False,
|
||||
)
|
||||
|
||||
e.edge_label = r
|
||||
@ -4605,22 +4500,18 @@ class AdditionalInformation(models.Model):
|
||||
|
||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||
|
||||
@staticmethod
|
||||
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
||||
def get(self) -> "EnviPyModel":
|
||||
from envipy_additional_information import registry
|
||||
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[ai_type](**ai_data)
|
||||
inst = MAPPING[self.type](**self.data)
|
||||
except Exception as e:
|
||||
print(f"Error loading {ai_type}: {e}")
|
||||
print(f"Error loading {self.type}: {e}")
|
||||
raise e
|
||||
|
||||
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,7 +19,6 @@ 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,
|
||||
@ -786,11 +785,6 @@ 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": {
|
||||
@ -803,14 +797,12 @@ def models(request):
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"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():
|
||||
@ -818,9 +810,6 @@ def models(request):
|
||||
"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()] = {
|
||||
@ -829,6 +818,12 @@ 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":
|
||||
@ -943,12 +938,13 @@ def package_models(request, package_uuid):
|
||||
"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():
|
||||
@ -1124,11 +1120,6 @@ 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:
|
||||
@ -1232,7 +1223,9 @@ 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)
|
||||
pack_json = PackageManager.export_package(
|
||||
current_package, include_models=False, include_external_identifiers=False
|
||||
)
|
||||
response = JsonResponse(pack_json, content_type="application/json")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
|
||||
@ -2368,13 +2361,7 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
||||
node_description = request.POST.get("node-description")
|
||||
|
||||
node_smiles = request.POST.get("node-smiles").strip()
|
||||
|
||||
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"
|
||||
)
|
||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
@ -2482,22 +2469,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
||||
|
||||
return JsonResponse({"success": current_node.url})
|
||||
|
||||
new_node_name = request.POST.get("node-name")
|
||||
new_node_description = request.POST.get("node-description")
|
||||
|
||||
if any([new_node_name, new_node_description]):
|
||||
if new_node_name is not None and new_node_name.strip() != "":
|
||||
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
current_node.name = new_node_name
|
||||
|
||||
if new_node_description is not None and new_node_description.strip() != "":
|
||||
new_node_description = nh3.clean(new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
current_node.description = new_node_description
|
||||
|
||||
current_node.save()
|
||||
return redirect(current_node.url)
|
||||
|
||||
return error(request, "Node update failed!", "No changes were made to the node")
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotAllowed(["GET", "POST"])
|
||||
|
||||
@ -3029,15 +3001,9 @@ def settings(request):
|
||||
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
||||
|
||||
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
|
||||
temp_max_nodes = request.POST.get("prediction-setting-max-nodes")
|
||||
if temp_max_nodes is None or temp_max_nodes == "" or int(temp_max_nodes) == -1:
|
||||
temp_max_nodes = s.DEFAULT_MAX_NUMBER_OF_NODES
|
||||
else:
|
||||
temp_max_nodes = int(request.POST.get("prediction-setting-max-nodes", 1))
|
||||
|
||||
max_nodes = min(
|
||||
max(
|
||||
temp_max_nodes,
|
||||
int(request.POST.get("prediction-setting-max-nodes", 1)),
|
||||
2,
|
||||
),
|
||||
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
||||
@ -3058,7 +3024,6 @@ def settings(request):
|
||||
|
||||
model_uuid = model_url.split("/")[-1]
|
||||
params["model"] = EPModel.objects.get(uuid=model_uuid)
|
||||
# TODO Check if removed if request contains "" or not at all
|
||||
params["model_threshold"] = request.POST.get(
|
||||
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
||||
)
|
||||
@ -3148,21 +3113,12 @@ 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")
|
||||
|
||||
# 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)
|
||||
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -65,25 +65,6 @@ 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)
|
||||
|
||||
@ -128,11 +109,11 @@ def migration(request):
|
||||
),
|
||||
"id": str(r.uuid),
|
||||
"url": r.url,
|
||||
"status": res or r.name in accepted_diffs,
|
||||
"status": res,
|
||||
}
|
||||
)
|
||||
|
||||
if res or r.name in accepted_diffs:
|
||||
if res:
|
||||
success += 1
|
||||
else:
|
||||
error += 1
|
||||
@ -154,16 +135,7 @@ 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,11 +21,5 @@
|
||||
"django",
|
||||
"tailwindcss",
|
||||
"daisyui"
|
||||
],
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@tailwindcss/oxide"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
- '@tailwindcss/oxide'
|
||||
|
||||
@ -34,12 +34,3 @@
|
||||
}
|
||||
|
||||
@import "./daisyui-theme.css";
|
||||
|
||||
select.select[multiple] {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
p a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
@ -59,9 +59,6 @@ document.addEventListener("alpine:init", () => {
|
||||
get isEditMode() {
|
||||
return this.mode === "edit";
|
||||
},
|
||||
get isRequired() {
|
||||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
||||
}
|
||||
});
|
||||
|
||||
// Text widget
|
||||
@ -296,34 +293,6 @@ 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",
|
||||
|
||||
@ -5,125 +5,6 @@
|
||||
*/
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
|
||||
const basePagination = (
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId
|
||||
) => ({
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId,
|
||||
|
||||
get paginatedItems() {
|
||||
return this.items;
|
||||
},
|
||||
|
||||
get showingStart() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return (this.currentPage - 1) * this.perPage + 1;
|
||||
},
|
||||
|
||||
get showingEnd() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.fetchPage(this.currentPage + 1);
|
||||
}
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.fetchPage(this.currentPage - 1);
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.fetchPage(page);
|
||||
}
|
||||
},
|
||||
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const total = this.totalPages;
|
||||
const current = this.currentPage;
|
||||
|
||||
if (total === 0) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (total <= 7) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
} else {
|
||||
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||
|
||||
let rangeStart;
|
||||
let rangeEnd;
|
||||
|
||||
if (current <= 4) {
|
||||
rangeStart = 2;
|
||||
rangeEnd = 5;
|
||||
} else if (current >= total - 3) {
|
||||
rangeStart = total - 4;
|
||||
rangeEnd = total - 1;
|
||||
} else {
|
||||
rangeStart = current - 1;
|
||||
rangeEnd = current + 1;
|
||||
}
|
||||
|
||||
if (rangeStart > 2) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||
}
|
||||
|
||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
|
||||
if (rangeEnd < total - 1) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||
}
|
||||
|
||||
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
});
|
||||
|
||||
Alpine.data('paginatedList',
|
||||
(items, options = {}) => ({
|
||||
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||
|
||||
init() {
|
||||
this.fetchPage(1);
|
||||
},
|
||||
|
||||
async fetchPage(page) {
|
||||
this.totalItems = this.items.length;
|
||||
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||
this.currentPage = page
|
||||
|
||||
const start = page * this.perPage - this.perPage;
|
||||
const end = page * this.perPage;
|
||||
return this.items.slice(start, end);
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||
items: [],
|
||||
currentPage: 1,
|
||||
|
||||
204
static/js/pw.js
204
static/js/pw.js
@ -1,37 +1,5 @@
|
||||
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",
|
||||
@ -55,7 +23,6 @@ function predictFromNode(url) {
|
||||
// elem = 'vizdiv'
|
||||
function draw(pathway, elem) {
|
||||
|
||||
const initialzoom = 2.5
|
||||
const nodeRadius = 20;
|
||||
const linkDistance = 100;
|
||||
const chargeStrength = -200;
|
||||
@ -67,9 +34,6 @@ function draw(pathway, elem) {
|
||||
const horizontalSpacing = 75; // horizontal space between nodes
|
||||
const depthMap = new Map();
|
||||
|
||||
// Avoid leaving unconnected Nodes leaving the viewport
|
||||
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||
|
||||
// Sort nodes by depth first to minimize crossings
|
||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||
|
||||
@ -99,7 +63,7 @@ function draw(pathway, elem) {
|
||||
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
||||
}
|
||||
|
||||
node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
|
||||
node.fy = node.depth * levelSpacing + 50;
|
||||
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||
});
|
||||
}
|
||||
@ -137,30 +101,12 @@ function draw(pathway, elem) {
|
||||
// Update pseudo node positions first
|
||||
updatePseudoNodePositions();
|
||||
|
||||
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}`;
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
|
||||
linkText
|
||||
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||
}
|
||||
|
||||
function dragstarted(event, d) {
|
||||
@ -263,7 +209,7 @@ function draw(pathway, elem) {
|
||||
}
|
||||
|
||||
// Wait before showing popup (ms)
|
||||
var popupWaitBeforeShow = 500;
|
||||
var popupWaitBeforeShow = 1000;
|
||||
|
||||
// Custom popover element
|
||||
let popoverTimeout = null;
|
||||
@ -517,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) + " days<br>"
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -617,8 +563,6 @@ 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);
|
||||
@ -637,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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -654,68 +598,43 @@ function draw(pathway, elem) {
|
||||
.on("tick", ticked);
|
||||
|
||||
// Kanten zeichnen
|
||||
const linkGroup = zoomable.append("g")
|
||||
.selectAll("g")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "link-group");
|
||||
|
||||
const link = linkGroup
|
||||
.append("path")
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||
.attr("fill", "none")
|
||||
|
||||
// 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 => {
|
||||
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")
|
||||
const link = zoomable.append("g")
|
||||
.selectAll("line")
|
||||
.data(links)
|
||||
.enter().append("line")
|
||||
// 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)')
|
||||
.on("click", function(event, d) {
|
||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
|
||||
d3.selectAll("line").classed("highlighted", false);
|
||||
|
||||
if (!wasHighlighted) {
|
||||
const toHighlight = [];
|
||||
toHighlight.push(d.el);
|
||||
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);
|
||||
if (d.source.pseudo || d.target.pseudo) {
|
||||
if (d.target.pseudo) {
|
||||
d3.selectAll("line").each(e => {
|
||||
if (e !== undefined && e.source.id === d.target.id) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3.selectAll("line").each(e => {
|
||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.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);
|
||||
}
|
||||
for (const e of toHighlight) {
|
||||
d3.select(e).classed("highlighted", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const linkText = linkGroup
|
||||
.append("text")
|
||||
.attr("class", "link-label")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", -6)
|
||||
.style("font-size", "4px")
|
||||
.style("pointer-events", "none")
|
||||
.style("display", "none")
|
||||
.text(d => d.name)
|
||||
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||
})
|
||||
|
||||
// add element to links array
|
||||
link.each(function (d) {
|
||||
@ -732,41 +651,16 @@ 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);
|
||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||
|
||||
for (const e of inEdges) {
|
||||
d3.select(e.el).classed("inedge", true);
|
||||
if (!wasHighlighted) {
|
||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||
}
|
||||
|
||||
for (const e of outEdges) {
|
||||
d3.select(e.el).classed("outedge", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Kreise für die Knoten hinzufügen
|
||||
node.append("circle")
|
||||
@ -840,14 +734,14 @@ function draw(pathway, elem) {
|
||||
|
||||
function serializeSVG(svgElement) {
|
||||
|
||||
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||
svgElement.querySelectorAll("line.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("path.link_no_arrow").forEach(line => {
|
||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
||||
const style = getComputedStyle(line);
|
||||
line.setAttribute("stroke", style.stroke);
|
||||
line.setAttribute("stroke-width", style.strokeWidth);
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,13 +0,0 @@
|
||||
{% 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,73 +3,73 @@
|
||||
{% block page_title %}Packages{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
<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"
|
||||
<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;"
|
||||
>
|
||||
<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="16"
|
||||
height="16"
|
||||
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-chevron-down ml-1"
|
||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
<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="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>
|
||||
</div>
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
|
||||
@ -37,11 +37,7 @@
|
||||
perPage: {{ per_page|default:50 }}
|
||||
})"
|
||||
>
|
||||
{% 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 %}
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -123,17 +123,6 @@
|
||||
</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'"
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
{# 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,7 +44,6 @@
|
||||
: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 === 'ml-relative-reasoning';
|
||||
return this.selectedType === 'mlrr';
|
||||
},
|
||||
|
||||
get showRbrr() {
|
||||
return this.selectedType === 'rule-based-relative-reasoning';
|
||||
return this.selectedType === 'rbrr';
|
||||
},
|
||||
|
||||
get showEnviformer() {
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
reactionImageUrl: '',
|
||||
pes: false,
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
@ -16,30 +15,20 @@
|
||||
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
||||
const productsSelect = document.getElementById('add_pathway_edge_products');
|
||||
|
||||
const pesLinks = [];
|
||||
|
||||
const substrates = [];
|
||||
for (const option of substratesSelect.selectedOptions) {
|
||||
substrates.push(option.dataset.smiles);
|
||||
if (option.dataset.pes === 'true') {
|
||||
pesLinks.push(option.dataset.pes);
|
||||
}
|
||||
}
|
||||
|
||||
const products = [];
|
||||
for (const option of productsSelect.selectedOptions) {
|
||||
products.push(option.dataset.smiles);
|
||||
if (option.dataset.pes === 'true') {
|
||||
pesLinks.push(option.dataset.pes);
|
||||
}
|
||||
}
|
||||
|
||||
if (substrates.length > 0 && products.length > 0) {
|
||||
const reaction = substrates.join('.') + '>>' + products.join('.');
|
||||
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
||||
this.pes = pesLinks.length > 0;
|
||||
} else {
|
||||
this.pes = false;
|
||||
this.reactionImageUrl = '';
|
||||
}
|
||||
},
|
||||
@ -117,7 +106,6 @@
|
||||
{% for n in pathway.nodes %}
|
||||
<option
|
||||
data-smiles="{{ n.default_node_label.smiles }}"
|
||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
||||
value="{{ n.url }}"
|
||||
>
|
||||
{{ n.default_node_label.name|safe }}
|
||||
@ -144,7 +132,6 @@
|
||||
{% for n in pathway.nodes %}
|
||||
<option
|
||||
data-smiles="{{ n.default_node_label.smiles }}"
|
||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
||||
value="{{ n.url }}"
|
||||
>
|
||||
{{ n.default_node_label.name|safe }}
|
||||
@ -157,9 +144,6 @@
|
||||
|
||||
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
||||
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
|
||||
<div x-show="pes">
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
class="modal"
|
||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||
@modal-opened.window="
|
||||
const links = d3.selectAll('path.highlighted');
|
||||
const links = d3.selectAll('line.highlighted');
|
||||
console.log(links);
|
||||
if (!links.empty()) {
|
||||
const el = links.node();
|
||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||
@ -17,7 +18,9 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
|
||||
}
|
||||
"
|
||||
@close="reset()"
|
||||
|
||||
@ -6,14 +6,17 @@
|
||||
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'));
|
||||
}
|
||||
"
|
||||
|
||||
@ -82,9 +82,6 @@
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
||||
</div>
|
||||
{% if edge.edge_label.contains_pes %}
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Reaction Description -->
|
||||
@ -95,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.get_name }}</a
|
||||
>{{ educt.name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<svg
|
||||
@ -115,7 +112,7 @@
|
||||
</svg>
|
||||
{% for product in edge.end_nodes.all %}
|
||||
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
||||
>{{ product.get_name }}</a
|
||||
>{{ product.name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@ -56,9 +56,7 @@
|
||||
<ul class="menu bg-base-200 rounded-box">
|
||||
{% for um in group.user_member.all %}
|
||||
<li>
|
||||
<a
|
||||
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
||||
class="hover:bg-base-300"
|
||||
<a href="{{ um.url }}" class="hover:bg-base-300"
|
||||
>{{ um.username }}
|
||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||
>
|
||||
|
||||
@ -55,15 +55,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
{% if node.description and node.description != "no description" %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Description</div>
|
||||
<div class="collapse-content">{{ node.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||
|
||||
{% for tpl in viz_templates %}
|
||||
|
||||
@ -21,7 +21,6 @@
|
||||
.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: 0.6;
|
||||
stroke-width: 1.5px;
|
||||
/* marker-end: url(#arrow); */
|
||||
}
|
||||
|
||||
@ -73,15 +72,6 @@
|
||||
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>
|
||||
|
||||
@ -121,7 +111,7 @@
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Graphical Representation
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="collapse-content ">
|
||||
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
||||
<div class="navbar bg-base-100 rounded-lg">
|
||||
<div class="flex-1">
|
||||
@ -178,52 +168,6 @@
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||
>
|
||||
<li>
|
||||
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
id="compound-names-icon"
|
||||
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-eye"
|
||||
>
|
||||
<path
|
||||
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Compound Names
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
id="reaction-names-icon"
|
||||
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-eye"
|
||||
>
|
||||
<path
|
||||
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Reaction Names
|
||||
</a>
|
||||
</li>
|
||||
{% if pathway.setting.model.app_domain %}
|
||||
<li>
|
||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||
@ -249,7 +193,6 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 1 == 0 %}
|
||||
<li>
|
||||
<a id="timeseries-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
@ -300,7 +243,6 @@
|
||||
Show Predicted Properties
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -425,18 +367,6 @@
|
||||
>
|
||||
<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"
|
||||
@ -544,10 +474,6 @@
|
||||
{{ pathway.d3_json|json_script:"pathway" }}
|
||||
|
||||
<script>
|
||||
// Global switch for compound names view
|
||||
var compoundNamesViewEnabled = false;
|
||||
// Gloabl switch for reaction names view
|
||||
var reactionNamesViewEnabled = false;
|
||||
// Global switch for app domain view
|
||||
var appDomainViewEnabled = false;
|
||||
// Global switch for timeseries view
|
||||
@ -583,67 +509,6 @@
|
||||
descContent.innerHTML = newDesc;
|
||||
}
|
||||
|
||||
const compoundNamesBtn = document.getElementById("compound-names-toggle-button");
|
||||
if (compoundNamesBtn) {
|
||||
compoundNamesBtn.addEventListener("click", function () {
|
||||
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||
const icon = document.getElementById("compound-names-icon");
|
||||
|
||||
if (compoundNamesViewEnabled) {
|
||||
// Change to eye-off icon
|
||||
icon.innerHTML =
|
||||
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||
|
||||
nodes.forEach((x) => {
|
||||
if (x.name) {
|
||||
d3.select(x.el)
|
||||
.append("text")
|
||||
.text(d => d.name)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("y", -20)
|
||||
.style("font-size", "4px");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Change back to eye icon
|
||||
icon.innerHTML =
|
||||
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||
|
||||
nodes.forEach((x) => {
|
||||
d3.select(x.el)
|
||||
.select("text")
|
||||
.remove()
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const reactionNamesBtn = document.getElementById("reaction-names-toggle-button");
|
||||
if (reactionNamesBtn) {
|
||||
reactionNamesBtn.addEventListener("click", function () {
|
||||
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||
const icon = document.getElementById("reaction-names-icon");
|
||||
|
||||
if (reactionNamesViewEnabled) {
|
||||
// Change to eye-off icon
|
||||
icon.innerHTML =
|
||||
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||
|
||||
d3.selectAll(".link-label")
|
||||
.style("display", null);
|
||||
|
||||
} else {
|
||||
// Change back to eye icon
|
||||
icon.innerHTML =
|
||||
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||
|
||||
d3.selectAll(".link-label")
|
||||
.style("display", "none");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App domain toggle
|
||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||
if (appDomainBtn) {
|
||||
|
||||
@ -79,9 +79,6 @@
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
||||
</div>
|
||||
{% if reaction.contains_pes %}
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Reaction Description -->
|
||||
|
||||
@ -24,76 +24,6 @@
|
||||
<td>Setting Name</td>
|
||||
<td>{{ setting_to_render.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Setting URL</td>
|
||||
<td>
|
||||
<a href="{{ setting_to_render.url }}" class="link link-primary">{{ setting_to_render.url }}</a>
|
||||
<div
|
||||
x-data="{
|
||||
value: '{{ setting_to_render.url }}',
|
||||
copied: false,
|
||||
|
||||
async copy() {
|
||||
await navigator.clipboard.writeText(this.value)
|
||||
this.copied = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
}, 1500)
|
||||
}
|
||||
}"
|
||||
class="join"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
:data-tip="copied ? 'Copied!' : 'Copy'"
|
||||
@click="copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg
|
||||
x-show="!copied"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2"
|
||||
/>
|
||||
<rect
|
||||
width="12"
|
||||
height="12"
|
||||
x="8"
|
||||
y="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
x-show="copied"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% if setting_to_render.description %}
|
||||
<tr>
|
||||
<td>Setting Description</td>
|
||||
|
||||
@ -105,7 +105,7 @@
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<label class="select mb-8 w-full" id="prediction-setting-label">
|
||||
<label class="select mb-8 w-full">
|
||||
<span class="label">Predictor</span>
|
||||
<select id="prediction-setting" name="prediction-setting">
|
||||
<option disabled>Select a Setting</option>
|
||||
@ -148,21 +148,6 @@
|
||||
</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);
|
||||
@ -212,13 +197,7 @@
|
||||
e.preventDefault();
|
||||
const button = this;
|
||||
button.disabled = true;
|
||||
|
||||
// Set text depending on mode
|
||||
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
||||
button.textContent = "Building...";
|
||||
} else {
|
||||
button.textContent = "Predicting...";
|
||||
}
|
||||
button.textContent = "Predicting...";
|
||||
|
||||
// Get SMILES from either input or Ketcher
|
||||
const smilesInput = document.getElementById("predict-smiles");
|
||||
|
||||
@ -110,6 +110,8 @@
|
||||
<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>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
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
|
||||
|
||||
@ -35,13 +33,13 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(c.description, "No Desc")
|
||||
|
||||
def test_missing_smiles(self):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||
|
||||
def test_smiles_are_trimmed(self):
|
||||
@ -98,7 +96,7 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(len(self.package.compounds), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = 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,7 +1,6 @@
|
||||
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
|
||||
|
||||
@ -164,7 +163,7 @@ class ReactionTest(TestCase):
|
||||
self.assertEqual(len(self.package.reactions), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
with self.assertRaises(ValueError):
|
||||
_ = Reaction.create(
|
||||
package=self.package,
|
||||
name="Eawag BBD reaction r0001",
|
||||
|
||||
@ -46,14 +46,6 @@ 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,
|
||||
@ -70,6 +62,14 @@ 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"]
|
||||
|
||||
@ -125,7 +125,6 @@ from envipy_additional_information.parsers import (
|
||||
LiquidMatrixSourceParser,
|
||||
OxygenUptakeRateParser,
|
||||
InitiatingOrganismParser,
|
||||
PFASConfidenceParser,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -142,7 +141,7 @@ def get_parameter(request, paramname):
|
||||
res = request.POST.get(paramname)
|
||||
if res is not None and res.strip() != "":
|
||||
return res
|
||||
raise ValueError("Not all parameters are set!")
|
||||
return ValueError("Not all parameters are set!")
|
||||
|
||||
|
||||
def get_parameter_or_empty_string(request, paramname):
|
||||
@ -674,8 +673,7 @@ 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!")
|
||||
|
||||
|
||||
1497
utilities/misc.py
1497
utilities/misc.py
File diff suppressed because it is too large
Load Diff
2
uv.lock
generated
2
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#ad825570480bbe2f1a35c04923fede756c450751" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#676dae1c5678539beac637b87e49b9dadfdfd85a" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user