forked from enviPath/enviPy
Compare commits
27 Commits
520fb9f510
...
develop-ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 57f5ce089c | |||
| 28e5dbbcf5 | |||
| 8620f98082 | |||
| 4ee37b5c71 | |||
| c65be30b6b | |||
| b5e3c1957b | |||
| 3bb9220b22 | |||
| d06ed33196 | |||
| ea672ec91a | |||
| dfc1309fd3 | |||
| 6e2dbccd6c | |||
| b77fbc9b90 | |||
| ce1d709715 | |||
| b0004ea66e | |||
| 2fbffb4b3b | |||
| 365af14448 | |||
| 8017070de8 | |||
| a0ac79a897 | |||
| caeec2ab79 | |||
| 3525621281 | |||
| 9bc9f86ff1 | |||
| dba6514013 | |||
| a092d4a558 | |||
| 2502c020f7 | |||
| 6ab9180291 | |||
| 3657d14659 | |||
| ef6091d416 |
178
bayer/additional_information/__init__.py
Normal file
178
bayer/additional_information/__init__.py
Normal file
@ -0,0 +1,178 @@
|
||||
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,5 +1,6 @@
|
||||
import logging
|
||||
|
||||
from bayer import additional_information # noqa: F401
|
||||
from epdb.template_registry import register_template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -36,4 +37,4 @@ register_template(
|
||||
register_template(
|
||||
"epdb.objects.node.viz",
|
||||
"objects/node_viz.html",
|
||||
)
|
||||
)
|
||||
|
||||
@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
|
||||
def create(
|
||||
compound: Compound,
|
||||
pes_link: str,
|
||||
mol_file: str,
|
||||
molfile: str,
|
||||
smiles: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
cs.smiles = smiles
|
||||
cs.mol_file = mol_file
|
||||
cs.molfile = molfile
|
||||
cs.pes_link = pes_link
|
||||
cs.compound = compound
|
||||
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Representation -->
|
||||
|
||||
@ -2,14 +2,13 @@ import base64
|
||||
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from bayer.models import PESCompound
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Pathway, Node
|
||||
from epdb.views import _anonymous_or_real
|
||||
from epdb.views import _anonymous_or_real, error
|
||||
from utilities.decorators import package_permission_required
|
||||
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
@ -23,7 +22,11 @@ def create_pes(request, package_uuid):
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
return error(
|
||||
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_description = request.POST.get('compound-description')
|
||||
@ -33,21 +36,25 @@ def create_pes(request, package_uuid):
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return BadRequest(
|
||||
return HttpResponseBadRequest(
|
||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||
|
||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||
|
||||
return redirect(pes.url)
|
||||
else:
|
||||
return BadRequest("Please provide a PES link.")
|
||||
return HttpResponseBadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
@ -61,7 +68,11 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
if request.method == "POST":
|
||||
|
||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||
raise BadRequest("Cannot create PESs for internal packages.")
|
||||
return error(
|
||||
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_description = request.POST.get('compound-description')
|
||||
@ -71,18 +82,18 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
try:
|
||||
pes_data = fetch_pes(request, pes_link)
|
||||
except ValueError as e:
|
||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if current_package.classification_level != Package.Classification.SECRET:
|
||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
||||
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||
return BadRequest(
|
||||
return HttpResponseBadRequest(
|
||||
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)
|
||||
@ -105,7 +116,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
else:
|
||||
return BadRequest("Please provide a PES link.")
|
||||
return HttpResponseBadRequest("Please provide a PES link.")
|
||||
else:
|
||||
pass
|
||||
|
||||
@ -114,6 +125,9 @@ def fetch_pes(request, pes_url) -> dict:
|
||||
from epauth.views import get_access_token_from_request
|
||||
token = get_access_token_from_request(request)
|
||||
|
||||
if token is None:
|
||||
token = pes_url.split('/')[-1] == 'dummy'
|
||||
|
||||
if token:
|
||||
for k, v in s.PES_API_MAPPING.items():
|
||||
if pes_url.startswith(k):
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
import enum
|
||||
|
||||
import requests
|
||||
from django.conf import settings as s
|
||||
from envipy_additional_information import EnviPyModel, UIConfig, WidgetType
|
||||
from envipy_additional_information import register
|
||||
from envipy_additional_information import register, EnviPyModel, UIConfig, WidgetType
|
||||
|
||||
from bridge.contracts import Classifier # noqa: I001
|
||||
from bridge.dto import (
|
||||
@ -17,6 +18,9 @@ from bridge.dto import (
|
||||
TransformationProductPrediction,
|
||||
) # noqa: I001
|
||||
|
||||
logger = logging.getLogger("epdb")
|
||||
|
||||
|
||||
class SamplingAlgorithm(enum.Enum):
|
||||
EXACT = "exact"
|
||||
|
||||
@ -85,14 +89,13 @@ class BB4G(Classifier):
|
||||
}
|
||||
|
||||
started = False
|
||||
retries = 0
|
||||
while not started and retries < 5:
|
||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
||||
|
||||
while not started:
|
||||
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
|
||||
logger.info(f"Starting BB4G: {res.status_code}")
|
||||
if res.status_code == 200:
|
||||
started = True
|
||||
elif res.status_code in [500, 502]:
|
||||
retries += 1
|
||||
import time
|
||||
time.sleep(5)
|
||||
else:
|
||||
@ -166,18 +169,30 @@ class BB4G(Classifier):
|
||||
"cutoff": self.config.cutoff,
|
||||
}
|
||||
|
||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None)
|
||||
retries = 0
|
||||
while retries < 100:
|
||||
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
|
||||
proxies=s.PROXIES or None)
|
||||
|
||||
resp.raise_for_status()
|
||||
if resp.status_code == 418:
|
||||
retries += 1
|
||||
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
|
||||
import time
|
||||
time.sleep(3)
|
||||
continue
|
||||
|
||||
for substrate, predictions in resp.json().items():
|
||||
preds = {}
|
||||
resp.raise_for_status()
|
||||
|
||||
for pred in predictions:
|
||||
prod = pred["prediction"]
|
||||
prob = math.exp(pred["log_likelihood"])
|
||||
preds[prod] = prob
|
||||
for substrate, predictions in resp.json().items():
|
||||
preds = {}
|
||||
|
||||
result[substrate] = preds
|
||||
for pred in predictions:
|
||||
prod = pred["prediction"]
|
||||
prob = math.exp(pred["log_likelihood"])
|
||||
preds[prod] = prob
|
||||
|
||||
result[substrate] = preds
|
||||
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
@ -261,7 +261,6 @@ class Classifier(Plugin):
|
||||
for k, v in data.items():
|
||||
if v != "":
|
||||
cpy[k] = v
|
||||
|
||||
return cls.Config(**cpy)
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -25,25 +25,14 @@ services:
|
||||
- ep_bayer_redis_data:/data
|
||||
|
||||
biotransformer3:
|
||||
image: envipath/biotransformer3:1.0
|
||||
image: git.envipath.com/envipath/biotransformer3:1.0
|
||||
container_name: epbiotransformer3
|
||||
|
||||
# web:
|
||||
# image: envipath/envipy-bayer:1.0
|
||||
# container_name: epdjango
|
||||
# ports:
|
||||
# - "127.0.0.1:8000:8000"
|
||||
# env_file:
|
||||
# - .env
|
||||
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
# volumes:
|
||||
# - ep_bayer_data:/opt/enviPy/
|
||||
|
||||
celery_worker:
|
||||
image: envipath/envipy-bayer:1.0
|
||||
image: git.envipath.com/envipath/envipy-bayer:1.2
|
||||
container_name: epcelery
|
||||
env_file:
|
||||
- .env.dev
|
||||
- .env
|
||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||
volumes:
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
@ -275,6 +275,12 @@ LOGGING = {
|
||||
"filename": os.path.join(LOG_DIR, "debug.log"),
|
||||
"formatter": "simple",
|
||||
},
|
||||
"auth_file": {
|
||||
"level": "INFO", # Or higher
|
||||
"class": "logging.FileHandler",
|
||||
"filename": os.path.join(LOG_DIR, "auth.log"),
|
||||
"formatter": "simple",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
# For everything under epdb/ loaded via getlogger(__name__)
|
||||
@ -295,6 +301,11 @@ LOGGING = {
|
||||
"propagate": True,
|
||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||
},
|
||||
"auth": {
|
||||
"handlers": ["auth_file"],
|
||||
"propagate": True,
|
||||
"level": os.environ.get("LOG_LEVEL", "INFO"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -343,7 +354,7 @@ DEFAULT_MODEL_PARAMS = {
|
||||
"num_chains": 10,
|
||||
}
|
||||
|
||||
DEFAULT_MAX_NUMBER_OF_NODES = 50
|
||||
DEFAULT_MAX_NUMBER_OF_NODES = 9999
|
||||
DEFAULT_MAX_DEPTH = 8
|
||||
DEFAULT_MODEL_THRESHOLD = 0.25
|
||||
|
||||
|
||||
5
epdb/exceptions.py
Normal file
5
epdb/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
class InvalidSMILESException(Exception):
|
||||
pass
|
||||
|
||||
class PackageImportException(Exception):
|
||||
pass
|
||||
@ -633,9 +633,14 @@ class CompoundSchema(Schema):
|
||||
reviewStatus: str = Field(False, alias="review_status")
|
||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||
structures: List["CompoundStructureSchema"] = []
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_status(obj: CompoundStructure):
|
||||
def resolve_pes_link(obj: Compound):
|
||||
return getattr(obj.default_structure, "pes_link", None)
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_status(obj: Compound):
|
||||
return "reviewed" if obj.package.reviewed else "unreviewed"
|
||||
|
||||
@staticmethod
|
||||
@ -709,6 +714,7 @@ class CompoundStructureSchema(Schema):
|
||||
reviewStatus: str = Field(None, alias="review_status")
|
||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||
smiles: str = Field(None, alias="smiles")
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_status(obj: CompoundStructure):
|
||||
@ -871,6 +877,10 @@ def create_package_compound(
|
||||
|
||||
classification = pes_data.get("classificationLevel", "")
|
||||
if "secret" == classification.lower():
|
||||
|
||||
if p.classification_level != Package.Classification.SECRET:
|
||||
return 400, {"Cannot create PESs for non-secret packages."}
|
||||
|
||||
data_pools = pes_data.get("dataPools")
|
||||
if data_pools:
|
||||
if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools:
|
||||
@ -1683,14 +1693,15 @@ class PathwayNode(Schema):
|
||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||
id: str = Field(None, alias="url")
|
||||
idcomp: str = Field(None, alias="default_node_label.url")
|
||||
idreact: str = Field(None, alias="default_node_label.url")
|
||||
idcomp: str = Field(None, alias="node_label_id")
|
||||
idreact: str = Field(None, alias="node_label_id")
|
||||
image: str = Field(None, alias="image")
|
||||
imageSize: int = Field(None, alias="image_size")
|
||||
name: str = Field(None, alias="name")
|
||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
smiles: str = Field(None, alias="smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
pesLink: str | None = Field(None, alias="pes_link")
|
||||
|
||||
@staticmethod
|
||||
def resolve_atom_count(obj: Node):
|
||||
@ -1703,24 +1714,10 @@ class PathwayNode(Schema):
|
||||
# TODO
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def resolve_engineered_intermediate(obj: Node):
|
||||
# TODO
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def resolve_image(obj: Node):
|
||||
return f"{obj.default_node_label.url}?image=svg"
|
||||
|
||||
@staticmethod
|
||||
def resolve_image_size(obj: Node):
|
||||
return 400
|
||||
|
||||
@staticmethod
|
||||
def resolve_proposed_intermediate(obj: Node):
|
||||
# TODO
|
||||
return []
|
||||
|
||||
|
||||
class PathwaySchema(Schema):
|
||||
aliases: List[str] = Field([], alias="aliases")
|
||||
@ -2388,3 +2385,31 @@ def get_setting(request, setting_uuid):
|
||||
return 403, {
|
||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
|
||||
########
|
||||
# Util #
|
||||
########
|
||||
class NonPersistent(Schema):
|
||||
smiles: str
|
||||
setting_url: str = Field(..., alias="settingUri")
|
||||
|
||||
|
||||
@router.post("/util", response={200: Any, 403: Error})
|
||||
def predict(request, np: Form[NonPersistent]):
|
||||
try:
|
||||
print(request.user)
|
||||
print(np.setting_url)
|
||||
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||
|
||||
from epdb.logic import SPathway
|
||||
|
||||
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||
spw.predict()
|
||||
|
||||
return spw.to_json()
|
||||
|
||||
except ValueError:
|
||||
return 403, {
|
||||
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
@ -1078,10 +1078,8 @@ class PackageManager(object):
|
||||
data: Dict[str, Any],
|
||||
owner: User,
|
||||
preserve_uuids=False,
|
||||
add_import_timestamp=True,
|
||||
trust_reviewed=False,
|
||||
) -> Package:
|
||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
||||
importer = PackageImporter(data, preserve_uuids)
|
||||
imported_package = importer.do_import()
|
||||
|
||||
up = UserPackagePermission()
|
||||
@ -1898,6 +1896,12 @@ class SPathway(object):
|
||||
"to": to_indices,
|
||||
}
|
||||
|
||||
if edge.rule:
|
||||
e["rule"] = edge.rule.simple_json()
|
||||
|
||||
if edge.probability:
|
||||
e["probability"] = edge.probability
|
||||
|
||||
edges.append(e)
|
||||
|
||||
return {
|
||||
|
||||
@ -99,11 +99,15 @@ class Command(BaseCommand):
|
||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||
new_license.save()
|
||||
|
||||
def import_package(self, data, owner):
|
||||
return PackageManager.import_legacy_package(
|
||||
def import_package(self, data, owner, all_envipath_user_group):
|
||||
p = PackageManager.import_legacy_package(
|
||||
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
||||
)
|
||||
|
||||
PackageManager.grant_read(owner, p, all_envipath_user_group)
|
||||
|
||||
return p
|
||||
|
||||
def create_default_setting(self, owner, packages):
|
||||
s = SettingManager.create_setting(
|
||||
owner,
|
||||
@ -198,7 +202,7 @@ class Command(BaseCommand):
|
||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||
).read()
|
||||
)
|
||||
imported_package = self.import_package(package_data, admin)
|
||||
imported_package = self.import_package(package_data, admin, g)
|
||||
mapping[p.replace(".json", "")] = imported_package
|
||||
|
||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||
|
||||
@ -23,6 +23,7 @@ MAPPING = {
|
||||
"true": HalfLifeModel.SFO,
|
||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||
"other": HalfLifeModel.OTHER,
|
||||
}
|
||||
|
||||
|
||||
|
||||
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
@ -0,0 +1,150 @@
|
||||
# Generated by Django 6.0.3 on 2026-07-01 20:59
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0026_auto_20260602_1718"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="compound",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="compound",
|
||||
name="default_structure",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="compound_default_structure",
|
||||
to="epdb.compoundstructure",
|
||||
verbose_name="Default Structure",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="compound",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="compoundstructure",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="compoundstructure",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edge",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edge",
|
||||
name="edge_label",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="epdb.reaction",
|
||||
verbose_name="Edge label",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edge",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="node",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="node",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pathway",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pathway",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="reaction",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="reaction",
|
||||
name="medline_references",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(),
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Medline References",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="reaction",
|
||||
name="rules",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="reaction_rule", to="epdb.rule", verbose_name="Rule"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="reaction",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rule",
|
||||
name="aliases",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rule",
|
||||
name="scenarios",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||
),
|
||||
),
|
||||
]
|
||||
170
epdb/models.py
170
epdb/models.py
@ -31,6 +31,7 @@ from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
from bridge.contracts import Property
|
||||
from bridge.dto import RunResult, PropertyPrediction
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||
from utilities.ml import (
|
||||
ApplicabilityDomainPCA,
|
||||
@ -576,6 +577,10 @@ class ReactionIdentifierMixin(ExternalIdentifierMixin):
|
||||
def get_uniprot_identifiers(self):
|
||||
return self.get_external_identifier("UniProt")
|
||||
|
||||
def contains_pes(self):
|
||||
from bayer.models import PESStructure
|
||||
return any([isinstance(o, PESStructure) for o in self.educts.all()]) or any(
|
||||
[isinstance(o, PESStructure) for o in self.products.all()])
|
||||
|
||||
##############
|
||||
# EP Objects #
|
||||
@ -632,7 +637,7 @@ class EnviPathModel(TimeStampedModel):
|
||||
|
||||
class AliasMixin(models.Model):
|
||||
aliases = ArrayField(
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
@ -655,7 +660,9 @@ class AliasMixin(models.Model):
|
||||
|
||||
|
||||
class ScenarioMixin(models.Model):
|
||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
||||
scenarios = models.ManyToManyField(
|
||||
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||
@ -781,7 +788,7 @@ class Compound(
|
||||
"CompoundStructure",
|
||||
verbose_name="Default Structure",
|
||||
related_name="compound_default_structure",
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
|
||||
@ -858,13 +865,17 @@ class Compound(
|
||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
) -> "Compound":
|
||||
if smiles is None or smiles.strip() == "":
|
||||
raise ValueError("SMILES is required")
|
||||
raise InvalidSMILESException("SMILES is required")
|
||||
|
||||
smiles = smiles.strip()
|
||||
|
||||
parsed = FormatConverter.from_smiles(smiles)
|
||||
if parsed is None:
|
||||
raise ValueError("Given SMILES is invalid")
|
||||
raise InvalidSMILESException("Given SMILES is invalid")
|
||||
|
||||
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)
|
||||
|
||||
@ -876,7 +887,14 @@ class Compound(
|
||||
|
||||
# Check if we find a direct match for a given SMILES
|
||||
if qs.exists():
|
||||
return qs.first().compound
|
||||
found_structure = qs.first()
|
||||
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)
|
||||
@ -886,16 +904,19 @@ class Compound(
|
||||
# Check if we can find the standardized one
|
||||
if qs.exists():
|
||||
# TODO should we add a structure?
|
||||
return qs.first().compound
|
||||
found_structure = qs.first()
|
||||
found_compound = found_structure.compound
|
||||
|
||||
if name:
|
||||
found_structure.add_alias(name)
|
||||
found_compound.add_alias(name)
|
||||
|
||||
return found_compound
|
||||
|
||||
# Generate Compound
|
||||
c = Compound()
|
||||
c.package = package
|
||||
|
||||
if name is not None:
|
||||
# Clean for potential XSS
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1146,18 +1167,27 @@ class CompoundStructure(
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
compound: Compound, smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||
compound: Compound, smiles: str, name: str = None, description: str = None, molfile: 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():
|
||||
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
|
||||
|
||||
if name:
|
||||
found_cs.add_alias(name)
|
||||
|
||||
return found_cs
|
||||
|
||||
if compound.pk is None:
|
||||
raise ValueError("Unpersisted Compound! Persist compound first!")
|
||||
|
||||
cs = CompoundStructure()
|
||||
# Clean for potential XSS
|
||||
|
||||
if name is not None:
|
||||
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
cs.name = name
|
||||
|
||||
if description is not None:
|
||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
@ -1165,6 +1195,10 @@ class CompoundStructure(
|
||||
cs.smiles = smiles
|
||||
cs.compound = compound
|
||||
|
||||
# Check if molfile is present and valid
|
||||
if molfile is not None and FormatConverter.from_molfile(molfile) is not None:
|
||||
cs.molfile = molfile
|
||||
|
||||
if "normalized_structure" in kwargs:
|
||||
cs.normalized_structure = kwargs["normalized_structure"]
|
||||
|
||||
@ -1182,6 +1216,8 @@ class CompoundStructure(
|
||||
|
||||
@property
|
||||
def as_svg(self, width: int = 800, height: int = 400):
|
||||
if self.molfile is not None and self.molfile.strip() != "":
|
||||
return IndigoUtils.mol_to_svg(self.molfile, width=width, height=height)
|
||||
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
||||
|
||||
@property
|
||||
@ -1379,6 +1415,9 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if not FormatConverter.is_valid_smirks(smirks):
|
||||
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
|
||||
|
||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
||||
@ -1390,14 +1429,17 @@ class SimpleAmbitRule(SimpleRule):
|
||||
if query.exists():
|
||||
if query.count() > 1:
|
||||
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.package = package
|
||||
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
||||
|
||||
@ -1618,10 +1660,15 @@ class Reaction(
|
||||
products = models.ManyToManyField(
|
||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||
)
|
||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
||||
rules = models.ManyToManyField(
|
||||
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
||||
)
|
||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||
medline_references = ArrayField(
|
||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
||||
models.TextField(blank=False, null=False),
|
||||
null=True,
|
||||
verbose_name="Medline References",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||
@ -1638,8 +1685,13 @@ class Reaction(
|
||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||
products: Union[List[str], List[CompoundStructure]] = None,
|
||||
rules: Union[Rule | List[Rule]] = None,
|
||||
multi_step: bool = True,
|
||||
multi_step: bool = False,
|
||||
):
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None and name.strip() != "":
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
_educts = []
|
||||
_products = []
|
||||
|
||||
@ -1694,14 +1746,19 @@ class Reaction(
|
||||
logger.error(
|
||||
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.package = package
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None and name.strip() != "":
|
||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
if r is not None:
|
||||
r.name = name
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
@ -2174,7 +2231,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
smiles: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
depth: Optional[int] = 0,
|
||||
depth: Optional[int] = -1,
|
||||
):
|
||||
return Node.create(self, smiles, depth, name=name, description=description)
|
||||
|
||||
@ -2205,7 +2262,25 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
depth_map[0] = list()
|
||||
processed = set()
|
||||
|
||||
for n in self.root_nodes:
|
||||
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
|
||||
@ -2285,7 +2360,12 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
"node_label_id": self.default_node_label.url,
|
||||
"image": f"{self.url}?image=svg",
|
||||
"image_svg": IndigoUtils.mol_to_svg(
|
||||
self.default_node_label.smiles, width=40, height=40
|
||||
self.default_node_label.molfile
|
||||
if self.default_node_label.molfile is not None
|
||||
and self.default_node_label.molfile.strip()
|
||||
else self.default_node_label.smiles,
|
||||
width=40,
|
||||
height=40,
|
||||
),
|
||||
"image_type": "svg",
|
||||
"name": self.get_name(),
|
||||
@ -2299,6 +2379,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
},
|
||||
"predicted_properties": predicted_properties,
|
||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||
"proposed": self.is_proposed_intermediate(),
|
||||
"timeseries": self.get_timeseries_data(),
|
||||
**structure_data,
|
||||
}
|
||||
@ -2339,12 +2420,17 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
@property
|
||||
def as_svg(self):
|
||||
if (
|
||||
self.default_node_label.molfile is not None
|
||||
and self.default_node_label.molfile.strip() != ""
|
||||
):
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||
|
||||
def get_timeseries_data(self):
|
||||
for ai in self.additional_information.all():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
if ai.type == "OECD301FTimeSeries":
|
||||
return ai.get().model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
@ -2367,6 +2453,26 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
|
||||
return data
|
||||
|
||||
def is_proposed_intermediate(self):
|
||||
collected = defaultdict(dict)
|
||||
for ai in self.additional_information.filter(
|
||||
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"]
|
||||
, scenario__isnull=False
|
||||
):
|
||||
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||
|
||||
if ai.type == "ProposedIntermediate":
|
||||
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||
|
||||
if ai.type == "Confidence":
|
||||
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||
|
||||
if ai.type == "TransformationProductImportance":
|
||||
collected[str(ai.scenario.uuid)]["Transformation product importance"] = ai.get().importance.value
|
||||
|
||||
return list(collected.values())
|
||||
|
||||
def simple_json(self, include_description=False):
|
||||
res = super().simple_json()
|
||||
name = res.get("name", None)
|
||||
@ -2381,7 +2487,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
edge_label = models.ForeignKey(
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.SET_NULL
|
||||
"epdb.Reaction", verbose_name="Edge label", null=True, on_delete=models.CASCADE
|
||||
)
|
||||
start_nodes = models.ManyToManyField(
|
||||
"epdb.Node", verbose_name="Start Nodes", related_name="edge_educts"
|
||||
|
||||
@ -19,6 +19,7 @@ from sentry_sdk import capture_exception
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .exceptions import InvalidSMILESException
|
||||
|
||||
from .logic import (
|
||||
EPDBURLParser,
|
||||
@ -785,6 +786,11 @@ def models(request):
|
||||
{"Model": s.SERVER_URL + "/model"},
|
||||
]
|
||||
|
||||
context["entity_type"] = "model"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "models"
|
||||
|
||||
# Keep model_types for potential modal/action use
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": {
|
||||
@ -797,12 +803,14 @@ def models(request):
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
@ -810,6 +818,9 @@ def models(request):
|
||||
"type": k,
|
||||
"requires_rule_packages": v.requires_rule_packages(),
|
||||
"requires_data_packages": v.requires_data_packages(),
|
||||
"additional_parameters": v.Config.__name__.lower()
|
||||
if v.Config.__name__ != ""
|
||||
else None,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v.display()] = {
|
||||
@ -818,12 +829,6 @@ def models(request):
|
||||
"requires_data_packages": v.requires_data_packages(),
|
||||
}
|
||||
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "model"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "models"
|
||||
|
||||
return render(request, "collections/models_paginated.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
@ -938,13 +943,12 @@ def package_models(request, package_uuid):
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
context["model_types"]["EnviFormer"] = {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
@ -1120,6 +1124,11 @@ def package_model(request, package_uuid, model_uuid):
|
||||
}
|
||||
)
|
||||
|
||||
# Sort data by prob desc
|
||||
res["pred"] = sorted(
|
||||
res["pred"], key=lambda x: x["probability"], reverse=True
|
||||
)
|
||||
|
||||
return JsonResponse(res, safe=False)
|
||||
|
||||
elif half_life:
|
||||
@ -1223,9 +1232,7 @@ def package(request, package_uuid):
|
||||
if request.method == "GET":
|
||||
if request.GET.get("export", False) == "true":
|
||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||
pack_json = PackageManager.export_package(
|
||||
current_package, include_models=False, include_external_identifiers=False
|
||||
)
|
||||
pack_json = PackageManager.export_package(current_package)
|
||||
response = JsonResponse(pack_json, content_type="application/json")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
|
||||
@ -2361,7 +2368,13 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
||||
node_description = request.POST.get("node-description")
|
||||
|
||||
node_smiles = request.POST.get("node-smiles").strip()
|
||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||
|
||||
try:
|
||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
||||
except InvalidSMILESException:
|
||||
return error(
|
||||
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
||||
)
|
||||
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
@ -2469,7 +2482,22 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
||||
|
||||
return JsonResponse({"success": current_node.url})
|
||||
|
||||
return HttpResponseBadRequest()
|
||||
new_node_name = request.POST.get("node-name")
|
||||
new_node_description = request.POST.get("node-description")
|
||||
|
||||
if any([new_node_name, new_node_description]):
|
||||
if new_node_name is not None and new_node_name.strip() != "":
|
||||
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
current_node.name = new_node_name
|
||||
|
||||
if new_node_description is not None and new_node_description.strip() != "":
|
||||
new_node_description = nh3.clean(new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
current_node.description = new_node_description
|
||||
|
||||
current_node.save()
|
||||
return redirect(current_node.url)
|
||||
|
||||
return error(request, "Node update failed!", "No changes were made to the node")
|
||||
else:
|
||||
return HttpResponseNotAllowed(["GET", "POST"])
|
||||
|
||||
@ -3001,9 +3029,15 @@ def settings(request):
|
||||
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
|
||||
|
||||
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
|
||||
temp_max_nodes = request.POST.get("prediction-setting-max-nodes")
|
||||
if temp_max_nodes is None or temp_max_nodes == "" or int(temp_max_nodes) == -1:
|
||||
temp_max_nodes = s.DEFAULT_MAX_NUMBER_OF_NODES
|
||||
else:
|
||||
temp_max_nodes = int(request.POST.get("prediction-setting-max-nodes", 1))
|
||||
|
||||
max_nodes = min(
|
||||
max(
|
||||
int(request.POST.get("prediction-setting-max-nodes", 1)),
|
||||
temp_max_nodes,
|
||||
2,
|
||||
),
|
||||
s.DEFAULT_MAX_NUMBER_OF_NODES,
|
||||
@ -3024,6 +3058,7 @@ def settings(request):
|
||||
|
||||
model_uuid = model_url.split("/")[-1]
|
||||
params["model"] = EPModel.objects.get(uuid=model_uuid)
|
||||
# TODO Check if removed if request contains "" or not at all
|
||||
params["model_threshold"] = request.POST.get(
|
||||
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
|
||||
)
|
||||
|
||||
1610504
fixtures/SOIL_ engineered soil degradation pathways for ML 20251103.json
Normal file
1610504
fixtures/SOIL_ engineered soil degradation pathways for ML 20251103.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
154
fixtures/pes.json
Normal file
154
fixtures/pes.json
Normal file
File diff suppressed because one or more lines are too long
@ -65,6 +65,25 @@ def run_both_engines(SMILES, SMIRKS):
|
||||
|
||||
|
||||
def migration(request):
|
||||
accepted_diffs = [
|
||||
"bt0055-3469.1",
|
||||
"bt0056-2685",
|
||||
"bt0193-4263",
|
||||
"bt0231-1871.1",
|
||||
"bt0242-3803",
|
||||
"bt0254-4224.1",
|
||||
"bt0254-4224.2",
|
||||
"bt0291-1129",
|
||||
"bt0337-3543",
|
||||
"bt0391-4285",
|
||||
"bt0402-3576",
|
||||
"bt0404-3928",
|
||||
"bt0416-4269",
|
||||
"bt0432-4254",
|
||||
"bt0337-4117",
|
||||
"bt0322-3393",
|
||||
]
|
||||
|
||||
if request.method == "GET":
|
||||
context = get_base_context(request)
|
||||
|
||||
@ -109,11 +128,11 @@ def migration(request):
|
||||
),
|
||||
"id": str(r.uuid),
|
||||
"url": r.url,
|
||||
"status": res,
|
||||
"status": res or r.name in accepted_diffs,
|
||||
}
|
||||
)
|
||||
|
||||
if res:
|
||||
if res or r.name in accepted_diffs:
|
||||
success += 1
|
||||
else:
|
||||
error += 1
|
||||
@ -135,7 +154,16 @@ def migration(request):
|
||||
|
||||
for r in migration_status["results"]:
|
||||
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
|
||||
if r["name"] in accepted_diffs:
|
||||
r["status"] = True
|
||||
|
||||
migration_status["results"] = sorted(
|
||||
migration_status["results"], key=lambda x: (x["status"], x["name"])
|
||||
)
|
||||
|
||||
num_success = sum([int(x["status"]) for x in migration_status["results"]])
|
||||
migration_status["success"] = num_success
|
||||
migration_status["error"] = migration_status["total"] - num_success
|
||||
context.update(**migration_status)
|
||||
|
||||
return render(request, "migration.html", context)
|
||||
|
||||
@ -34,3 +34,12 @@
|
||||
}
|
||||
|
||||
@import "./daisyui-theme.css";
|
||||
|
||||
select.select[multiple] {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
p a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
@ -5,6 +5,125 @@
|
||||
*/
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
|
||||
const basePagination = (
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId
|
||||
) => ({
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId,
|
||||
|
||||
get paginatedItems() {
|
||||
return this.items;
|
||||
},
|
||||
|
||||
get showingStart() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return (this.currentPage - 1) * this.perPage + 1;
|
||||
},
|
||||
|
||||
get showingEnd() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.fetchPage(this.currentPage + 1);
|
||||
}
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.fetchPage(this.currentPage - 1);
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.fetchPage(page);
|
||||
}
|
||||
},
|
||||
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const total = this.totalPages;
|
||||
const current = this.currentPage;
|
||||
|
||||
if (total === 0) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (total <= 7) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
} else {
|
||||
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||
|
||||
let rangeStart;
|
||||
let rangeEnd;
|
||||
|
||||
if (current <= 4) {
|
||||
rangeStart = 2;
|
||||
rangeEnd = 5;
|
||||
} else if (current >= total - 3) {
|
||||
rangeStart = total - 4;
|
||||
rangeEnd = total - 1;
|
||||
} else {
|
||||
rangeStart = current - 1;
|
||||
rangeEnd = current + 1;
|
||||
}
|
||||
|
||||
if (rangeStart > 2) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||
}
|
||||
|
||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
|
||||
if (rangeEnd < total - 1) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||
}
|
||||
|
||||
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
});
|
||||
|
||||
Alpine.data('paginatedList',
|
||||
(items, options = {}) => ({
|
||||
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||
|
||||
init() {
|
||||
this.fetchPage(1);
|
||||
},
|
||||
|
||||
async fetchPage(page) {
|
||||
this.totalItems = this.items.length;
|
||||
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||
this.currentPage = page
|
||||
|
||||
const start = page * this.perPage - this.perPage;
|
||||
const end = page * this.perPage;
|
||||
return this.items.slice(start, end);
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||
items: [],
|
||||
currentPage: 1,
|
||||
|
||||
194
static/js/pw.js
194
static/js/pw.js
@ -1,5 +1,37 @@
|
||||
console.log("loaded pw.js")
|
||||
|
||||
function findPaths(source_idx, links) {
|
||||
const resultLinks = new Set();
|
||||
const visited = new Set();
|
||||
// Helper function for depth-first search
|
||||
function dfs(current) {
|
||||
|
||||
if (visited.has(current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (links.filter(link => link.target.id === current).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(current);
|
||||
links.forEach(link => {
|
||||
if (
|
||||
link.target.id === current &&
|
||||
!visited.has(link.source.id) &&
|
||||
link.source.id !== link.target.id // Avoid self-loops
|
||||
) {
|
||||
resultLinks.add(link); // Add link to result
|
||||
dfs(link.source.id);
|
||||
}
|
||||
});
|
||||
visited.delete(current);
|
||||
}
|
||||
// Start DFS
|
||||
dfs(source_idx);
|
||||
return Array.from(resultLinks);
|
||||
}
|
||||
|
||||
function predictFromNode(url) {
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
@ -23,6 +55,7 @@ function predictFromNode(url) {
|
||||
// elem = 'vizdiv'
|
||||
function draw(pathway, elem) {
|
||||
|
||||
const initialzoom = 2.5
|
||||
const nodeRadius = 20;
|
||||
const linkDistance = 100;
|
||||
const chargeStrength = -200;
|
||||
@ -34,6 +67,9 @@ function draw(pathway, elem) {
|
||||
const horizontalSpacing = 75; // horizontal space between nodes
|
||||
const depthMap = new Map();
|
||||
|
||||
// Avoid leaving unconnected Nodes leaving the viewport
|
||||
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||
|
||||
// Sort nodes by depth first to minimize crossings
|
||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||
|
||||
@ -63,7 +99,7 @@ function draw(pathway, elem) {
|
||||
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
||||
}
|
||||
|
||||
node.fy = node.depth * levelSpacing + 50;
|
||||
node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
|
||||
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||
});
|
||||
}
|
||||
@ -101,12 +137,30 @@ function draw(pathway, elem) {
|
||||
// Update pseudo node positions first
|
||||
updatePseudoNodePositions();
|
||||
|
||||
link.attr("x1", d => d.source.x)
|
||||
.attr("y1", d => d.source.y)
|
||||
.attr("x2", d => d.target.x)
|
||||
.attr("y2", d => d.target.y);
|
||||
link.attr("d", d => {
|
||||
// Check if it's a self-loop (source equals target)
|
||||
if (d.source.id === d.target.id) {
|
||||
// Create a bezier curve for self-loops
|
||||
const x = d.source.x;
|
||||
const y = d.source.y;
|
||||
const loopRadius = nodeRadius * 2; // Adjust size of the loop
|
||||
|
||||
// Create a circular path to the left of the node
|
||||
return `M ${x},${y - nodeRadius}
|
||||
C ${x - loopRadius},${y - nodeRadius - loopRadius}
|
||||
${x - loopRadius},${y + nodeRadius + loopRadius}
|
||||
${x},${y + nodeRadius}`;
|
||||
} else {
|
||||
// Regular straight line for normal edges
|
||||
return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
|
||||
}
|
||||
});
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
|
||||
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) {
|
||||
@ -209,7 +263,7 @@ function draw(pathway, elem) {
|
||||
}
|
||||
|
||||
// Wait before showing popup (ms)
|
||||
var popupWaitBeforeShow = 1000;
|
||||
var popupWaitBeforeShow = 500;
|
||||
|
||||
// Custom popover element
|
||||
let popoverTimeout = null;
|
||||
@ -563,6 +617,8 @@ function draw(pathway, elem) {
|
||||
// Apply zoom to the SVG element - this enables wheel zoom
|
||||
svg.call(zoom);
|
||||
|
||||
svg.call(zoom.scaleBy, initialzoom);
|
||||
|
||||
// Also apply zoom to container to catch events that might not reach SVG
|
||||
// This ensures drag-to-pan works even when clicking on empty space
|
||||
container.call(zoom);
|
||||
@ -598,43 +654,68 @@ function draw(pathway, elem) {
|
||||
.on("tick", ticked);
|
||||
|
||||
// Kanten zeichnen
|
||||
const link = zoomable.append("g")
|
||||
.selectAll("line")
|
||||
.data(links)
|
||||
.enter().append("line")
|
||||
// Check if target is pseudo and draw marker only if not pseudo
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)')
|
||||
const linkGroup = zoomable.append("g")
|
||||
.selectAll("g")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "link-group");
|
||||
|
||||
const link = linkGroup
|
||||
.append("path")
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||
.attr("fill", "none")
|
||||
|
||||
// Check if target is pseudo and draw marker only if not pseudo
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => {
|
||||
if (d.target.pseudo) return '';
|
||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||
}
|
||||
)
|
||||
.attr("fill", "none")
|
||||
.on("click", function(event, d) {
|
||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||
|
||||
d3.selectAll("line").classed("highlighted", false);
|
||||
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
if (!wasHighlighted) {
|
||||
const toHighlight = [];
|
||||
toHighlight.push(d.el);
|
||||
const toHighlight = [];
|
||||
toHighlight.push(d.el);
|
||||
|
||||
if (d.source.pseudo || d.target.pseudo) {
|
||||
if (d.target.pseudo) {
|
||||
d3.selectAll("line").each(e => {
|
||||
if (e !== undefined && e.source.id === d.target.id) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3.selectAll("line").each(e => {
|
||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
if (d.source.pseudo || d.target.pseudo) {
|
||||
if (d.target.pseudo) {
|
||||
d3.selectAll("path").each(e => {
|
||||
if (e !== undefined && e.source.id === d.target.id) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3.selectAll("path").each(e => {
|
||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
||||
toHighlight.push(e.el);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of toHighlight) {
|
||||
d3.select(e).classed("highlighted", true);
|
||||
}
|
||||
for (const e of toHighlight) {
|
||||
d3.select(e).classed("highlighted", true);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const linkText = linkGroup
|
||||
.append("text")
|
||||
.attr("class", "link-label")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", -6)
|
||||
.style("font-size", "4px")
|
||||
.style("pointer-events", "none")
|
||||
.style("display", "none")
|
||||
.text(d => d.name)
|
||||
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||
|
||||
// add element to links array
|
||||
link.each(function (d) {
|
||||
@ -651,16 +732,41 @@ function draw(pathway, elem) {
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended))
|
||||
.on("end", dragended)
|
||||
)
|
||||
.on("click", function (event, d) {
|
||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||
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);
|
||||
|
||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||
|
||||
if (!wasHighlighted) {
|
||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||
if (!wasHighlighted) {
|
||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||
const inEdges = findPaths(d.id, links);
|
||||
const outEdges = []
|
||||
// Colorize out edges green
|
||||
for (const l of links) {
|
||||
if (l.source.id === d.id) {
|
||||
outEdges.push(l);
|
||||
if (l.target.pseudo) {
|
||||
for (const l2 of links) {
|
||||
if (l.target.id === l2.source.id) {
|
||||
outEdges.push(l2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const e of inEdges) {
|
||||
d3.select(e.el).classed("inedge", true);
|
||||
}
|
||||
|
||||
for (const e of outEdges) {
|
||||
d3.select(e.el).classed("outedge", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Kreise für die Knoten hinzufügen
|
||||
node.append("circle")
|
||||
@ -734,14 +840,14 @@ function draw(pathway, elem) {
|
||||
|
||||
function serializeSVG(svgElement) {
|
||||
|
||||
svgElement.querySelectorAll("line.link").forEach(line => {
|
||||
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||
const style = getComputedStyle(line);
|
||||
line.setAttribute("stroke", style.stroke);
|
||||
line.setAttribute("stroke-width", style.strokeWidth);
|
||||
line.setAttribute("fill", style.fill);
|
||||
});
|
||||
|
||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
||||
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
|
||||
const style = getComputedStyle(line);
|
||||
line.setAttribute("stroke", style.stroke);
|
||||
line.setAttribute("stroke-width", style.strokeWidth);
|
||||
|
||||
@ -3,73 +3,73 @@
|
||||
{% block page_title %}Packages{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
id="new-package-button"
|
||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
id="new-package-button"
|
||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
||||
>
|
||||
<path d="M12 10v6" />
|
||||
<path d="M9 13h6" />
|
||||
<path
|
||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm">
|
||||
Import
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
||||
class="lucide lucide-chevron-down ml-1"
|
||||
>
|
||||
<path d="M12 10v6" />
|
||||
<path d="M9 13h6" />
|
||||
<path
|
||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||
/>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm">
|
||||
Import
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down ml-1"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from JSON
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from legacy JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from JSON
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
|
||||
>
|
||||
Import Package from legacy JSON
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block action_button %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
{% if meta.can_edit or not meta.url_contains_package %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
|
||||
@ -65,11 +65,11 @@
|
||||
},
|
||||
|
||||
get showMlrr() {
|
||||
return this.selectedType === 'mlrr';
|
||||
return this.selectedType === 'ml-relative-reasoning';
|
||||
},
|
||||
|
||||
get showRbrr() {
|
||||
return this.selectedType === 'rbrr';
|
||||
return this.selectedType === 'rule-based-relative-reasoning';
|
||||
},
|
||||
|
||||
get showEnviformer() {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
reactionImageUrl: '',
|
||||
pes: false,
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
@ -15,20 +16,30 @@
|
||||
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
|
||||
const productsSelect = document.getElementById('add_pathway_edge_products');
|
||||
|
||||
const pesLinks = [];
|
||||
|
||||
const substrates = [];
|
||||
for (const option of substratesSelect.selectedOptions) {
|
||||
substrates.push(option.dataset.smiles);
|
||||
if (option.dataset.pes === 'true') {
|
||||
pesLinks.push(option.dataset.pes);
|
||||
}
|
||||
}
|
||||
|
||||
const products = [];
|
||||
for (const option of productsSelect.selectedOptions) {
|
||||
products.push(option.dataset.smiles);
|
||||
if (option.dataset.pes === 'true') {
|
||||
pesLinks.push(option.dataset.pes);
|
||||
}
|
||||
}
|
||||
|
||||
if (substrates.length > 0 && products.length > 0) {
|
||||
const reaction = substrates.join('.') + '>>' + products.join('.');
|
||||
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
|
||||
this.pes = pesLinks.length > 0;
|
||||
} else {
|
||||
this.pes = false;
|
||||
this.reactionImageUrl = '';
|
||||
}
|
||||
},
|
||||
@ -106,6 +117,7 @@
|
||||
{% for n in pathway.nodes %}
|
||||
<option
|
||||
data-smiles="{{ n.default_node_label.smiles }}"
|
||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
||||
value="{{ n.url }}"
|
||||
>
|
||||
{{ n.default_node_label.name|safe }}
|
||||
@ -132,6 +144,7 @@
|
||||
{% for n in pathway.nodes %}
|
||||
<option
|
||||
data-smiles="{{ n.default_node_label.smiles }}"
|
||||
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
|
||||
value="{{ n.url }}"
|
||||
>
|
||||
{{ n.default_node_label.name|safe }}
|
||||
@ -144,6 +157,9 @@
|
||||
|
||||
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
|
||||
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
|
||||
<div x-show="pes">
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
class="modal"
|
||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||
@modal-opened.window="
|
||||
const links = d3.selectAll('line.highlighted');
|
||||
console.log(links);
|
||||
const links = d3.selectAll('path.highlighted');
|
||||
if (!links.empty()) {
|
||||
const el = links.node();
|
||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||
@ -18,9 +17,7 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
|
||||
}
|
||||
"
|
||||
@close="reset()"
|
||||
|
||||
@ -6,17 +6,14 @@
|
||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||
@modal-opened.window="
|
||||
const el = d3.select('circle.highlighted').node();
|
||||
|
||||
if (el !== null) {
|
||||
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
||||
|
||||
for (let option of selectElement.options) {
|
||||
if (option.value === el.__data__.url) {
|
||||
option.selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
}
|
||||
"
|
||||
|
||||
@ -82,6 +82,9 @@
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
|
||||
</div>
|
||||
{% if edge.edge_label.contains_pes %}
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Reaction Description -->
|
||||
@ -92,7 +95,7 @@
|
||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||
{% for educt in edge.start_nodes.all %}
|
||||
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
|
||||
>{{ educt.name }}</a
|
||||
>{{ educt.get_name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<svg
|
||||
@ -112,7 +115,7 @@
|
||||
</svg>
|
||||
{% for product in edge.end_nodes.all %}
|
||||
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
|
||||
>{{ product.name }}</a
|
||||
>{{ product.get_name }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@ -56,8 +56,10 @@
|
||||
<ul class="menu bg-base-200 rounded-box">
|
||||
{% for um in group.user_member.all %}
|
||||
<li>
|
||||
<a href="{% if not user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
|
||||
>{{ um.username }}
|
||||
<a
|
||||
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
||||
class="hover:bg-base-300"
|
||||
>{{ um.username }}
|
||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||
>
|
||||
</li>
|
||||
|
||||
@ -55,6 +55,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
{% if node.description and node.description != "no description" %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Description</div>
|
||||
<div class="collapse-content">{{ node.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||
|
||||
{% for tpl in viz_templates %}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: 0.6;
|
||||
stroke-width: 1.5px;
|
||||
/* marker-end: url(#arrow); */
|
||||
}
|
||||
|
||||
@ -72,6 +73,15 @@
|
||||
stroke: red;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.inedge {
|
||||
stroke: red;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.outedge {
|
||||
stroke: green;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
</style>
|
||||
<script src="{% static 'js/pw.js' %}"></script>
|
||||
|
||||
@ -111,7 +121,7 @@
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Graphical Representation
|
||||
</div>
|
||||
<div class="collapse-content ">
|
||||
<div class="collapse-content">
|
||||
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
||||
<div class="navbar bg-base-100 rounded-lg">
|
||||
<div class="flex-1">
|
||||
@ -168,6 +178,52 @@
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||
>
|
||||
<li>
|
||||
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
id="compound-names-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-eye"
|
||||
>
|
||||
<path
|
||||
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Compound Names
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
id="reaction-names-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-eye"
|
||||
>
|
||||
<path
|
||||
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Reaction Names
|
||||
</a>
|
||||
</li>
|
||||
{% if pathway.setting.model.app_domain %}
|
||||
<li>
|
||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||
@ -193,6 +249,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 1 == 0 %}
|
||||
<li>
|
||||
<a id="timeseries-toggle-button" class="cursor-pointer">
|
||||
<svg
|
||||
@ -243,6 +300,7 @@
|
||||
Show Predicted Properties
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -367,6 +425,18 @@
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||
</marker>
|
||||
<marker
|
||||
id="curve-arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="10"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
|
||||
</marker>
|
||||
<marker
|
||||
id="doublearrow"
|
||||
viewBox="0 0 20 30"
|
||||
@ -474,6 +544,10 @@
|
||||
{{ pathway.d3_json|json_script:"pathway" }}
|
||||
|
||||
<script>
|
||||
// Global switch for compound names view
|
||||
var compoundNamesViewEnabled = false;
|
||||
// Gloabl switch for reaction names view
|
||||
var reactionNamesViewEnabled = false;
|
||||
// Global switch for app domain view
|
||||
var appDomainViewEnabled = false;
|
||||
// Global switch for timeseries view
|
||||
@ -509,6 +583,67 @@
|
||||
descContent.innerHTML = newDesc;
|
||||
}
|
||||
|
||||
const compoundNamesBtn = document.getElementById("compound-names-toggle-button");
|
||||
if (compoundNamesBtn) {
|
||||
compoundNamesBtn.addEventListener("click", function () {
|
||||
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||
const icon = document.getElementById("compound-names-icon");
|
||||
|
||||
if (compoundNamesViewEnabled) {
|
||||
// Change to eye-off icon
|
||||
icon.innerHTML =
|
||||
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||
|
||||
nodes.forEach((x) => {
|
||||
if (x.name) {
|
||||
d3.select(x.el)
|
||||
.append("text")
|
||||
.text(d => d.name)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("y", -20)
|
||||
.style("font-size", "4px");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Change back to eye icon
|
||||
icon.innerHTML =
|
||||
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||
|
||||
nodes.forEach((x) => {
|
||||
d3.select(x.el)
|
||||
.select("text")
|
||||
.remove()
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const reactionNamesBtn = document.getElementById("reaction-names-toggle-button");
|
||||
if (reactionNamesBtn) {
|
||||
reactionNamesBtn.addEventListener("click", function () {
|
||||
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||
const icon = document.getElementById("reaction-names-icon");
|
||||
|
||||
if (reactionNamesViewEnabled) {
|
||||
// Change to eye-off icon
|
||||
icon.innerHTML =
|
||||
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||
|
||||
d3.selectAll(".link-label")
|
||||
.style("display", null);
|
||||
|
||||
} else {
|
||||
// Change back to eye icon
|
||||
icon.innerHTML =
|
||||
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||
|
||||
d3.selectAll(".link-label")
|
||||
.style("display", "none");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App domain toggle
|
||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||
if (appDomainBtn) {
|
||||
|
||||
@ -79,6 +79,9 @@
|
||||
<div class="collapse-content">
|
||||
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
|
||||
</div>
|
||||
{% if reaction.contains_pes %}
|
||||
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Reaction Description -->
|
||||
|
||||
@ -24,6 +24,76 @@
|
||||
<td>Setting Name</td>
|
||||
<td>{{ setting_to_render.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Setting URL</td>
|
||||
<td>
|
||||
<a href="{{ setting_to_render.url }}" class="link link-primary">{{ setting_to_render.url }}</a>
|
||||
<div
|
||||
x-data="{
|
||||
value: '{{ setting_to_render.url }}',
|
||||
copied: false,
|
||||
|
||||
async copy() {
|
||||
await navigator.clipboard.writeText(this.value)
|
||||
this.copied = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
}, 1500)
|
||||
}
|
||||
}"
|
||||
class="join"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
:data-tip="copied ? 'Copied!' : 'Copy'"
|
||||
@click="copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg
|
||||
x-show="!copied"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2"
|
||||
/>
|
||||
<rect
|
||||
width="12"
|
||||
height="12"
|
||||
x="8"
|
||||
y="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
x-show="copied"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% if setting_to_render.description %}
|
||||
<tr>
|
||||
<td>Setting Description</td>
|
||||
|
||||
@ -158,7 +158,6 @@
|
||||
document.getElementById("predict-submit-button").innerText = "Predict";
|
||||
}
|
||||
}
|
||||
|
||||
const radioButtons = document.querySelectorAll('input[name="predict"]');
|
||||
radioButtons.forEach(radio => {
|
||||
radio.addEventListener('change', radioChange);
|
||||
@ -213,7 +212,13 @@
|
||||
e.preventDefault();
|
||||
const button = this;
|
||||
button.disabled = true;
|
||||
button.textContent = "Predicting...";
|
||||
|
||||
// Set text depending on mode
|
||||
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
||||
button.textContent = "Building...";
|
||||
} else {
|
||||
button.textContent = "Predicting...";
|
||||
}
|
||||
|
||||
// Get SMILES from either input or Ketcher
|
||||
const smilesInput = document.getElementById("predict-smiles");
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from django.conf import settings as s
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, CompoundStructure
|
||||
|
||||
@ -33,13 +35,13 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(c.description, "No Desc")
|
||||
|
||||
def test_missing_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||
|
||||
def test_smiles_are_trimmed(self):
|
||||
@ -96,7 +98,7 @@ class CompoundTest(TestCase):
|
||||
self.assertEqual(len(self.package.compounds), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Compound.create(
|
||||
self.package,
|
||||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django.conf import settings as s
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from epdb.exceptions import InvalidSMILESException
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, Reaction, Rule
|
||||
|
||||
@ -163,7 +164,7 @@ class ReactionTest(TestCase):
|
||||
self.assertEqual(len(self.package.reactions), 1)
|
||||
|
||||
def test_wrong_smiles(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(InvalidSMILESException):
|
||||
_ = Reaction.create(
|
||||
package=self.package,
|
||||
name="Eawag BBD reaction r0001",
|
||||
|
||||
@ -46,6 +46,14 @@ class ModelViewTest(TestCase):
|
||||
)
|
||||
|
||||
expected = [
|
||||
{
|
||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||
"probability": 0.75,
|
||||
"btrule": {
|
||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||
"name": "bt0001-3568",
|
||||
},
|
||||
},
|
||||
{
|
||||
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
||||
"probability": 0.25,
|
||||
@ -62,14 +70,6 @@ class ModelViewTest(TestCase):
|
||||
"name": "bt0243-4301",
|
||||
},
|
||||
},
|
||||
{
|
||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||
"probability": 0.75,
|
||||
"btrule": {
|
||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||
"name": "bt0001-3568",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
actual = response.json()["pred"]
|
||||
|
||||
1497
utilities/misc.py
1497
utilities/misc.py
File diff suppressed because it is too large
Load Diff
2
uv.lock
generated
2
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.4.2"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#ad825570480bbe2f1a35c04923fede756c450751" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user