6 Commits

Author SHA1 Message Date
c4067cb11e flip boolean
Some checks failed
API CI / api-tests (pull_request) Failing after 16s
CI / test (pull_request) Failing after 31s
2026-06-19 09:55:25 +02:00
a2761dcaaf Add Edge check for pes 2026-06-19 09:52:39 +02:00
831496992c Added viz hint if PES is part of reaction 2026-06-19 09:32:34 +02:00
297c87f4d9 missing import
Some checks failed
CI / test (pull_request) Failing after 15s
API CI / api-tests (pull_request) Failing after 19s
2026-06-16 22:38:46 +02:00
5d08ee9760 auth log, bb4g fix 2026-06-16 22:38:46 +02:00
4a4ebf6955 adjusted migration
Initial bayer app

Show Pack Classification

Adjusted docker compose to bayer specifics

Adjusted Dockerfile for Bayer

Adding secret flags to group, add secret pools to packages

Adjusted View for Package creation

Prep configs, added Package Create Modal

wip

More on PES

wip

wip

Wip

minor

PW interactions

API PES

wip

Make Select Widget reflect required

make required generallay available

Update UI if pathway mode is set to build

Added ais

circle adjustments

Initial Zoom, fix AD Creation

wip
2026-06-16 22:38:45 +02:00
34 changed files with 1099 additions and 1612132 deletions

View File

@ -37,7 +37,7 @@ RUN mkdir -p -m 0700 /root/.ssh \
# We'll need access to private repos, let docker make use of host ssh agent and use it like: # We'll need access to private repos, let docker make use of host ssh agent and use it like:
# docker build --ssh default -t envipath/envipy-bayer:1.0 . # docker build --ssh default -t envipath/envipy-bayer:1.0 .
RUN --mount=type=ssh \ RUN --mount=type=ssh \
uv sync --locked --extra ms-login --extra pepper-plugin uv sync --locked --extra pepper-plugin
# Now copy source and do a final sync to install the project itself # Now copy source and do a final sync to install the project itself
# Ensure .dockerignore is reasonable # Ensure .dockerignore is reasonable

View File

@ -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

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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,25 @@ 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: if current_package.classification_level != Package.Classification.SECRET:
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.") return BadRequest("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 +65,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,18 +75,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: if current_package.classification_level != Package.Classification.SECRET:
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.") return BadRequest("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)
@ -116,7 +109,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

View File

@ -89,13 +89,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}") 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:
@ -170,7 +171,7 @@ class BB4G(Classifier):
} }
retries = 0 retries = 0
while retries < 100: while retries < 5:
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
proxies=s.PROXIES or None) proxies=s.PROXIES or None)
@ -178,7 +179,7 @@ class BB4G(Classifier):
retries += 1 retries += 1
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds") logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
import time import time
time.sleep(3) time.sleep(60)
continue continue
resp.raise_for_status() resp.raise_for_status()

View File

@ -29,10 +29,10 @@ services:
container_name: epbiotransformer3 container_name: epbiotransformer3
celery_worker: celery_worker:
image: git.envipath.com/envipath/envipy-bayer:1.2 image: git.envipath.com/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/

View File

@ -354,7 +354,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

View File

@ -1,5 +0,0 @@
class InvalidSMILESException(Exception):
pass
class PackageImportException(Exception):
pass

View File

@ -633,14 +633,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 +709,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):
@ -1693,15 +1687,14 @@ 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") 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 +1707,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")
@ -2385,31 +2392,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!"
}

View File

@ -1078,8 +1078,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 +1898,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 {

View File

@ -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"]])

View File

@ -23,7 +23,6 @@ MAPPING = {
"true": HalfLifeModel.SFO, "true": HalfLifeModel.SFO,
"SFO-SFO": HalfLifeModel.SFO_SFO, "SFO-SFO": HalfLifeModel.SFO_SFO,
"DFOP-SFO": HalfLifeModel.DFOP_SFO, "DFOP-SFO": HalfLifeModel.DFOP_SFO,
"other": HalfLifeModel.OTHER,
} }

View File

@ -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"
),
),
]

View File

@ -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,
@ -637,7 +636,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 +659,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 +785,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 +862,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 +880,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 +890,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 +1150,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 +1169,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 +1186,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 +1383,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 +1394,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 +1622,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 +1642,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,19 +1698,14 @@ 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 description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@ -2262,12 +2209,10 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
depth_map[0] = list() depth_map[0] = list()
processed = set() processed = set()
data_driven_root_nodes = ( data_driven_root_nodes = self.node_set.all().annotate(
self.node_set.all() prod_cnt=Count('edge_products'),
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts")) educt_cnt=Count('edge_educts')
.filter(prod_cnt=0, educt_cnt__gt=0) ).filter(prod_cnt=0, educt_cnt__gt=0).distinct()
.distinct()
)
# Eval QuerySet # Eval QuerySet
root_nodes_by_depth = list(self.root_nodes) root_nodes_by_depth = list(self.root_nodes)
@ -2360,12 +2305,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 +2319,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,11 +2359,6 @@ 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):
@ -2453,26 +2387,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)

View File

@ -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"])
@ -3029,15 +3001,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 +3024,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
) )

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -81,7 +81,6 @@ def migration(request):
"bt0416-4269", "bt0416-4269",
"bt0432-4254", "bt0432-4254",
"bt0337-4117", "bt0337-4117",
"bt0322-3393",
] ]
if request.method == "GET": if request.method == "GET":

View File

@ -34,12 +34,3 @@
} }
@import "./daisyui-theme.css"; @import "./daisyui-theme.css";
select.select[multiple] {
display: block;
white-space: normal;
}
p a {
@apply underline;
}

View File

@ -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,

View File

@ -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",
@ -67,9 +35,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);
@ -157,10 +122,6 @@ function draw(pathway, elem) {
}); });
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 +224,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;
@ -654,68 +615,48 @@ 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("path")
.data(links) .data(links)
.enter() .enter().append("path")
.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 => {
const link = linkGroup if (d.target.pseudo) return '';
.append("path") if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)") })
.attr("fill", "none") .attr("fill", "none")
.on("click", function(event, d) {
// 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"); const wasHighlighted = d3.select(this).classed("highlighted");
d3.selectAll("path").classed("highlighted", false);
d3.selectAll("path").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("path").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("path").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 +673,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 +756,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);

View File

@ -6,6 +6,7 @@
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('path.highlighted');
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()"

View File

@ -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'));
} }
" "

View File

@ -56,10 +56,8 @@
<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="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" >{{ um.username }}
class="hover:bg-base-300"
>{{ um.username }}
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a {% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
> >
</li> </li>

View File

@ -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 %}

View File

@ -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>
@ -544,10 +486,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 +521,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) {

View File

@ -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>

View File

@ -212,13 +212,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");

View File

@ -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",

View File

@ -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",

View File

@ -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"]

File diff suppressed because it is too large Load Diff