forked from enviPath/enviPy
Compare commits
5 Commits
develop-ba
...
e3876ac945
| Author | SHA1 | Date | |
|---|---|---|---|
| e3876ac945 | |||
| ad1e575e4c | |||
| 15c23a2151 | |||
| 72399b16b3 | |||
| 54056c654d |
13
Dockerfile
13
Dockerfile
@ -6,23 +6,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
curl \
|
curl \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
git \
|
git \
|
||||||
ca-certificates \
|
nodejs \
|
||||||
|
npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Node 22 + pnpm
|
# Install pnpm
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
RUN npm install -g pnpm
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& corepack enable \
|
|
||||||
&& corepack prepare pnpm@latest --activate \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
ENV PATH="/root/.local/bin:${PATH}"
|
ENV PATH="/root/.local/bin:${PATH}"
|
||||||
|
|||||||
@ -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
|
import logging
|
||||||
|
|
||||||
from bayer import additional_information # noqa: F401
|
|
||||||
from epdb.template_registry import register_template
|
from epdb.template_registry import register_template
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -37,4 +36,4 @@ register_template(
|
|||||||
register_template(
|
register_template(
|
||||||
"epdb.objects.node.viz",
|
"epdb.objects.node.viz",
|
||||||
"objects/node_viz.html",
|
"objects/node_viz.html",
|
||||||
)
|
)
|
||||||
@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
|
|||||||
def create(
|
def create(
|
||||||
compound: Compound,
|
compound: Compound,
|
||||||
pes_link: str,
|
pes_link: str,
|
||||||
molfile: str,
|
mol_file: str,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
|
|||||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
cs.smiles = smiles
|
cs.smiles = smiles
|
||||||
cs.molfile = molfile
|
cs.mol_file = mol_file
|
||||||
cs.pes_link = pes_link
|
cs.pes_link = pes_link
|
||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
|
||||||
@ -232,6 +232,5 @@ class PESStructure(CompoundStructure):
|
|||||||
"is_pes": True,
|
"is_pes": True,
|
||||||
"pes_link": self.pes_link,
|
"pes_link": self.pes_link,
|
||||||
# Will overwrite image from Node
|
# Will overwrite image from Node
|
||||||
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
"image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}"
|
||||||
"image_type": "png",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,7 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||||
<p>
|
|
||||||
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -3,11 +3,7 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||||
<p>
|
|
||||||
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -3,11 +3,7 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||||
<p>
|
|
||||||
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import base64
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings as s
|
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 django.shortcuts import redirect
|
||||||
|
|
||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Pathway, Node
|
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
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
@ -22,11 +23,7 @@ def create_pes(request, package_uuid):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
return error(
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
request,
|
|
||||||
f'Creation of PESs for package {current_package.name} failed!',
|
|
||||||
"Creating PESs for internal packages is not allowed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
compound_name = request.POST.get('compound-name')
|
compound_name = request.POST.get('compound-name')
|
||||||
compound_description = request.POST.get('compound-description')
|
compound_description = request.POST.get('compound-description')
|
||||||
@ -36,25 +33,21 @@ def create_pes(request, package_uuid):
|
|||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link)
|
||||||
except ValueError as e:
|
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", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
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")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in 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")
|
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)
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
return redirect(pes.url)
|
return redirect(pes.url)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest("Please provide a PES link.")
|
return BadRequest("Please provide a PES link.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -68,11 +61,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
return error(
|
raise BadRequest("Cannot create PESs for internal packages.")
|
||||||
request,
|
|
||||||
f'Creation of PESs for package {current_package.name} failed!',
|
|
||||||
"Creating PESs for internal packages is not allowed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
compound_name = request.POST.get('compound-name')
|
compound_name = request.POST.get('compound-name')
|
||||||
compound_description = request.POST.get('compound-description')
|
compound_description = request.POST.get('compound-description')
|
||||||
@ -82,26 +71,18 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link)
|
||||||
except ValueError as e:
|
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", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
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")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in 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")
|
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)
|
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 = Node()
|
||||||
n.stereo_removed = False
|
n.stereo_removed = False
|
||||||
n.pathway = current_pathway
|
n.pathway = current_pathway
|
||||||
@ -116,7 +97,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest("Please provide a PES link.")
|
return BadRequest("Please provide a PES link.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -125,9 +106,6 @@ def fetch_pes(request, pes_url) -> dict:
|
|||||||
from epauth.views import get_access_token_from_request
|
from epauth.views import get_access_token_from_request
|
||||||
token = get_access_token_from_request(request)
|
token = get_access_token_from_request(request)
|
||||||
|
|
||||||
if token is None:
|
|
||||||
token = pes_url.split('/')[-1] == 'dummy'
|
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
for k, v in s.PES_API_MAPPING.items():
|
for k, v in s.PES_API_MAPPING.items():
|
||||||
if pes_url.startswith(k):
|
if pes_url.startswith(k):
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import enum
|
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
import enum
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings as s
|
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.contracts import Classifier # noqa: I001
|
||||||
from bridge.dto import (
|
from bridge.dto import (
|
||||||
@ -18,9 +17,6 @@ from bridge.dto import (
|
|||||||
TransformationProductPrediction,
|
TransformationProductPrediction,
|
||||||
) # noqa: I001
|
) # noqa: I001
|
||||||
|
|
||||||
logger = logging.getLogger("epdb")
|
|
||||||
|
|
||||||
|
|
||||||
class SamplingAlgorithm(enum.Enum):
|
class SamplingAlgorithm(enum.Enum):
|
||||||
EXACT = "exact"
|
EXACT = "exact"
|
||||||
|
|
||||||
@ -89,13 +85,14 @@ class BB4G(Classifier):
|
|||||||
}
|
}
|
||||||
|
|
||||||
started = False
|
started = False
|
||||||
|
retries = 0
|
||||||
while not started:
|
while not started and retries < 5:
|
||||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
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:
|
if res.status_code == 200:
|
||||||
started = True
|
started = True
|
||||||
elif res.status_code in [500, 502]:
|
elif res.status_code in [500, 502]:
|
||||||
|
retries += 1
|
||||||
import time
|
import time
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
else:
|
else:
|
||||||
@ -169,30 +166,18 @@ class BB4G(Classifier):
|
|||||||
"cutoff": self.config.cutoff,
|
"cutoff": self.config.cutoff,
|
||||||
}
|
}
|
||||||
|
|
||||||
retries = 0
|
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
|
||||||
while retries < 100:
|
|
||||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
|
|
||||||
proxies=s.PROXIES or None)
|
|
||||||
|
|
||||||
if resp.status_code == 418:
|
resp.raise_for_status()
|
||||||
retries += 1
|
|
||||||
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
|
|
||||||
import time
|
|
||||||
time.sleep(3)
|
|
||||||
continue
|
|
||||||
|
|
||||||
resp.raise_for_status()
|
for substrate, predictions in resp.json().items():
|
||||||
|
preds = {}
|
||||||
|
|
||||||
for substrate, predictions in resp.json().items():
|
for pred in predictions:
|
||||||
preds = {}
|
prod = pred["prediction"]
|
||||||
|
prob = math.exp(pred["log_likelihood"])
|
||||||
|
preds[prod] = prob
|
||||||
|
|
||||||
for pred in predictions:
|
result[substrate] = preds
|
||||||
prod = pred["prediction"]
|
|
||||||
prob = math.exp(pred["log_likelihood"])
|
|
||||||
preds[prod] = prob
|
|
||||||
|
|
||||||
result[substrate] = preds
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -261,6 +261,7 @@ class Classifier(Plugin):
|
|||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
if v != "":
|
if v != "":
|
||||||
cpy[k] = v
|
cpy[k] = v
|
||||||
|
|
||||||
return cls.Config(**cpy)
|
return cls.Config(**cpy)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -25,14 +25,25 @@ services:
|
|||||||
- ep_bayer_redis_data:/data
|
- ep_bayer_redis_data:/data
|
||||||
|
|
||||||
biotransformer3:
|
biotransformer3:
|
||||||
image: git.envipath.com/envipath/biotransformer3:1.0
|
image: envipath/biotransformer3:1.0
|
||||||
container_name: epbiotransformer3
|
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:
|
celery_worker:
|
||||||
image: git.envipath.com/envipath/envipy-bayer:1.2
|
image: envipath/envipy-bayer:1.0
|
||||||
container_name: epcelery
|
container_name: epcelery
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env.dev
|
||||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||||
volumes:
|
volumes:
|
||||||
- ep_bayer_data:/opt/enviPy/
|
- ep_bayer_data:/opt/enviPy/
|
||||||
|
|||||||
@ -275,12 +275,6 @@ LOGGING = {
|
|||||||
"filename": os.path.join(LOG_DIR, "debug.log"),
|
"filename": os.path.join(LOG_DIR, "debug.log"),
|
||||||
"formatter": "simple",
|
"formatter": "simple",
|
||||||
},
|
},
|
||||||
"auth_file": {
|
|
||||||
"level": "INFO", # Or higher
|
|
||||||
"class": "logging.FileHandler",
|
|
||||||
"filename": os.path.join(LOG_DIR, "auth.log"),
|
|
||||||
"formatter": "simple",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
# For everything under epdb/ loaded via getlogger(__name__)
|
# For everything under epdb/ loaded via getlogger(__name__)
|
||||||
@ -301,11 +295,6 @@ LOGGING = {
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
"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,
|
"num_chains": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_MAX_NUMBER_OF_NODES = 9999
|
DEFAULT_MAX_NUMBER_OF_NODES = 50
|
||||||
DEFAULT_MAX_DEPTH = 8
|
DEFAULT_MAX_DEPTH = 8
|
||||||
DEFAULT_MODEL_THRESHOLD = 0.25
|
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_ID = os.environ.get("BB4G_CLIENT_ID")
|
||||||
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET")
|
||||||
BB4G_SCOPE = os.environ.get("BB4G_SCOPE")
|
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,
|
additional_information,
|
||||||
settings,
|
settings,
|
||||||
groups,
|
groups,
|
||||||
joblogs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main router with authentication
|
# Main router with authentication
|
||||||
@ -38,7 +37,6 @@ router.add_router("", structure.router)
|
|||||||
router.add_router("", additional_information.router)
|
router.add_router("", additional_information.router)
|
||||||
router.add_router("", settings.router)
|
router.add_router("", settings.router)
|
||||||
router.add_router("", groups.router)
|
router.add_router("", groups.router)
|
||||||
router.add_router("", joblogs.router)
|
|
||||||
|
|
||||||
if s.IUCLID_EXPORT_ENABLED:
|
if s.IUCLID_EXPORT_ENABLED:
|
||||||
from epiuclid.api import router as iuclid_router
|
from epiuclid.api import router as iuclid_router
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
from datetime import datetime
|
from ninja import FilterSchema, FilterLookup, Schema
|
||||||
from typing import Annotated, Optional, List, Dict, Any
|
from typing import Annotated, Optional, List, Dict, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from django.urls import reverse
|
|
||||||
from ninja import Field, FilterSchema, FilterLookup, Schema
|
|
||||||
|
|
||||||
|
|
||||||
# Filter schema for query parameters
|
# Filter schema for query parameters
|
||||||
class ReviewStatusFilter(FilterSchema):
|
class ReviewStatusFilter(FilterSchema):
|
||||||
@ -136,23 +133,3 @@ class GroupOutSchema(Schema):
|
|||||||
url: str = ""
|
url: str = ""
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
|
|
||||||
class SimpleUserOutSchema(Schema):
|
|
||||||
uuid: UUID
|
|
||||||
url: str
|
|
||||||
name: str = Field(alias="username")
|
|
||||||
|
|
||||||
|
|
||||||
class JobLogOutSchema(Schema):
|
|
||||||
user: SimpleUserOutSchema
|
|
||||||
id: UUID = Field(alias="task_id")
|
|
||||||
url: str
|
|
||||||
name: str = Field(alias="job_name")
|
|
||||||
created: datetime = Field(alias="created")
|
|
||||||
status: str = Field(alias="status")
|
|
||||||
done: Optional[datetime] = Field(None, alias="done_at")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_url(obj):
|
|
||||||
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
class InvalidSMILESException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class PackageImportException(Exception):
|
|
||||||
pass
|
|
||||||
@ -439,50 +439,23 @@ class PackageSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_readers(obj: Package):
|
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)
|
return [{u.id: u.get_name()} for u in users]
|
||||||
|
|
||||||
users = User.objects.filter(id__in=user_ids).distinct()
|
|
||||||
|
|
||||||
for u in users:
|
|
||||||
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
|
||||||
|
|
||||||
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
|
|
||||||
"group", flat=True
|
|
||||||
)
|
|
||||||
|
|
||||||
groups = Group.objects.filter(id__in=group_ids).distinct()
|
|
||||||
|
|
||||||
for g in groups:
|
|
||||||
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
|
||||||
|
|
||||||
return readers
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_writers(obj: Package):
|
def resolve_writers(obj: Package):
|
||||||
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(
|
return [{u.id: u.get_name()} for u in users]
|
||||||
package=obj,
|
|
||||||
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
|
|
||||||
).values_list("user", flat=True)
|
|
||||||
|
|
||||||
users = User.objects.filter(id__in=user_ids).distinct()
|
|
||||||
|
|
||||||
for u in users:
|
|
||||||
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
|
||||||
|
|
||||||
group_ids = GroupPackagePermission.objects.filter(
|
|
||||||
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
|
|
||||||
).values_list("group", flat=True)
|
|
||||||
|
|
||||||
groups = Group.objects.filter(id__in=group_ids).distinct()
|
|
||||||
|
|
||||||
for g in groups:
|
|
||||||
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
|
||||||
|
|
||||||
return writers
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_comment(obj):
|
def resolve_review_comment(obj):
|
||||||
@ -633,14 +606,9 @@ class CompoundSchema(Schema):
|
|||||||
reviewStatus: str = Field(False, alias="review_status")
|
reviewStatus: str = Field(False, alias="review_status")
|
||||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||||
structures: List["CompoundStructureSchema"] = []
|
structures: List["CompoundStructureSchema"] = []
|
||||||
pesLink: str | None = Field(None, alias="pes_link")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_pes_link(obj: Compound):
|
def resolve_review_status(obj: CompoundStructure):
|
||||||
return getattr(obj.default_structure, "pes_link", None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_review_status(obj: Compound):
|
|
||||||
return "reviewed" if obj.package.reviewed else "unreviewed"
|
return "reviewed" if obj.package.reviewed else "unreviewed"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -714,7 +682,6 @@ class CompoundStructureSchema(Schema):
|
|||||||
reviewStatus: str = Field(None, alias="review_status")
|
reviewStatus: str = Field(None, alias="review_status")
|
||||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||||
smiles: str = Field(None, alias="smiles")
|
smiles: str = Field(None, alias="smiles")
|
||||||
pesLink: str | None = Field(None, alias="pes_link")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: CompoundStructure):
|
def resolve_review_status(obj: CompoundStructure):
|
||||||
@ -877,10 +844,6 @@ def create_package_compound(
|
|||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
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")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in 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(
|
@router.delete(
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||||
)
|
)
|
||||||
@ -1456,7 +1398,6 @@ class ScenarioSchema(Schema):
|
|||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||||
collectionID: Optional[str] = None
|
collectionID: Optional[str] = None
|
||||||
date: str = Field(None, alias="scenario_date")
|
|
||||||
description: str = Field(None, alias="description")
|
description: str = Field(None, alias="description")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
identifier: str = "scenario"
|
identifier: str = "scenario"
|
||||||
@ -1600,56 +1541,28 @@ def create_package_additional_information(request, package_uuid):
|
|||||||
scen = request.POST.get("scenario")
|
scen = request.POST.get("scenario")
|
||||||
scenario = Scenario.objects.get(package=p, url=scen)
|
scenario = Scenario.objects.get(package=p, url=scen)
|
||||||
|
|
||||||
if request.POST.get("adInfoTypes[]"):
|
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
attach_obj = url_parser.get_object()
|
||||||
attach_obj = url_parser.get_object()
|
|
||||||
|
|
||||||
if not hasattr(attach_obj, "additional_information"):
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
raise ValueError("Can't attach additional information to this object!")
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
if not attach_obj.url.startswith(p.url):
|
if not attach_obj.url.startswith(p.url):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Additional Information can only be set to objects stored in the same package!"
|
"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:
|
for t in types:
|
||||||
ai = build_additional_information_from_request(request, t)
|
ai = build_additional_information_from_request(request, t)
|
||||||
|
|
||||||
AdditionalInformation.create(
|
AdditionalInformation.create(
|
||||||
p,
|
p,
|
||||||
ai,
|
ai,
|
||||||
scenario=scenario,
|
scenario=scenario,
|
||||||
content_object=attach_obj,
|
content_object=attach_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif request.POST.get("ais"):
|
|
||||||
import json
|
|
||||||
|
|
||||||
parsed_ais = json.loads(request.POST.get("ais"))
|
|
||||||
|
|
||||||
for ai_type, ais in parsed_ais.items():
|
|
||||||
for ai in ais:
|
|
||||||
attach_obj = None
|
|
||||||
if ai.get("related"):
|
|
||||||
url_parser = EPDBURLParser(ai.get("related").get("url"))
|
|
||||||
attach_obj = url_parser.get_object()
|
|
||||||
|
|
||||||
if not hasattr(attach_obj, "additional_information"):
|
|
||||||
raise ValueError("Can't attach additional information to this object!")
|
|
||||||
|
|
||||||
if not attach_obj.url.startswith(p.url):
|
|
||||||
raise ValueError(
|
|
||||||
"Additional Information can only be set to objects stored in the same package!"
|
|
||||||
)
|
|
||||||
|
|
||||||
AdditionalInformation.create(
|
|
||||||
p,
|
|
||||||
AdditionalInformation.from_dict(ai_type, ai),
|
|
||||||
scenario=scenario,
|
|
||||||
content_object=attach_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO implement additional information endpoint ?
|
# TODO implement additional information endpoint ?
|
||||||
return redirect(f"{scenario.url}")
|
return redirect(f"{scenario.url}")
|
||||||
@ -1693,15 +1606,13 @@ class PathwayNode(Schema):
|
|||||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
idcomp: str = Field(None, alias="node_label_id")
|
idcomp: str = Field(None, alias="default_node_label.url")
|
||||||
idreact: str = Field(None, alias="node_label_id")
|
idreact: str = Field(None, alias="default_node_label.url")
|
||||||
image: str = Field(None, alias="image")
|
image: str = Field(None, alias="image")
|
||||||
imageSize: int = Field(None, alias="image_size")
|
imageSize: int = Field(None, alias="image_size")
|
||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||||
smiles: str = Field(None, alias="smiles")
|
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||||
pseudo: bool = Field(False, alias="pseudo")
|
|
||||||
pesLink: str | None = Field(None, alias="pes_link")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_atom_count(obj: Node):
|
def resolve_atom_count(obj: Node):
|
||||||
@ -1714,10 +1625,24 @@ class PathwayNode(Schema):
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_engineered_intermediate(obj: Node):
|
||||||
|
# TODO
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_image(obj: Node):
|
||||||
|
return f"{obj.default_node_label.url}?image=svg"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_image_size(obj: Node):
|
def resolve_image_size(obj: Node):
|
||||||
return 400
|
return 400
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_proposed_intermediate(obj: Node):
|
||||||
|
# TODO
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class PathwaySchema(Schema):
|
class PathwaySchema(Schema):
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
@ -1841,29 +1766,6 @@ def create_package_pathway(
|
|||||||
return 403, {"message": str(e)}
|
return 403, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
|
||||||
def update_pathway(request, package_uuid, pathway_uuid):
|
|
||||||
try:
|
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
|
||||||
|
|
||||||
if request.POST.get("scenario"):
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
|
|
||||||
|
|
||||||
pw.scenarios.add(scen)
|
|
||||||
pw.save()
|
|
||||||
|
|
||||||
return redirect(f"{pw.url}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
return 400, {"message": "No scenario specified!"}
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||||
try:
|
try:
|
||||||
@ -1977,42 +1879,30 @@ class CreateNode(Schema):
|
|||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
||||||
response={200: str | Any, 400: Error, 403: Error},
|
response={200: str | Any, 403: Error},
|
||||||
)
|
)
|
||||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||||
|
|
||||||
# TODO Code Dup from bayer.views
|
|
||||||
|
|
||||||
if n.pesLink:
|
if n.pesLink:
|
||||||
from bayer.views import fetch_pes
|
from bayer.views import fetch_pes
|
||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, n.pesLink)
|
pes_data = fetch_pes(request, c.pesLink)
|
||||||
except ValueError as e:
|
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", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
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")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||||
return 400, {
|
return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"}
|
||||||
"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)
|
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||||
|
|
||||||
node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure)
|
|
||||||
if node_qs.exists():
|
|
||||||
return redirect(pw.url)
|
|
||||||
|
|
||||||
node = Node()
|
node = Node()
|
||||||
node.stereo_removed = False
|
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(","):
|
for pr in e.products.split(","):
|
||||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
||||||
|
|
||||||
multi_step = False
|
|
||||||
if e.multistep and e.multistep.strip() == "true":
|
|
||||||
multi_step = True
|
|
||||||
|
|
||||||
new_e = Edge.create(
|
new_e = Edge.create(
|
||||||
pathway=pw,
|
pathway=pw,
|
||||||
start_nodes=educts,
|
start_nodes=educts,
|
||||||
@ -2186,12 +2072,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|||||||
rule=None,
|
rule=None,
|
||||||
name=None,
|
name=None,
|
||||||
description=e.edgeReason,
|
description=e.edgeReason,
|
||||||
multi_step=multi_step,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update depths as sideeffect of above operation
|
|
||||||
pw.update_depths()
|
|
||||||
|
|
||||||
return redirect(new_e.url)
|
return redirect(new_e.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {"message": "Adding Edge failed!"}
|
return 403, {"message": "Adding Edge failed!"}
|
||||||
@ -2385,31 +2267,3 @@ def get_setting(request, setting_uuid):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
########
|
|
||||||
# Util #
|
|
||||||
########
|
|
||||||
class NonPersistent(Schema):
|
|
||||||
smiles: str
|
|
||||||
setting_url: str = Field(..., alias="settingUri")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/util", response={200: Any, 403: Error})
|
|
||||||
def predict(request, np: Form[NonPersistent]):
|
|
||||||
try:
|
|
||||||
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!"
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1065,9 +1065,52 @@ class PackageManager(object):
|
|||||||
|
|
||||||
print("Fixing Node depths...")
|
print("Fixing Node depths...")
|
||||||
total_pws = Pathway.objects.filter(package=pack).count()
|
total_pws = Pathway.objects.filter(package=pack).count()
|
||||||
|
|
||||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||||
pw.update_depths()
|
in_count = defaultdict(lambda: 0)
|
||||||
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
for e in pw.edges:
|
||||||
|
# TODO check if this will remain
|
||||||
|
for react in e.start_nodes.all():
|
||||||
|
out_count[str(react.uuid)] += 1
|
||||||
|
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
in_count[str(prod.uuid)] += 1
|
||||||
|
|
||||||
|
root_nodes = []
|
||||||
|
for n in pw.nodes:
|
||||||
|
num_parents = in_count[str(n.uuid)]
|
||||||
|
if num_parents == 0:
|
||||||
|
# must be a root node or unconnected node
|
||||||
|
if n.depth != 0:
|
||||||
|
n.depth = 0
|
||||||
|
n.save()
|
||||||
|
|
||||||
|
# Only root node may have children
|
||||||
|
if out_count[str(n.uuid)] > 0:
|
||||||
|
root_nodes.append(n)
|
||||||
|
|
||||||
|
levels = [root_nodes]
|
||||||
|
seen = set()
|
||||||
|
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
|
||||||
|
for i, level_nodes in enumerate(levels):
|
||||||
|
new_level = []
|
||||||
|
for n in level_nodes:
|
||||||
|
for e in n.out_edges.all():
|
||||||
|
for prod in e.end_nodes.all():
|
||||||
|
if str(prod.uuid) not in seen:
|
||||||
|
old_depth = prod.depth
|
||||||
|
if old_depth != i + 1:
|
||||||
|
prod.depth = i + 1
|
||||||
|
prod.save()
|
||||||
|
|
||||||
|
new_level.append(prod)
|
||||||
|
|
||||||
|
seen.add(str(n.uuid))
|
||||||
|
|
||||||
|
if new_level:
|
||||||
|
levels.append(new_level)
|
||||||
|
|
||||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||||
|
|
||||||
return pack
|
return pack
|
||||||
@ -1078,8 +1121,10 @@ class PackageManager(object):
|
|||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
owner: User,
|
owner: User,
|
||||||
preserve_uuids=False,
|
preserve_uuids=False,
|
||||||
|
add_import_timestamp=True,
|
||||||
|
trust_reviewed=False,
|
||||||
) -> Package:
|
) -> Package:
|
||||||
importer = PackageImporter(data, preserve_uuids)
|
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
||||||
imported_package = importer.do_import()
|
imported_package = importer.do_import()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@ -1896,12 +1941,6 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
if edge.rule:
|
|
||||||
e["rule"] = edge.rule.simple_json()
|
|
||||||
|
|
||||||
if edge.probability:
|
|
||||||
e["probability"] = edge.probability
|
|
||||||
|
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
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.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||||
new_license.save()
|
new_license.save()
|
||||||
|
|
||||||
def import_package(self, data, owner, all_envipath_user_group):
|
def import_package(self, data, owner):
|
||||||
p = PackageManager.import_legacy_package(
|
return PackageManager.import_legacy_package(
|
||||||
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
||||||
)
|
)
|
||||||
|
|
||||||
PackageManager.grant_read(owner, p, all_envipath_user_group)
|
|
||||||
|
|
||||||
return p
|
|
||||||
|
|
||||||
def create_default_setting(self, owner, packages):
|
def create_default_setting(self, owner, packages):
|
||||||
s = SettingManager.create_setting(
|
s = SettingManager.create_setting(
|
||||||
owner,
|
owner,
|
||||||
@ -202,7 +198,7 @@ class Command(BaseCommand):
|
|||||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||||
).read()
|
).read()
|
||||||
)
|
)
|
||||||
imported_package = self.import_package(package_data, admin, g)
|
imported_package = self.import_package(package_data, admin)
|
||||||
mapping[p.replace(".json", "")] = imported_package
|
mapping[p.replace(".json", "")] = imported_package
|
||||||
|
|
||||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
|
||||||
|
|
||||||
MAPPING = {
|
|
||||||
"": HalfLifeModel.OTHER,
|
|
||||||
"HS-SFO": HalfLifeModel.HS_SFO,
|
|
||||||
"FOMC": HalfLifeModel.FOMC,
|
|
||||||
"FOTC": HalfLifeModel.DFOP,
|
|
||||||
"FMOC": HalfLifeModel.FOMC,
|
|
||||||
"DFOP": HalfLifeModel.DFOP,
|
|
||||||
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
|
||||||
"first order kinetics": HalfLifeModel.SFO,
|
|
||||||
"SFO²": HalfLifeModel.SFO,
|
|
||||||
"HS": HalfLifeModel.HS,
|
|
||||||
"top down": HalfLifeModel.OTHER,
|
|
||||||
"SFO": HalfLifeModel.SFO,
|
|
||||||
"First Order": HalfLifeModel.SFO,
|
|
||||||
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
|
||||||
"true": HalfLifeModel.SFO,
|
|
||||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
|
||||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
|
||||||
"other": HalfLifeModel.OTHER,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def forward_func(apps, schema_editor):
|
|
||||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
|
||||||
|
|
||||||
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
|
||||||
|
|
||||||
for hl in hls:
|
|
||||||
data = hl.data
|
|
||||||
data["model"] = MAPPING[data["model"]].value
|
|
||||||
hl.data = HalfLife(**data).model_dump(mode="json")
|
|
||||||
hl.save()
|
|
||||||
|
|
||||||
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
|
||||||
|
|
||||||
for hl in hlws:
|
|
||||||
data = hl.data
|
|
||||||
data["model"] = MAPPING[data["model"]].value
|
|
||||||
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
|
||||||
hl.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0024_user_contacted"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
||||||
@ -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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
235
epdb/models.py
235
epdb/models.py
@ -31,7 +31,6 @@ from sklearn.model_selection import ShuffleSplit
|
|||||||
|
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from bridge.dto import RunResult, PropertyPrediction
|
from bridge.dto import RunResult, PropertyPrediction
|
||||||
from epdb.exceptions import InvalidSMILESException
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
ApplicabilityDomainPCA,
|
||||||
@ -577,10 +576,6 @@ class ReactionIdentifierMixin(ExternalIdentifierMixin):
|
|||||||
def get_uniprot_identifiers(self):
|
def get_uniprot_identifiers(self):
|
||||||
return self.get_external_identifier("UniProt")
|
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 #
|
# EP Objects #
|
||||||
@ -637,7 +632,7 @@ class EnviPathModel(TimeStampedModel):
|
|||||||
|
|
||||||
class AliasMixin(models.Model):
|
class AliasMixin(models.Model):
|
||||||
aliases = ArrayField(
|
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
|
@transaction.atomic
|
||||||
@ -660,9 +655,7 @@ class AliasMixin(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ScenarioMixin(models.Model):
|
class ScenarioMixin(models.Model):
|
||||||
scenarios = models.ManyToManyField(
|
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
||||||
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||||
@ -788,7 +781,7 @@ class Compound(
|
|||||||
"CompoundStructure",
|
"CompoundStructure",
|
||||||
verbose_name="Default Structure",
|
verbose_name="Default Structure",
|
||||||
related_name="compound_default_structure",
|
related_name="compound_default_structure",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -865,17 +858,13 @@ class Compound(
|
|||||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
if smiles is None or smiles.strip() == "":
|
if smiles is None or smiles.strip() == "":
|
||||||
raise InvalidSMILESException("SMILES is required")
|
raise ValueError("SMILES is required")
|
||||||
|
|
||||||
smiles = smiles.strip()
|
smiles = smiles.strip()
|
||||||
|
|
||||||
parsed = FormatConverter.from_smiles(smiles)
|
parsed = FormatConverter.from_smiles(smiles)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
raise InvalidSMILESException("Given SMILES is invalid")
|
raise ValueError("Given SMILES is invalid")
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
# Clean for potential XSS
|
|
||||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
|
|
||||||
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
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
|
# Check if we find a direct match for a given SMILES
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
found_structure = qs.first()
|
return qs.first().compound
|
||||||
found_compound = found_structure.compound
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_structure.add_alias(name)
|
|
||||||
found_compound.add_alias(name)
|
|
||||||
|
|
||||||
return found_compound
|
|
||||||
|
|
||||||
|
|
||||||
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||||
@ -904,19 +886,16 @@ class Compound(
|
|||||||
# Check if we can find the standardized one
|
# Check if we can find the standardized one
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
# TODO should we add a structure?
|
# TODO should we add a structure?
|
||||||
found_structure = qs.first()
|
return qs.first().compound
|
||||||
found_compound = found_structure.compound
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_structure.add_alias(name)
|
|
||||||
found_compound.add_alias(name)
|
|
||||||
|
|
||||||
return found_compound
|
|
||||||
|
|
||||||
# Generate Compound
|
# Generate Compound
|
||||||
c = Compound()
|
c = Compound()
|
||||||
c.package = package
|
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 == "":
|
if name is None or name == "":
|
||||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
@ -1167,27 +1146,18 @@ class CompoundStructure(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
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():
|
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
|
||||||
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||||
|
|
||||||
if name:
|
|
||||||
found_cs.add_alias(name)
|
|
||||||
|
|
||||||
return found_cs
|
|
||||||
|
|
||||||
if compound.pk is None:
|
if compound.pk is None:
|
||||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||||
|
|
||||||
cs = CompoundStructure()
|
cs = CompoundStructure()
|
||||||
|
# Clean for potential XSS
|
||||||
if name is not None:
|
if name is not None:
|
||||||
cs.name = name
|
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if description is not None:
|
if description is not None:
|
||||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
@ -1195,10 +1165,6 @@ class CompoundStructure(
|
|||||||
cs.smiles = smiles
|
cs.smiles = smiles
|
||||||
cs.compound = compound
|
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:
|
if "normalized_structure" in kwargs:
|
||||||
cs.normalized_structure = kwargs["normalized_structure"]
|
cs.normalized_structure = kwargs["normalized_structure"]
|
||||||
|
|
||||||
@ -1216,8 +1182,6 @@ class CompoundStructure(
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self, width: int = 800, height: int = 400):
|
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)
|
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1415,9 +1379,6 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
if not FormatConverter.is_valid_smirks(smirks):
|
if not FormatConverter.is_valid_smirks(smirks):
|
||||||
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
|
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)
|
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
|
||||||
|
|
||||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
||||||
@ -1429,17 +1390,14 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
if query.exists():
|
if query.exists():
|
||||||
if query.count() > 1:
|
if query.count() > 1:
|
||||||
logger.error(f"More than one rule matched this one! {query}")
|
logger.error(f"More than one rule matched this one! {query}")
|
||||||
|
return query.first()
|
||||||
found_rule = query.first()
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_rule.add_alias(name)
|
|
||||||
|
|
||||||
return found_rule
|
|
||||||
|
|
||||||
r = SimpleAmbitRule()
|
r = SimpleAmbitRule()
|
||||||
r.package = package
|
r.package = package
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
if name is None or name == "":
|
if name is None or name == "":
|
||||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||||
|
|
||||||
@ -1660,15 +1618,10 @@ class Reaction(
|
|||||||
products = models.ManyToManyField(
|
products = models.ManyToManyField(
|
||||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||||
)
|
)
|
||||||
rules = models.ManyToManyField(
|
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
||||||
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
|
||||||
)
|
|
||||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||||
medline_references = ArrayField(
|
medline_references = ArrayField(
|
||||||
models.TextField(blank=False, null=False),
|
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
||||||
null=True,
|
|
||||||
verbose_name="Medline References",
|
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
@ -1685,13 +1638,8 @@ class Reaction(
|
|||||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||||
products: Union[List[str], List[CompoundStructure]] = None,
|
products: Union[List[str], List[CompoundStructure]] = None,
|
||||||
rules: Union[Rule | List[Rule]] = 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 = []
|
_educts = []
|
||||||
_products = []
|
_products = []
|
||||||
|
|
||||||
@ -1746,21 +1694,16 @@ class Reaction(
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
f"Found more than one reaction for given input! {existing_reaction_qs}"
|
||||||
)
|
)
|
||||||
|
return existing_reaction_qs.first()
|
||||||
found_reaction = existing_reaction_qs.first()
|
|
||||||
|
|
||||||
if name:
|
|
||||||
found_reaction.add_alias(name)
|
|
||||||
|
|
||||||
return found_reaction
|
|
||||||
|
|
||||||
r = Reaction()
|
r = Reaction()
|
||||||
r.package = package
|
r.package = package
|
||||||
|
|
||||||
if r is not None:
|
# Clean for potential XSS
|
||||||
r.name = name
|
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.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
r.multi_step = multi_step
|
r.multi_step = multi_step
|
||||||
@ -1838,7 +1781,7 @@ class Reaction(
|
|||||||
return new_reaction
|
return new_reaction
|
||||||
|
|
||||||
def smirks(self):
|
def smirks(self):
|
||||||
return f"{'.'.join([cs.smiles for cs in self.educts.all().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
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
@ -1951,9 +1894,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
if n not in queue:
|
if n not in queue:
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
|
|
||||||
for i in queue:
|
|
||||||
processed.add(i)
|
|
||||||
|
|
||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
@ -2231,7 +2171,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: 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)
|
return Node.create(self, smiles, depth, name=name, description=description)
|
||||||
|
|
||||||
@ -2246,68 +2186,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
):
|
):
|
||||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||||
|
|
||||||
def update_depths(self):
|
|
||||||
# Collect number of in and out links per node
|
|
||||||
in_count = defaultdict(lambda: 0)
|
|
||||||
out_count = defaultdict(lambda: 0)
|
|
||||||
|
|
||||||
for e in self.edges:
|
|
||||||
for react in e.start_nodes.all():
|
|
||||||
out_count[str(react.uuid)] += 1
|
|
||||||
|
|
||||||
for prod in e.end_nodes.all():
|
|
||||||
in_count[str(prod.uuid)] += 1
|
|
||||||
|
|
||||||
depth_map = {}
|
|
||||||
depth_map[0] = list()
|
|
||||||
processed = set()
|
|
||||||
|
|
||||||
data_driven_root_nodes = (
|
|
||||||
self.node_set.all()
|
|
||||||
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts"))
|
|
||||||
.filter(prod_cnt=0, educt_cnt__gt=0)
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Eval QuerySet
|
|
||||||
root_nodes_by_depth = list(self.root_nodes)
|
|
||||||
|
|
||||||
data_driven_root_nodes.update(depth=0)
|
|
||||||
root_nodes = [n for n in data_driven_root_nodes]
|
|
||||||
|
|
||||||
for n in root_nodes_by_depth:
|
|
||||||
if n not in root_nodes:
|
|
||||||
if len(n.edge_products.all()) == 0:
|
|
||||||
root_nodes.append(n)
|
|
||||||
|
|
||||||
for n in root_nodes:
|
|
||||||
depth_map[0].append(n)
|
|
||||||
|
|
||||||
# At most depth len(nodes) is possible
|
|
||||||
for i in range(self.nodes.count()):
|
|
||||||
level_nodes = depth_map.get(i, [])
|
|
||||||
|
|
||||||
if len(level_nodes) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
unique_next_level = set()
|
|
||||||
for n in level_nodes:
|
|
||||||
processed.add(n)
|
|
||||||
for e in self.edges:
|
|
||||||
if n in e.start_nodes.all():
|
|
||||||
for p in e.end_nodes.all():
|
|
||||||
if p not in processed:
|
|
||||||
unique_next_level.add(p)
|
|
||||||
|
|
||||||
if len(unique_next_level) > 0:
|
|
||||||
depth_map[i + 1] = list(unique_next_level)
|
|
||||||
|
|
||||||
for depth, nodes in depth_map.items():
|
|
||||||
for n in nodes:
|
|
||||||
if n.depth != depth and depth != 0:
|
|
||||||
n.depth = depth
|
|
||||||
n.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2360,12 +2238,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"node_label_id": self.default_node_label.url,
|
"node_label_id": self.default_node_label.url,
|
||||||
"image": f"{self.url}?image=svg",
|
"image": f"{self.url}?image=svg",
|
||||||
"image_svg": IndigoUtils.mol_to_svg(
|
"image_svg": IndigoUtils.mol_to_svg(
|
||||||
self.default_node_label.molfile
|
self.default_node_label.smiles, width=40, height=40
|
||||||
if self.default_node_label.molfile is not None
|
|
||||||
and self.default_node_label.molfile.strip()
|
|
||||||
else self.default_node_label.smiles,
|
|
||||||
width=40,
|
|
||||||
height=40,
|
|
||||||
),
|
),
|
||||||
"image_type": "svg",
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
@ -2379,7 +2252,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
},
|
},
|
||||||
"predicted_properties": predicted_properties,
|
"predicted_properties": predicted_properties,
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||||
"proposed": self.is_proposed_intermediate(),
|
|
||||||
"timeseries": self.get_timeseries_data(),
|
"timeseries": self.get_timeseries_data(),
|
||||||
**structure_data,
|
**structure_data,
|
||||||
}
|
}
|
||||||
@ -2420,17 +2292,12 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
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)
|
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||||
|
|
||||||
def get_timeseries_data(self):
|
def get_timeseries_data(self):
|
||||||
for ai in self.additional_information.all():
|
for ai in self.additional_information.all():
|
||||||
if ai.type == "OECD301FTimeSeries":
|
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||||
return ai.get().model_dump(mode="json")
|
return ai.model_dump(mode="json")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -2453,26 +2320,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return data
|
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):
|
def simple_json(self, include_description=False):
|
||||||
res = super().simple_json()
|
res = super().simple_json()
|
||||||
name = res.get("name", None)
|
name = res.get("name", None)
|
||||||
@ -2487,7 +2334,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
edge_label = models.ForeignKey(
|
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(
|
start_nodes = models.ManyToManyField(
|
||||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||||
@ -2566,8 +2413,6 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
rule: Optional[Rule] = None,
|
rule: Optional[Rule] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
):
|
):
|
||||||
e = Edge()
|
e = Edge()
|
||||||
e.pathway = pathway
|
e.pathway = pathway
|
||||||
@ -2597,7 +2442,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||||
rules=rule,
|
rules=rule,
|
||||||
multi_step=kwargs.get("multi_step", False),
|
multi_step=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
e.edge_label = r
|
e.edge_label = r
|
||||||
@ -4605,22 +4450,18 @@ class AdditionalInformation(models.Model):
|
|||||||
|
|
||||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||||
|
|
||||||
@staticmethod
|
def get(self) -> "EnviPyModel":
|
||||||
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||||
try:
|
try:
|
||||||
inst = MAPPING[ai_type](**ai_data)
|
inst = MAPPING[self.type](**self.data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading {ai_type}: {e}")
|
print(f"Error loading {self.type}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return inst
|
|
||||||
|
|
||||||
def get(self) -> "EnviPyModel":
|
|
||||||
inst = AdditionalInformation.from_dict(self.type, self.data)
|
|
||||||
inst.__dict__["uuid"] = str(self.uuid)
|
inst.__dict__["uuid"] = str(self.uuid)
|
||||||
|
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@ -19,7 +19,6 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
from .exceptions import InvalidSMILESException
|
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
EPDBURLParser,
|
||||||
@ -786,11 +785,6 @@ def models(request):
|
|||||||
{"Model": s.SERVER_URL + "/model"},
|
{"Model": s.SERVER_URL + "/model"},
|
||||||
]
|
]
|
||||||
|
|
||||||
context["entity_type"] = "model"
|
|
||||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "models"
|
|
||||||
|
|
||||||
# Keep model_types for potential modal/action use
|
# Keep model_types for potential modal/action use
|
||||||
context["model_types"] = {
|
context["model_types"] = {
|
||||||
"ML Relative Reasoning": {
|
"ML Relative Reasoning": {
|
||||||
@ -803,14 +797,12 @@ def models(request):
|
|||||||
"requires_rule_packages": True,
|
"requires_rule_packages": True,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
}
|
"EnviFormer": {
|
||||||
|
|
||||||
if s.ENVIFORMER_PRESENT:
|
|
||||||
context["model_types"]["EnviFormer"] = {
|
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -818,9 +810,6 @@ def models(request):
|
|||||||
"type": k,
|
"type": k,
|
||||||
"requires_rule_packages": v.requires_rule_packages(),
|
"requires_rule_packages": v.requires_rule_packages(),
|
||||||
"requires_data_packages": v.requires_data_packages(),
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
"additional_parameters": v.Config.__name__.lower()
|
|
||||||
if v.Config.__name__ != ""
|
|
||||||
else None,
|
|
||||||
}
|
}
|
||||||
for k, v in s.PROPERTY_PLUGINS.items():
|
for k, v in s.PROPERTY_PLUGINS.items():
|
||||||
context["model_types"][v.display()] = {
|
context["model_types"][v.display()] = {
|
||||||
@ -829,6 +818,12 @@ def models(request):
|
|||||||
"requires_data_packages": v.requires_data_packages(),
|
"requires_data_packages": v.requires_data_packages(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Context for paginated template
|
||||||
|
context["entity_type"] = "model"
|
||||||
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||||
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
|
context["list_title"] = "models"
|
||||||
|
|
||||||
return render(request, "collections/models_paginated.html", context)
|
return render(request, "collections/models_paginated.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
@ -943,12 +938,13 @@ def package_models(request, package_uuid):
|
|||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ENVIFORMER_PRESENT:
|
if s.ENVIFORMER_PRESENT:
|
||||||
context["model_types"]["EnviFormer"] = {
|
context["model_types"]["EnviFormer"] = {
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
}
|
},
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -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)
|
return JsonResponse(res, safe=False)
|
||||||
|
|
||||||
elif half_life:
|
elif half_life:
|
||||||
@ -1232,7 +1223,9 @@ def package(request, package_uuid):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
if request.GET.get("export", False) == "true":
|
if request.GET.get("export", False) == "true":
|
||||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||||
pack_json = PackageManager.export_package(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 = JsonResponse(pack_json, content_type="application/json")
|
||||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
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_description = request.POST.get("node-description")
|
||||||
|
|
||||||
node_smiles = request.POST.get("node-smiles").strip()
|
node_smiles = request.POST.get("node-smiles").strip()
|
||||||
|
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||||
try:
|
|
||||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
|
||||||
except InvalidSMILESException:
|
|
||||||
return error(
|
|
||||||
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
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})
|
return JsonResponse({"success": current_node.url})
|
||||||
|
|
||||||
new_node_name = request.POST.get("node-name")
|
return HttpResponseBadRequest()
|
||||||
new_node_description = request.POST.get("node-description")
|
|
||||||
|
|
||||||
if any([new_node_name, new_node_description]):
|
|
||||||
if new_node_name is not None and new_node_name.strip() != "":
|
|
||||||
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
current_node.name = new_node_name
|
|
||||||
|
|
||||||
if new_node_description is not None and new_node_description.strip() != "":
|
|
||||||
new_node_description = nh3.clean(new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
current_node.description = new_node_description
|
|
||||||
|
|
||||||
current_node.save()
|
|
||||||
return redirect(current_node.url)
|
|
||||||
|
|
||||||
return error(request, "Node update failed!", "No changes were made to the node")
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
|
|
||||||
@ -2567,9 +2539,6 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
|||||||
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update depths as sideeffect of above operation
|
|
||||||
current_pathway.update_depths()
|
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -3029,15 +2998,9 @@ def settings(request):
|
|||||||
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
||||||
|
|
||||||
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
|
# 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_nodes = min(
|
||||||
max(
|
max(
|
||||||
temp_max_nodes,
|
int(request.POST.get("prediction-setting-max-nodes", 1)),
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
||||||
@ -3058,7 +3021,6 @@ def settings(request):
|
|||||||
|
|
||||||
model_uuid = model_url.split("/")[-1]
|
model_uuid = model_url.split("/")[-1]
|
||||||
params["model"] = EPModel.objects.get(uuid=model_uuid)
|
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(
|
params["model_threshold"] = request.POST.get(
|
||||||
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
||||||
)
|
)
|
||||||
@ -3148,21 +3110,12 @@ def jobs(request):
|
|||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||||
]
|
]
|
||||||
# if current_user.is_superuser:
|
if current_user.is_superuser:
|
||||||
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||||
# else:
|
else:
|
||||||
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||||
|
|
||||||
# Context for paginated template
|
return render(request, "collections/joblog.html", context)
|
||||||
context["entity_type"] = "joblog"
|
|
||||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "joblog"
|
|
||||||
context["list_mode"] = "combined"
|
|
||||||
|
|
||||||
return render(request, "collections/joblog_paginated.html", context)
|
|
||||||
|
|
||||||
# return render(request, "collections/joblog.html", context)
|
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
job_name = request.POST.get("job-name")
|
job_name = request.POST.get("job-name")
|
||||||
|
|||||||
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):
|
def migration(request):
|
||||||
accepted_diffs = [
|
|
||||||
"bt0055-3469.1",
|
|
||||||
"bt0056-2685",
|
|
||||||
"bt0193-4263",
|
|
||||||
"bt0231-1871.1",
|
|
||||||
"bt0242-3803",
|
|
||||||
"bt0254-4224.1",
|
|
||||||
"bt0254-4224.2",
|
|
||||||
"bt0291-1129",
|
|
||||||
"bt0337-3543",
|
|
||||||
"bt0391-4285",
|
|
||||||
"bt0402-3576",
|
|
||||||
"bt0404-3928",
|
|
||||||
"bt0416-4269",
|
|
||||||
"bt0432-4254",
|
|
||||||
"bt0337-4117",
|
|
||||||
"bt0322-3393",
|
|
||||||
]
|
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
@ -128,11 +109,11 @@ def migration(request):
|
|||||||
),
|
),
|
||||||
"id": str(r.uuid),
|
"id": str(r.uuid),
|
||||||
"url": r.url,
|
"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
|
success += 1
|
||||||
else:
|
else:
|
||||||
error += 1
|
error += 1
|
||||||
@ -154,16 +135,7 @@ def migration(request):
|
|||||||
|
|
||||||
for r in migration_status["results"]:
|
for r in migration_status["results"]:
|
||||||
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
||||||
if r["name"] in accepted_diffs:
|
|
||||||
r["status"] = True
|
|
||||||
|
|
||||||
migration_status["results"] = sorted(
|
|
||||||
migration_status["results"], key=lambda x: (x["status"], x["name"])
|
|
||||||
)
|
|
||||||
|
|
||||||
num_success = sum([int(x["status"]) for x in migration_status["results"]])
|
|
||||||
migration_status["success"] = num_success
|
|
||||||
migration_status["error"] = migration_status["total"] - num_success
|
|
||||||
context.update(**migration_status)
|
context.update(**migration_status)
|
||||||
|
|
||||||
return render(request, "migration.html", context)
|
return render(request, "migration.html", context)
|
||||||
|
|||||||
@ -21,11 +21,5 @@
|
|||||||
"django",
|
"django",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"daisyui"
|
"daisyui"
|
||||||
],
|
]
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"@parcel/watcher",
|
|
||||||
"@tailwindcss/oxide"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
|
|
||||||
import matplotlib.patches as mpatches
|
import matplotlib.patches as mpatches
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib.figure import Figure
|
from matplotlib import pyplot as plt
|
||||||
from scipy import stats
|
from scipy import stats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -101,8 +101,7 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
mask_red = x > vp
|
mask_red = x > vp
|
||||||
|
|
||||||
# Plot
|
# Plot
|
||||||
fig = Figure(figsize=(9, 5.5))
|
fig, ax = plt.subplots(figsize=(9, 5.5))
|
||||||
ax = fig.subplots()
|
|
||||||
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
||||||
|
|
||||||
if np.any(mask_green):
|
if np.any(mask_green):
|
||||||
@ -147,12 +146,13 @@ class PepperPrediction(PropertyPrediction):
|
|||||||
]
|
]
|
||||||
ax.legend(handles=patches, frameon=True)
|
ax.legend(handles=patches, frameon=True)
|
||||||
|
|
||||||
fig.tight_layout()
|
plt.tight_layout()
|
||||||
|
|
||||||
# --- Export to SVG string ---
|
# --- Export to SVG string ---
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||||
svg = buf.getvalue()
|
svg = buf.getvalue()
|
||||||
|
plt.close(fig)
|
||||||
buf.close()
|
buf.close()
|
||||||
|
|
||||||
return svg
|
return svg
|
||||||
|
|||||||
@ -187,9 +187,8 @@ class Pepper:
|
|||||||
groups = [group for group in dataset.group_by("structure_id")]
|
groups = [group for group in dataset.group_by("structure_id")]
|
||||||
|
|
||||||
# Unless explicitly set compute everything serial
|
# Unless explicitly set compute everything serial
|
||||||
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
|
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
||||||
if n_threads > 1:
|
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
||||||
results = Parallel(n_jobs=n_threads)(
|
|
||||||
delayed(compute_bayes_per_group)(group[1])
|
delayed(compute_bayes_per_group)(group[1])
|
||||||
for group in dataset.group_by("structure_id")
|
for group in dataset.group_by("structure_id")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
allowBuilds:
|
|
||||||
'@parcel/watcher': true
|
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- '@parcel/watcher'
|
- '@parcel/watcher'
|
||||||
- '@tailwindcss/oxide'
|
- '@tailwindcss/oxide'
|
||||||
|
|||||||
@ -34,12 +34,3 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "./daisyui-theme.css";
|
@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() {
|
get isEditMode() {
|
||||||
return this.mode === "edit";
|
return this.mode === "edit";
|
||||||
},
|
},
|
||||||
get isRequired() {
|
|
||||||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text widget
|
// 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
|
// Compound link widget
|
||||||
Alpine.data(
|
Alpine.data(
|
||||||
"compoundWidget",
|
"compoundWidget",
|
||||||
|
|||||||
@ -5,125 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
|
||||||
const basePagination = (
|
|
||||||
items,
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
totalItems,
|
|
||||||
perPage,
|
|
||||||
isReviewed,
|
|
||||||
instanceId
|
|
||||||
) => ({
|
|
||||||
items,
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
totalItems,
|
|
||||||
perPage,
|
|
||||||
isReviewed,
|
|
||||||
instanceId,
|
|
||||||
|
|
||||||
get paginatedItems() {
|
|
||||||
return this.items;
|
|
||||||
},
|
|
||||||
|
|
||||||
get showingStart() {
|
|
||||||
if (this.totalItems === 0) return 0;
|
|
||||||
return (this.currentPage - 1) * this.perPage + 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
get showingEnd() {
|
|
||||||
if (this.totalItems === 0) return 0;
|
|
||||||
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
|
||||||
},
|
|
||||||
|
|
||||||
nextPage() {
|
|
||||||
if (this.currentPage < this.totalPages) {
|
|
||||||
this.fetchPage(this.currentPage + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
prevPage() {
|
|
||||||
if (this.currentPage > 1) {
|
|
||||||
this.fetchPage(this.currentPage - 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
goToPage(page) {
|
|
||||||
if (page >= 1 && page <= this.totalPages) {
|
|
||||||
this.fetchPage(page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
get pageNumbers() {
|
|
||||||
const pages = [];
|
|
||||||
const total = this.totalPages;
|
|
||||||
const current = this.currentPage;
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (total <= 7) {
|
|
||||||
for (let i = 1; i <= total; i++) {
|
|
||||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
|
||||||
|
|
||||||
let rangeStart;
|
|
||||||
let rangeEnd;
|
|
||||||
|
|
||||||
if (current <= 4) {
|
|
||||||
rangeStart = 2;
|
|
||||||
rangeEnd = 5;
|
|
||||||
} else if (current >= total - 3) {
|
|
||||||
rangeStart = total - 4;
|
|
||||||
rangeEnd = total - 1;
|
|
||||||
} else {
|
|
||||||
rangeStart = current - 1;
|
|
||||||
rangeEnd = current + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStart > 2) {
|
|
||||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
||||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeEnd < total - 1) {
|
|
||||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
|
||||||
}
|
|
||||||
|
|
||||||
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Alpine.data('paginatedList',
|
|
||||||
(items, options = {}) => ({
|
|
||||||
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.fetchPage(1);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchPage(page) {
|
|
||||||
this.totalItems = this.items.length;
|
|
||||||
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
|
||||||
this.currentPage = page
|
|
||||||
|
|
||||||
const start = page * this.perPage - this.perPage;
|
|
||||||
const end = page * this.perPage;
|
|
||||||
return this.items.slice(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||||
items: [],
|
items: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|||||||
204
static/js/pw.js
204
static/js/pw.js
@ -1,37 +1,5 @@
|
|||||||
console.log("loaded pw.js")
|
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) {
|
function predictFromNode(url) {
|
||||||
fetch("", {
|
fetch("", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -55,7 +23,6 @@ function predictFromNode(url) {
|
|||||||
// elem = 'vizdiv'
|
// elem = 'vizdiv'
|
||||||
function draw(pathway, elem) {
|
function draw(pathway, elem) {
|
||||||
|
|
||||||
const initialzoom = 2.5
|
|
||||||
const nodeRadius = 20;
|
const nodeRadius = 20;
|
||||||
const linkDistance = 100;
|
const linkDistance = 100;
|
||||||
const chargeStrength = -200;
|
const chargeStrength = -200;
|
||||||
@ -67,9 +34,6 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
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
|
// Sort nodes by depth first to minimize crossings
|
||||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
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.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);
|
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -137,30 +101,12 @@ function draw(pathway, elem) {
|
|||||||
// Update pseudo node positions first
|
// Update pseudo node positions first
|
||||||
updatePseudoNodePositions();
|
updatePseudoNodePositions();
|
||||||
|
|
||||||
link.attr("d", d => {
|
link.attr("x1", d => d.source.x)
|
||||||
// Check if it's a self-loop (source equals target)
|
.attr("y1", d => d.source.y)
|
||||||
if (d.source.id === d.target.id) {
|
.attr("x2", d => d.target.x)
|
||||||
// Create a bezier curve for self-loops
|
.attr("y2", d => d.target.y);
|
||||||
const x = d.source.x;
|
|
||||||
const y = d.source.y;
|
|
||||||
const loopRadius = nodeRadius * 2; // Adjust size of the loop
|
|
||||||
|
|
||||||
// Create a circular path to the left of the node
|
|
||||||
return `M ${x},${y - nodeRadius}
|
|
||||||
C ${x - loopRadius},${y - nodeRadius - loopRadius}
|
|
||||||
${x - loopRadius},${y + nodeRadius + loopRadius}
|
|
||||||
${x},${y + nodeRadius}`;
|
|
||||||
} else {
|
|
||||||
// Regular straight line for normal edges
|
|
||||||
return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
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) {
|
function dragstarted(event, d) {
|
||||||
@ -263,7 +209,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait before showing popup (ms)
|
||||||
var popupWaitBeforeShow = 500;
|
var popupWaitBeforeShow = 1000;
|
||||||
|
|
||||||
// Custom popover element
|
// Custom popover element
|
||||||
let popoverTimeout = null;
|
let popoverTimeout = null;
|
||||||
@ -517,7 +463,7 @@ function draw(pathway, elem) {
|
|||||||
// TODO needs to be generic once we store it as AddInf
|
// TODO needs to be generic once we store it as AddInf
|
||||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||||
if (s["mean"] != null) {
|
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
|
// Apply zoom to the SVG element - this enables wheel zoom
|
||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
|
|
||||||
svg.call(zoom.scaleBy, initialzoom);
|
|
||||||
|
|
||||||
// Also apply zoom to container to catch events that might not reach SVG
|
// 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
|
// This ensures drag-to-pan works even when clicking on empty space
|
||||||
container.call(zoom);
|
container.call(zoom);
|
||||||
@ -637,11 +581,11 @@ function draw(pathway, elem) {
|
|||||||
for (idx in parents) {
|
for (idx in parents) {
|
||||||
p = nodes[parents[idx]]
|
p = nodes[parents[idx]]
|
||||||
// console.log(p.depth)
|
// console.log(p.depth)
|
||||||
// if (p.depth >= n.depth) {
|
if (p.depth >= n.depth) {
|
||||||
// // keep the .5 steps for pseudo nodes
|
// keep the .5 steps for pseudo nodes
|
||||||
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||||
// // console.log("Adjusting", orig_depth, 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);
|
.on("tick", ticked);
|
||||||
|
|
||||||
// Kanten zeichnen
|
// Kanten zeichnen
|
||||||
const linkGroup = zoomable.append("g")
|
const link = zoomable.append("g")
|
||||||
.selectAll("g")
|
.selectAll("line")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter()
|
.enter().append("line")
|
||||||
.append("g")
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
.attr("class", "link-group");
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
|
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
|
||||||
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")
|
|
||||||
.on("click", function(event, d) {
|
.on("click", function(event, d) {
|
||||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||||
d3.selectAll("path").classed("highlighted", false);
|
|
||||||
|
d3.selectAll("line").classed("highlighted", false);
|
||||||
|
|
||||||
if (!wasHighlighted) {
|
if (!wasHighlighted) {
|
||||||
const toHighlight = [];
|
const toHighlight = [];
|
||||||
toHighlight.push(d.el);
|
toHighlight.push(d.el);
|
||||||
|
|
||||||
if (d.source.pseudo || d.target.pseudo) {
|
if (d.source.pseudo || d.target.pseudo) {
|
||||||
if (d.target.pseudo) {
|
if (d.target.pseudo) {
|
||||||
d3.selectAll("path").each(e => {
|
d3.selectAll("line").each(e => {
|
||||||
if (e !== undefined && e.source.id === d.target.id) {
|
if (e !== undefined && e.source.id === d.target.id) {
|
||||||
toHighlight.push(e.el);
|
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) {
|
for (const e of toHighlight) {
|
||||||
d3.select(e).classed("highlighted", true);
|
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
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
@ -732,41 +651,16 @@ function draw(pathway, elem) {
|
|||||||
.call(d3.drag()
|
.call(d3.drag()
|
||||||
.on("start", dragstarted)
|
.on("start", dragstarted)
|
||||||
.on("drag", dragged)
|
.on("drag", dragged)
|
||||||
.on("end", dragended)
|
.on("end", dragended))
|
||||||
)
|
|
||||||
.on("click", function (event, d) {
|
.on("click", function (event, d) {
|
||||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
|
||||||
d3.selectAll("path").classed("inedge", false);
|
|
||||||
d3.selectAll("path").classed("outedge", false);
|
|
||||||
|
|
||||||
if (!wasHighlighted) {
|
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
|
||||||
const inEdges = findPaths(d.id, links);
|
|
||||||
const outEdges = []
|
|
||||||
// Colorize out edges green
|
|
||||||
for (const l of links) {
|
|
||||||
if (l.source.id === d.id) {
|
|
||||||
outEdges.push(l);
|
|
||||||
if (l.target.pseudo) {
|
|
||||||
for (const l2 of links) {
|
|
||||||
if (l.target.id === l2.source.id) {
|
|
||||||
outEdges.push(l2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e of inEdges) {
|
if (!wasHighlighted) {
|
||||||
d3.select(e.el).classed("inedge", true);
|
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
|
// Kreise für die Knoten hinzufügen
|
||||||
node.append("circle")
|
node.append("circle")
|
||||||
@ -840,14 +734,14 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
function serializeSVG(svgElement) {
|
function serializeSVG(svgElement) {
|
||||||
|
|
||||||
svgElement.querySelectorAll("path.link").forEach(line => {
|
svgElement.querySelectorAll("line.link").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
line.setAttribute("fill", style.fill);
|
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);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
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 page_title %}Packages{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
id="new-package-button"
|
id="new-package-button"
|
||||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
|
||||||
>
|
>
|
||||||
<path d="M12 10v6" />
|
|
||||||
<path d="M9 13h6" />
|
|
||||||
<path
|
|
||||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-sm">
|
|
||||||
Import
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="24"
|
||||||
height="16"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="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>
|
</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>
|
</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>
|
||||||
</div>
|
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
|
|||||||
@ -37,11 +37,7 @@
|
|||||||
perPage: {{ per_page|default:50 }}
|
perPage: {{ per_page|default:50 }}
|
||||||
})"
|
})"
|
||||||
>
|
>
|
||||||
{% if entity_type == 'joblog' %}
|
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||||
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
|
||||||
{% else %}
|
|
||||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit or not meta.url_contains_package %}
|
{% if meta.can_edit or not meta.url_contains_package %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
|
|||||||
@ -123,17 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 -->
|
<!-- Compound link widget -->
|
||||||
<template
|
<template
|
||||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
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) }"
|
:class="{ 'select-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||||
x-model="value"
|
x-model="value"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:required="isRequired"
|
|
||||||
>
|
>
|
||||||
<option value="" :selected="!value">Select...</option>
|
<option value="" :selected="!value">Select...</option>
|
||||||
|
|
||||||
|
|||||||
@ -65,11 +65,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
get showMlrr() {
|
get showMlrr() {
|
||||||
return this.selectedType === 'ml-relative-reasoning';
|
return this.selectedType === 'mlrr';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showRbrr() {
|
get showRbrr() {
|
||||||
return this.selectedType === 'rule-based-relative-reasoning';
|
return this.selectedType === 'rbrr';
|
||||||
},
|
},
|
||||||
|
|
||||||
get showEnviformer() {
|
get showEnviformer() {
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
x-data="{
|
x-data="{
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
reactionImageUrl: '',
|
reactionImageUrl: '',
|
||||||
pes: false,
|
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
@ -16,30 +15,20 @@
|
|||||||
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
||||||
const productsSelect = document.getElementById('add_pathway_edge_products');
|
const productsSelect = document.getElementById('add_pathway_edge_products');
|
||||||
|
|
||||||
const pesLinks = [];
|
|
||||||
|
|
||||||
const substrates = [];
|
const substrates = [];
|
||||||
for (const option of substratesSelect.selectedOptions) {
|
for (const option of substratesSelect.selectedOptions) {
|
||||||
substrates.push(option.dataset.smiles);
|
substrates.push(option.dataset.smiles);
|
||||||
if (option.dataset.pes === 'true') {
|
|
||||||
pesLinks.push(option.dataset.pes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const products = [];
|
const products = [];
|
||||||
for (const option of productsSelect.selectedOptions) {
|
for (const option of productsSelect.selectedOptions) {
|
||||||
products.push(option.dataset.smiles);
|
products.push(option.dataset.smiles);
|
||||||
if (option.dataset.pes === 'true') {
|
|
||||||
pesLinks.push(option.dataset.pes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (substrates.length > 0 && products.length > 0) {
|
if (substrates.length > 0 && products.length > 0) {
|
||||||
const reaction = substrates.join('.') + '>>' + products.join('.');
|
const reaction = substrates.join('.') + '>>' + products.join('.');
|
||||||
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
||||||
this.pes = pesLinks.length > 0;
|
|
||||||
} else {
|
} else {
|
||||||
this.pes = false;
|
|
||||||
this.reactionImageUrl = '';
|
this.reactionImageUrl = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -117,7 +106,6 @@
|
|||||||
{% for n in pathway.nodes %}
|
{% for n in pathway.nodes %}
|
||||||
<option
|
<option
|
||||||
data-smiles="{{ n.default_node_label.smiles }}"
|
data-smiles="{{ n.default_node_label.smiles }}"
|
||||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
|
||||||
value="{{ n.url }}"
|
value="{{ n.url }}"
|
||||||
>
|
>
|
||||||
{{ n.default_node_label.name|safe }}
|
{{ n.default_node_label.name|safe }}
|
||||||
@ -144,7 +132,6 @@
|
|||||||
{% for n in pathway.nodes %}
|
{% for n in pathway.nodes %}
|
||||||
<option
|
<option
|
||||||
data-smiles="{{ n.default_node_label.smiles }}"
|
data-smiles="{{ n.default_node_label.smiles }}"
|
||||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
|
||||||
value="{{ n.url }}"
|
value="{{ n.url }}"
|
||||||
>
|
>
|
||||||
{{ n.default_node_label.name|safe }}
|
{{ n.default_node_label.name|safe }}
|
||||||
@ -157,9 +144,6 @@
|
|||||||
|
|
||||||
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
||||||
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
class="modal"
|
class="modal"
|
||||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
@modal-opened.window="
|
||||||
const links = d3.selectAll('path.highlighted');
|
const links = d3.selectAll('line.highlighted');
|
||||||
|
console.log(links);
|
||||||
if (!links.empty()) {
|
if (!links.empty()) {
|
||||||
const el = links.node();
|
const el = links.node();
|
||||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||||
@ -17,7 +18,9 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
|
|||||||
@ -6,14 +6,17 @@
|
|||||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
@modal-opened.window="
|
||||||
const el = d3.select('circle.highlighted').node();
|
const el = d3.select('circle.highlighted').node();
|
||||||
|
|
||||||
if (el !== null) {
|
if (el !== null) {
|
||||||
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
||||||
|
|
||||||
for (let option of selectElement.options) {
|
for (let option of selectElement.options) {
|
||||||
if (option.value === el.__data__.url) {
|
if (option.value === el.__data__.url) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
|||||||
@ -82,9 +82,6 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Reaction Description -->
|
<!-- Reaction Description -->
|
||||||
@ -95,7 +92,7 @@
|
|||||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||||
{% for educt in edge.start_nodes.all %}
|
{% for educt in edge.start_nodes.all %}
|
||||||
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
||||||
>{{ educt.get_name }}</a
|
>{{ educt.name }}</a
|
||||||
>
|
>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<svg
|
<svg
|
||||||
@ -115,7 +112,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{% for product in edge.end_nodes.all %}
|
{% for product in edge.end_nodes.all %}
|
||||||
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
||||||
>{{ product.get_name }}</a
|
>{{ product.name }}</a
|
||||||
>
|
>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,9 +56,7 @@
|
|||||||
<ul class="menu bg-base-200 rounded-box">
|
<ul class="menu bg-base-200 rounded-box">
|
||||||
{% for um in group.user_member.all %}
|
{% for um in group.user_member.all %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{{ um.url }}" class="hover:bg-base-300"
|
||||||
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
|
||||||
class="hover:bg-base-300"
|
|
||||||
>{{ um.username }}
|
>{{ um.username }}
|
||||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||||
>
|
>
|
||||||
|
|||||||
@ -55,15 +55,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|
||||||
{% for tpl in viz_templates %}
|
{% for tpl in viz_templates %}
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
.link {
|
.link {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
stroke-width: 1.5px;
|
|
||||||
/* marker-end: url(#arrow); */
|
/* marker-end: url(#arrow); */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,15 +72,6 @@
|
|||||||
stroke: red;
|
stroke: red;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inedge {
|
|
||||||
stroke: red;
|
|
||||||
stroke-width: 3px;
|
|
||||||
}
|
|
||||||
.outedge {
|
|
||||||
stroke: green;
|
|
||||||
stroke-width: 3px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script src="{% static 'js/pw.js' %}"></script>
|
<script src="{% static 'js/pw.js' %}"></script>
|
||||||
|
|
||||||
@ -121,7 +111,7 @@
|
|||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Graphical Representation
|
Graphical Representation
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content ">
|
||||||
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
||||||
<div class="navbar bg-base-100 rounded-lg">
|
<div class="navbar bg-base-100 rounded-lg">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@ -178,52 +168,6 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
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 %}
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
@ -249,7 +193,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 1 == 0 %}
|
|
||||||
<li>
|
<li>
|
||||||
<a id="timeseries-toggle-button" class="cursor-pointer">
|
<a id="timeseries-toggle-button" class="cursor-pointer">
|
||||||
<svg
|
<svg
|
||||||
@ -300,7 +243,6 @@
|
|||||||
Show Predicted Properties
|
Show Predicted Properties
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -425,18 +367,6 @@
|
|||||||
>
|
>
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||||
</marker>
|
</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
|
<marker
|
||||||
id="doublearrow"
|
id="doublearrow"
|
||||||
viewBox="0 0 20 30"
|
viewBox="0 0 20 30"
|
||||||
@ -544,10 +474,6 @@
|
|||||||
{{ pathway.d3_json|json_script:"pathway" }}
|
{{ pathway.d3_json|json_script:"pathway" }}
|
||||||
|
|
||||||
<script>
|
<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
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
// Global switch for timeseries view
|
// Global switch for timeseries view
|
||||||
@ -583,67 +509,6 @@
|
|||||||
descContent.innerHTML = newDesc;
|
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
|
// App domain toggle
|
||||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||||
if (appDomainBtn) {
|
if (appDomainBtn) {
|
||||||
|
|||||||
@ -79,9 +79,6 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Reaction Description -->
|
<!-- Reaction Description -->
|
||||||
|
|||||||
@ -24,76 +24,6 @@
|
|||||||
<td>Setting Name</td>
|
<td>Setting Name</td>
|
||||||
<td>{{ setting_to_render.name }}</td>
|
<td>{{ setting_to_render.name }}</td>
|
||||||
</tr>
|
</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 %}
|
{% if setting_to_render.description %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Setting Description</td>
|
<td>Setting Description</td>
|
||||||
|
|||||||
@ -105,7 +105,7 @@
|
|||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="select mb-8 w-full" id="prediction-setting-label">
|
<label class="select mb-8 w-full">
|
||||||
<span class="label">Predictor</span>
|
<span class="label">Predictor</span>
|
||||||
<select id="prediction-setting" name="prediction-setting">
|
<select id="prediction-setting" name="prediction-setting">
|
||||||
<option disabled>Select a Setting</option>
|
<option disabled>Select a Setting</option>
|
||||||
@ -148,21 +148,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{# prettier-ignore-start #}
|
{# prettier-ignore-start #}
|
||||||
<script>
|
<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
|
// Helper function to safely get Ketcher instance from iframe
|
||||||
function getKetcherInstance(iframeId) {
|
function getKetcherInstance(iframeId) {
|
||||||
const ketcherFrame = document.getElementById(iframeId);
|
const ketcherFrame = document.getElementById(iframeId);
|
||||||
@ -212,13 +197,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const button = this;
|
const button = this;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
button.textContent = "Predicting...";
|
||||||
// Set text depending on mode
|
|
||||||
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
|
||||||
button.textContent = "Building...";
|
|
||||||
} else {
|
|
||||||
button.textContent = "Predicting...";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get SMILES from either input or Ketcher
|
// Get SMILES from either input or Ketcher
|
||||||
const smilesInput = document.getElementById("predict-smiles");
|
const smilesInput = document.getElementById("predict-smiles");
|
||||||
|
|||||||
@ -110,6 +110,8 @@
|
|||||||
<div
|
<div
|
||||||
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
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>
|
<a href="/terms" class="link link-hover">Terms of Use</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from epdb.exceptions import InvalidSMILESException
|
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, CompoundStructure
|
from epdb.models import Compound, User, CompoundStructure
|
||||||
|
|
||||||
@ -35,13 +33,13 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(c.description, "No Desc")
|
self.assertEqual(c.description, "No Desc")
|
||||||
|
|
||||||
def test_missing_smiles(self):
|
def test_missing_smiles(self):
|
||||||
with self.assertRaises(InvalidSMILESException):
|
with self.assertRaises(ValueError):
|
||||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
_ = 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")
|
_ = 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")
|
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
def test_smiles_are_trimmed(self):
|
def test_smiles_are_trimmed(self):
|
||||||
@ -98,7 +96,7 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.compounds), 1)
|
self.assertEqual(len(self.package.compounds), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(InvalidSMILESException):
|
with self.assertRaises(ValueError):
|
||||||
_ = Compound.create(
|
_ = Compound.create(
|
||||||
self.package,
|
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",
|
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.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from epdb.exceptions import InvalidSMILESException
|
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, Reaction, Rule
|
from epdb.models import Compound, User, Reaction, Rule
|
||||||
|
|
||||||
@ -164,7 +163,7 @@ class ReactionTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.reactions), 1)
|
self.assertEqual(len(self.package.reactions), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(InvalidSMILESException):
|
with self.assertRaises(ValueError):
|
||||||
_ = Reaction.create(
|
_ = Reaction.create(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
name="Eawag BBD reaction r0001",
|
name="Eawag BBD reaction r0001",
|
||||||
|
|||||||
@ -46,14 +46,6 @@ class ModelViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
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"]],
|
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
||||||
"probability": 0.25,
|
"probability": 0.25,
|
||||||
@ -70,6 +62,14 @@ class ModelViewTest(TestCase):
|
|||||||
"name": "bt0243-4301",
|
"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"]
|
actual = response.json()["pred"]
|
||||||
|
|||||||
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel
|
from envipy_additional_information import HalfLife, HalfLifeWS
|
||||||
from envipy_additional_information.information import Interval
|
from envipy_additional_information.information import Interval
|
||||||
from envipy_additional_information.parsers import (
|
from envipy_additional_information.parsers import (
|
||||||
AcidityParser,
|
AcidityParser,
|
||||||
@ -125,7 +125,6 @@ from envipy_additional_information.parsers import (
|
|||||||
LiquidMatrixSourceParser,
|
LiquidMatrixSourceParser,
|
||||||
OxygenUptakeRateParser,
|
OxygenUptakeRateParser,
|
||||||
InitiatingOrganismParser,
|
InitiatingOrganismParser,
|
||||||
PFASConfidenceParser,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -142,7 +141,7 @@ def get_parameter(request, paramname):
|
|||||||
res = request.POST.get(paramname)
|
res = request.POST.get(paramname)
|
||||||
if res is not None and res.strip() != "":
|
if res is not None and res.strip() != "":
|
||||||
return res
|
return res
|
||||||
raise ValueError("Not all parameters are set!")
|
return ValueError("Not all parameters are set!")
|
||||||
|
|
||||||
|
|
||||||
def get_parameter_or_empty_string(request, paramname):
|
def get_parameter_or_empty_string(request, paramname):
|
||||||
@ -474,12 +473,17 @@ def build_additional_information_from_request(request, type_):
|
|||||||
|
|
||||||
comment = get_parameter_or_empty_string(request, "comment")
|
comment = get_parameter_or_empty_string(request, "comment")
|
||||||
source = get_parameter_or_empty_string(request, "source")
|
source = get_parameter_or_empty_string(request, "source")
|
||||||
# first_order = get_parameter_or_empty_string(request, "firstOrder")
|
first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||||
model = get_parameter_or_empty_string(request, "model")
|
model = get_parameter_or_empty_string(request, "model")
|
||||||
fit = get_parameter_or_empty_string(request, "fit")
|
fit = get_parameter_or_empty_string(request, "fit")
|
||||||
|
|
||||||
if model:
|
if first_order != "":
|
||||||
model = HalfLifeModel(model.upper())
|
if model != "":
|
||||||
|
raise ValueError("not both, model and firstOrder can be set!")
|
||||||
|
if first_order == "true":
|
||||||
|
model = "SFO"
|
||||||
|
else:
|
||||||
|
logger.info("firstOrder is set to false which is not meaningful")
|
||||||
|
|
||||||
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
||||||
|
|
||||||
@ -504,10 +508,6 @@ def build_additional_information_from_request(request, type_):
|
|||||||
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
||||||
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
||||||
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
||||||
|
|
||||||
if model_ws:
|
|
||||||
model_ws = HalfLifeModel(model_ws.upper())
|
|
||||||
|
|
||||||
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
||||||
|
|
||||||
dt50_total = IntervalParser.from_string(hl_ws_total)
|
dt50_total = IntervalParser.from_string(hl_ws_total)
|
||||||
@ -674,8 +674,7 @@ def build_additional_information_from_request(request, type_):
|
|||||||
elif type_ == "studywst":
|
elif type_ == "studywst":
|
||||||
# study_wst_cond = get_parameter(request, "studywstcond")
|
# study_wst_cond = get_parameter(request, "studywstcond")
|
||||||
raise ValueError("studywstcond is not yet implemented")
|
raise ValueError("studywstcond is not yet implemented")
|
||||||
elif type_ == "pfasconfidence":
|
|
||||||
return PFASConfidenceParser.from_string(get_parameter(request, "level"))
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")
|
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
34
uv.lock
generated
34
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
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#0a608c85c73a6ef5c38afea87d2b57fb43f01a70" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
@ -2763,9 +2763,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54", upload-time = "2025-10-01T23:35:50Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58", upload-time = "2025-10-01T23:35:52Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2785,19 +2785,19 @@ dependencies = [
|
|||||||
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5", upload-time = "2025-10-01T23:33:41Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d", upload-time = "2025-10-01T23:33:45Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e", upload-time = "2025-10-01T23:33:48Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d", upload-time = "2025-10-01T23:33:52Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434", upload-time = "2025-10-01T23:34:10Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d", upload-time = "2025-10-01T23:34:15Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25", upload-time = "2025-10-01T23:34:19Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de", upload-time = "2025-10-01T23:34:23Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856", upload-time = "2025-10-01T23:34:28Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88", upload-time = "2025-10-01T23:34:45Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041", upload-time = "2025-10-01T23:34:50Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab", upload-time = "2025-10-01T23:34:53Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
|
||||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" },
|
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user