3 Commits

Author SHA1 Message Date
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
38 changed files with 1113 additions and 1612164 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:
# docker build --ssh default -t envipath/envipy-bayer:1.0 .
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
# Ensure .dockerignore is reasonable

View File

@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
def create(
compound: Compound,
pes_link: str,
molfile: str,
mol_file: str,
smiles: str,
name: str = None,
description: str = None,
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
cs.smiles = smiles
cs.molfile = molfile
cs.mol_file = mol_file
cs.pes_link = pes_link
cs.compound = compound

View File

@ -3,11 +3,7 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">
<p>
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
</p>
</div>
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
</div>
<!-- Image Representation -->

View File

@ -3,11 +3,7 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">
<p>
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
</p>
</div>
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
</div>
<!-- Image Representation -->

View File

@ -3,11 +3,7 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Link to PES</div>
<div class="collapse-content">
<p>
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
</p>
</div>
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
</div>
<!-- Image Representation -->

View File

@ -2,13 +2,14 @@ import base64
import requests
from django.conf import settings as s
from django.http import HttpResponse, HttpResponseBadRequest
from django.core.exceptions import BadRequest
from django.http import HttpResponse
from django.shortcuts import redirect
from bayer.models import PESCompound
from epdb.logic import PackageManager
from epdb.models import Pathway, Node
from epdb.views import _anonymous_or_real, error
from epdb.views import _anonymous_or_real
from utilities.decorators import package_permission_required
Package = s.GET_PACKAGE_MODEL()
@ -22,11 +23,7 @@ def create_pes(request, package_uuid):
if request.method == "POST":
if current_package.classification_level == Package.Classification.INTERNAL:
return error(
request,
f'Creation of PESs for package {current_package.name} failed!',
"Creating PESs for internal packages is not allowed.",
)
raise BadRequest("Cannot create PESs for internal packages.")
compound_name = request.POST.get('compound-name')
compound_description = request.POST.get('compound-description')
@ -36,25 +33,25 @@ def create_pes(request, package_uuid):
try:
pes_data = fetch_pes(request, pes_link)
except ValueError as e:
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
return BadRequest(f"Could not fetch PES data for {pes_link}")
classification = pes_data.get("classificationLevel", "")
if "secret" == classification.lower():
if current_package.classification_level != Package.Classification.SECRET:
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
return BadRequest("Cannot create PESs for non-secret packages.")
data_pools = pes_data.get("dataPools")
if data_pools:
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
return HttpResponseBadRequest(
return BadRequest(
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
return redirect(pes.url)
else:
return HttpResponseBadRequest("Please provide a PES link.")
return BadRequest("Please provide a PES link.")
else:
pass
@ -68,11 +65,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
if request.method == "POST":
if current_package.classification_level == Package.Classification.INTERNAL:
return error(
request,
f'Creation of PESs for package {current_package.name} failed!',
"Creating PESs for internal packages is not allowed.",
)
raise BadRequest("Cannot create PESs for internal packages.")
compound_name = request.POST.get('compound-name')
compound_description = request.POST.get('compound-description')
@ -82,18 +75,18 @@ def create_pes_node(request, package_uuid, pathway_uuid):
try:
pes_data = fetch_pes(request, pes_link)
except ValueError as e:
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
return BadRequest(f"Could not fetch PES data for {pes_link}")
classification = pes_data.get("classificationLevel", "")
if "secret" == classification.lower():
if current_package.classification_level != Package.Classification.SECRET:
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
return BadRequest("Cannot create PESs for non-secret packages.")
data_pools = pes_data.get("dataPools")
if data_pools:
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
return HttpResponseBadRequest(
return BadRequest(
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
@ -116,7 +109,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
return redirect(current_pathway.url)
else:
return HttpResponseBadRequest("Please provide a PES link.")
return BadRequest("Please provide a PES link.")
else:
pass
@ -125,9 +118,6 @@ def fetch_pes(request, pes_url) -> dict:
from epauth.views import get_access_token_from_request
token = get_access_token_from_request(request)
if token is None:
token = pes_url.split('/')[-1] == 'dummy'
if token:
for k, v in s.PES_API_MAPPING.items():
if pes_url.startswith(k):

View File

@ -89,13 +89,14 @@ class BB4G(Classifier):
}
started = False
while not started:
retries = 0
while not started and retries < 5:
res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None)
logger.info(f"Starting BB4G: {res.status_code}")
if res.status_code == 200:
started = True
elif res.status_code in [500, 502]:
retries += 1
import time
time.sleep(5)
else:
@ -170,7 +171,7 @@ class BB4G(Classifier):
}
retries = 0
while retries < 100:
while retries < 5:
resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data),
proxies=s.PROXIES or None)
@ -178,7 +179,7 @@ class BB4G(Classifier):
retries += 1
logger.info(f"BB4G predict hit a 418, retrying in 60 seconds")
import time
time.sleep(3)
time.sleep(60)
continue
resp.raise_for_status()

View File

@ -28,11 +28,22 @@ services:
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: git.envipath.com/envipath/envipy-bayer:1.2
image: git.envipath.com/envipath/envipy-bayer:1.0
container_name: epcelery
env_file:
- .env
- .env.dev
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
volumes:
- ep_bayer_data:/opt/enviPy/

View File

@ -354,7 +354,7 @@ DEFAULT_MODEL_PARAMS = {
"num_chains": 10,
}
DEFAULT_MAX_NUMBER_OF_NODES = 9999
DEFAULT_MAX_NUMBER_OF_NODES = 50
DEFAULT_MAX_DEPTH = 8
DEFAULT_MODEL_THRESHOLD = 0.25

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")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
structures: List["CompoundStructureSchema"] = []
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_pes_link(obj: Compound):
return getattr(obj.default_structure, "pes_link", None)
@staticmethod
def resolve_review_status(obj: Compound):
def resolve_review_status(obj: CompoundStructure):
return "reviewed" if obj.package.reviewed else "unreviewed"
@staticmethod
@ -714,7 +709,6 @@ class CompoundStructureSchema(Schema):
reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
smiles: str = Field(None, alias="smiles")
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_review_status(obj: CompoundStructure):
@ -1693,15 +1687,14 @@ class PathwayNode(Schema):
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
id: str = Field(None, alias="url")
idcomp: str = Field(None, alias="node_label_id")
idreact: str = Field(None, alias="node_label_id")
idcomp: str = Field(None, alias="default_node_label.url")
idreact: str = Field(None, alias="default_node_label.url")
image: str = Field(None, alias="image")
imageSize: int = Field(None, alias="image_size")
name: str = Field(None, alias="name")
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
smiles: str = Field(None, alias="smiles")
smiles: str = Field(None, alias="default_node_label.smiles")
pseudo: bool = Field(False, alias="pseudo")
pesLink: str | None = Field(None, alias="pes_link")
@staticmethod
def resolve_atom_count(obj: Node):
@ -1714,10 +1707,24 @@ class PathwayNode(Schema):
# TODO
return []
@staticmethod
def resolve_engineered_intermediate(obj: Node):
# TODO
return False
@staticmethod
def resolve_image(obj: Node):
return f"{obj.default_node_label.url}?image=svg"
@staticmethod
def resolve_image_size(obj: Node):
return 400
@staticmethod
def resolve_proposed_intermediate(obj: Node):
# TODO
return []
class PathwaySchema(Schema):
aliases: List[str] = Field([], alias="aliases")
@ -2385,31 +2392,3 @@ def get_setting(request, setting_uuid):
return 403, {
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
}
########
# Util #
########
class NonPersistent(Schema):
smiles: str
setting_url: str = Field(..., alias="settingUri")
@router.post("/util", response={200: Any, 403: Error})
def predict(request, np: Form[NonPersistent]):
try:
print(request.user)
print(np.setting_url)
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
from epdb.logic import SPathway
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
spw.predict()
return spw.to_json()
except ValueError:
return 403, {
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
}

View File

@ -1078,8 +1078,10 @@ class PackageManager(object):
data: Dict[str, Any],
owner: User,
preserve_uuids=False,
add_import_timestamp=True,
trust_reviewed=False,
) -> Package:
importer = PackageImporter(data, preserve_uuids)
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
imported_package = importer.do_import()
up = UserPackagePermission()
@ -1896,12 +1898,6 @@ class SPathway(object):
"to": to_indices,
}
if edge.rule:
e["rule"] = edge.rule.simple_json()
if edge.probability:
e["probability"] = edge.probability
edges.append(e)
return {

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.save()
def import_package(self, data, owner, all_envipath_user_group):
p = PackageManager.import_legacy_package(
def import_package(self, data, owner):
return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
)
PackageManager.grant_read(owner, p, all_envipath_user_group)
return p
def create_default_setting(self, owner, packages):
s = SettingManager.create_setting(
owner,
@ -202,7 +198,7 @@ class Command(BaseCommand):
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
).read()
)
imported_package = self.import_package(package_data, admin, g)
imported_package = self.import_package(package_data, admin)
mapping[p.replace(".json", "")] = imported_package
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])

View File

@ -23,7 +23,6 @@ MAPPING = {
"true": HalfLifeModel.SFO,
"SFO-SFO": HalfLifeModel.SFO_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.dto import RunResult, PropertyPrediction
from epdb.exceptions import InvalidSMILESException
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
from utilities.ml import (
ApplicabilityDomainPCA,
@ -577,10 +576,6 @@ class ReactionIdentifierMixin(ExternalIdentifierMixin):
def get_uniprot_identifiers(self):
return self.get_external_identifier("UniProt")
def contains_pes(self):
from bayer.models import PESStructure
return any([isinstance(o, PESStructure) for o in self.educts.all()]) or any(
[isinstance(o, PESStructure) for o in self.products.all()])
##############
# EP Objects #
@ -637,7 +632,7 @@ class EnviPathModel(TimeStampedModel):
class AliasMixin(models.Model):
aliases = ArrayField(
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
)
@transaction.atomic
@ -660,9 +655,7 @@ class AliasMixin(models.Model):
class ScenarioMixin(models.Model):
scenarios = models.ManyToManyField(
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
)
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
@transaction.atomic
def set_scenarios(self, scenarios: List["Scenario"]):
@ -788,7 +781,7 @@ class Compound(
"CompoundStructure",
verbose_name="Default Structure",
related_name="compound_default_structure",
on_delete=models.SET_NULL,
on_delete=models.CASCADE,
null=True,
)
@ -865,17 +858,13 @@ class Compound(
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
) -> "Compound":
if smiles is None or smiles.strip() == "":
raise InvalidSMILESException("SMILES is required")
raise ValueError("SMILES is required")
smiles = smiles.strip()
parsed = FormatConverter.from_smiles(smiles)
if parsed is None:
raise InvalidSMILESException("Given SMILES is invalid")
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
raise ValueError("Given SMILES is invalid")
standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
@ -887,14 +876,7 @@ class Compound(
# Check if we find a direct match for a given SMILES
if qs.exists():
found_structure = qs.first()
found_compound = found_structure.compound
if name:
found_structure.add_alias(name)
found_compound.add_alias(name)
return found_compound
return qs.first().compound
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
@ -904,19 +886,16 @@ class Compound(
# Check if we can find the standardized one
if qs.exists():
# TODO should we add a structure?
found_structure = qs.first()
found_compound = found_structure.compound
if name:
found_structure.add_alias(name)
found_compound.add_alias(name)
return found_compound
return qs.first().compound
# Generate Compound
c = Compound()
c.package = package
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
@ -1167,27 +1146,18 @@ class CompoundStructure(
@staticmethod
@transaction.atomic
def create(
compound: Compound, smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
compound: Compound, smiles: str, name: str = None, description: str = None, *args, **kwargs
):
# Clean for potential XSS
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists():
found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles)
if name:
found_cs.add_alias(name)
return found_cs
return CompoundStructure.objects.get(compound=compound, smiles=smiles)
if compound.pk is None:
raise ValueError("Unpersisted Compound! Persist compound first!")
cs = CompoundStructure()
# Clean for potential XSS
if name is not None:
cs.name = name
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None:
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@ -1195,10 +1165,6 @@ class CompoundStructure(
cs.smiles = smiles
cs.compound = compound
# Check if molfile is present and valid
if molfile is not None and FormatConverter.from_molfile(molfile) is not None:
cs.molfile = molfile
if "normalized_structure" in kwargs:
cs.normalized_structure = kwargs["normalized_structure"]
@ -1216,8 +1182,6 @@ class CompoundStructure(
@property
def as_svg(self, width: int = 800, height: int = 400):
if self.molfile is not None and self.molfile.strip() != "":
return IndigoUtils.mol_to_svg(self.molfile, width=width, height=height)
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
@property
@ -1415,9 +1379,6 @@ class SimpleAmbitRule(SimpleRule):
if not FormatConverter.is_valid_smirks(smirks):
raise ValueError(f'SMIRKS "{smirks}" is invalid!')
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks)
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
@ -1429,17 +1390,14 @@ class SimpleAmbitRule(SimpleRule):
if query.exists():
if query.count() > 1:
logger.error(f"More than one rule matched this one! {query}")
found_rule = query.first()
if name:
found_rule.add_alias(name)
return found_rule
return query.first()
r = SimpleAmbitRule()
r.package = package
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
@ -1660,15 +1618,10 @@ class Reaction(
products = models.ManyToManyField(
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
)
rules = models.ManyToManyField(
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
)
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
medline_references = ArrayField(
models.TextField(blank=False, null=False),
null=True,
verbose_name="Medline References",
blank=True,
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
)
external_identifiers = GenericRelation("ExternalIdentifier")
@ -1685,13 +1638,8 @@ class Reaction(
educts: Union[List[str], List[CompoundStructure]] = None,
products: Union[List[str], List[CompoundStructure]] = None,
rules: Union[Rule | List[Rule]] = None,
multi_step: bool = False,
multi_step: bool = True,
):
# Clean for potential XSS
if name is not None and name.strip() != "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
_educts = []
_products = []
@ -1746,19 +1694,14 @@ class Reaction(
logger.error(
f"Found more than one reaction for given input! {existing_reaction_qs}"
)
found_reaction = existing_reaction_qs.first()
if name:
found_reaction.add_alias(name)
return found_reaction
return existing_reaction_qs.first()
r = Reaction()
r.package = package
if r is not None:
r.name = name
# Clean for potential XSS
if name is not None and name.strip() != "":
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@ -2262,12 +2205,10 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
depth_map[0] = list()
processed = set()
data_driven_root_nodes = (
self.node_set.all()
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts"))
.filter(prod_cnt=0, educt_cnt__gt=0)
.distinct()
)
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)
@ -2360,12 +2301,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
"node_label_id": self.default_node_label.url,
"image": f"{self.url}?image=svg",
"image_svg": IndigoUtils.mol_to_svg(
self.default_node_label.molfile
if self.default_node_label.molfile is not None
and self.default_node_label.molfile.strip()
else self.default_node_label.smiles,
width=40,
height=40,
self.default_node_label.smiles, width=40, height=40
),
"image_type": "svg",
"name": self.get_name(),
@ -2379,7 +2315,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
},
"predicted_properties": predicted_properties,
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
"proposed": self.is_proposed_intermediate(),
"timeseries": self.get_timeseries_data(),
**structure_data,
}
@ -2420,11 +2355,6 @@ 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):
@ -2453,26 +2383,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
return data
def is_proposed_intermediate(self):
collected = defaultdict(dict)
for ai in self.additional_information.filter(
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"]
, scenario__isnull=False
):
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
if ai.type == "ProposedIntermediate":
collected[str(ai.scenario.uuid)]["proposed"] = True
if ai.type == "Confidence":
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
if ai.type == "TransformationProductImportance":
collected[str(ai.scenario.uuid)]["Transformation product importance"] = ai.get().importance.value
return list(collected.values())
def simple_json(self, include_description=False):
res = super().simple_json()
name = res.get("name", None)

View File

@ -19,7 +19,6 @@ from sentry_sdk import capture_exception
from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
from .exceptions import InvalidSMILESException
from .logic import (
EPDBURLParser,
@ -786,11 +785,6 @@ def models(request):
{"Model": s.SERVER_URL + "/model"},
]
context["entity_type"] = "model"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
# Keep model_types for potential modal/action use
context["model_types"] = {
"ML Relative Reasoning": {
@ -803,14 +797,12 @@ def models(request):
"requires_rule_packages": True,
"requires_data_packages": True,
},
}
if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = {
"EnviFormer": {
"type": "enviformer",
"requires_rule_packages": False,
"requires_data_packages": True,
}
},
}
if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items():
@ -818,9 +810,6 @@ def models(request):
"type": k,
"requires_rule_packages": v.requires_rule_packages(),
"requires_data_packages": v.requires_data_packages(),
"additional_parameters": v.Config.__name__.lower()
if v.Config.__name__ != ""
else None,
}
for k, v in s.PROPERTY_PLUGINS.items():
context["model_types"][v.display()] = {
@ -829,6 +818,12 @@ def models(request):
"requires_data_packages": v.requires_data_packages(),
}
# Context for paginated template
context["entity_type"] = "model"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models"
return render(request, "collections/models_paginated.html", context)
elif request.method == "POST":
@ -943,12 +938,13 @@ def package_models(request, package_uuid):
"requires_data_packages": True,
},
}
if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = {
"type": "enviformer",
"requires_rule_packages": False,
"requires_data_packages": True,
}
},
if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items():
@ -1124,11 +1120,6 @@ def package_model(request, package_uuid, model_uuid):
}
)
# Sort data by prob desc
res["pred"] = sorted(
res["pred"], key=lambda x: x["probability"], reverse=True
)
return JsonResponse(res, safe=False)
elif half_life:
@ -1232,7 +1223,9 @@ def package(request, package_uuid):
if request.method == "GET":
if request.GET.get("export", False) == "true":
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
pack_json = PackageManager.export_package(current_package)
pack_json = PackageManager.export_package(
current_package, include_models=False, include_external_identifiers=False
)
response = JsonResponse(pack_json, content_type="application/json")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
@ -2368,13 +2361,7 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
node_description = request.POST.get("node-description")
node_smiles = request.POST.get("node-smiles").strip()
try:
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
except InvalidSMILESException:
return error(
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
)
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
return redirect(current_pathway.url)
@ -2482,22 +2469,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
return JsonResponse({"success": current_node.url})
new_node_name = request.POST.get("node-name")
new_node_description = request.POST.get("node-description")
if any([new_node_name, new_node_description]):
if new_node_name is not None and new_node_name.strip() != "":
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
current_node.name = new_node_name
if new_node_description is not None and new_node_description.strip() != "":
new_node_description = nh3.clean(new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
current_node.description = new_node_description
current_node.save()
return redirect(current_node.url)
return error(request, "Node update failed!", "No changes were made to the node")
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(["GET", "POST"])
@ -3029,15 +3001,9 @@ def settings(request):
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
# min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
temp_max_nodes = request.POST.get("prediction-setting-max-nodes")
if temp_max_nodes is None or temp_max_nodes == "" or int(temp_max_nodes) == -1:
temp_max_nodes = s.DEFAULT_MAX_NUMBER_OF_NODES
else:
temp_max_nodes = int(request.POST.get("prediction-setting-max-nodes", 1))
max_nodes = min(
max(
temp_max_nodes,
int(request.POST.get("prediction-setting-max-nodes", 1)),
2,
),
s.DEFAULT_MAX_NUMBER_OF_NODES,
@ -3058,7 +3024,6 @@ def settings(request):
model_uuid = model_url.split("/")[-1]
params["model"] = EPModel.objects.get(uuid=model_uuid)
# TODO Check if removed if request contains "" or not at all
params["model_threshold"] = request.POST.get(
"model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
)

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",
"bt0432-4254",
"bt0337-4117",
"bt0322-3393",
]
if request.method == "GET":

View File

@ -34,12 +34,3 @@
}
@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', () => {
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,

View File

@ -1,37 +1,5 @@
console.log("loaded pw.js")
function findPaths(source_idx, links) {
const resultLinks = new Set();
const visited = new Set();
// Helper function for depth-first search
function dfs(current) {
if (visited.has(current)) {
return;
}
if (links.filter(link => link.target.id === current).length === 0) {
return;
}
visited.add(current);
links.forEach(link => {
if (
link.target.id === current &&
!visited.has(link.source.id) &&
link.source.id !== link.target.id // Avoid self-loops
) {
resultLinks.add(link); // Add link to result
dfs(link.source.id);
}
});
visited.delete(current);
}
// Start DFS
dfs(source_idx);
return Array.from(resultLinks);
}
function predictFromNode(url) {
fetch("", {
method: "POST",
@ -67,9 +35,6 @@ function draw(pathway, elem) {
const horizontalSpacing = 75; // horizontal space between nodes
const depthMap = new Map();
// Avoid leaving unconnected Nodes leaving the viewport
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
// Sort nodes by depth first to minimize crossings
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
@ -157,10 +122,6 @@ function draw(pathway, elem) {
});
node.attr("transform", d => `translate(${d.x},${d.y})`);
linkText
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
}
function dragstarted(event, d) {
@ -263,7 +224,7 @@ function draw(pathway, elem) {
}
// Wait before showing popup (ms)
var popupWaitBeforeShow = 500;
var popupWaitBeforeShow = 1000;
// Custom popover element
let popoverTimeout = null;
@ -654,68 +615,48 @@ function draw(pathway, elem) {
.on("tick", ticked);
// Kanten zeichnen
const linkGroup = zoomable.append("g")
.selectAll("g")
.data(links)
.enter()
.append("g")
.attr("class", "link-group");
const link = linkGroup
.append("path")
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
.attr("fill", "none")
// Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
.attr("marker-end", d => {
if (d.target.pseudo) return '';
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
}
)
.attr("fill", "none")
.on("click", function(event, d) {
const link = zoomable.append("g")
.selectAll("path")
.data(links)
.enter().append("path")
// 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("path").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("path").each(e => {
if (e !== undefined && e.source.id === d.target.id) {
toHighlight.push(e.el);
if (d.source.pseudo || d.target.pseudo) {
if (d.target.pseudo) {
d3.selectAll("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);
}
});
}
});
} else {
d3.selectAll("path").each(e => {
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
toHighlight.push(e.el);
}
});
}
}
for (const e of toHighlight) {
d3.select(e).classed("highlighted", true);
}
for (const e of toHighlight) {
d3.select(e).classed("highlighted", true);
}
}
});
const linkText = linkGroup
.append("text")
.attr("class", "link-label")
.attr("text-anchor", "middle")
.attr("dy", -6)
.style("font-size", "4px")
.style("pointer-events", "none")
.style("display", "none")
.text(d => d.name)
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
})
// add element to links array
link.each(function (d) {
@ -732,41 +673,16 @@ function draw(pathway, elem) {
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.on("end", dragended))
.on("click", function (event, d) {
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
d3.selectAll('circle.highlighted').classed('highlighted', false);
d3.selectAll("path").classed("inedge", false);
d3.selectAll("path").classed("outedge", false);
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
if (!wasHighlighted) {
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
const inEdges = findPaths(d.id, links);
const outEdges = []
// Colorize out edges green
for (const l of links) {
if (l.source.id === d.id) {
outEdges.push(l);
if (l.target.pseudo) {
for (const l2 of links) {
if (l.target.id === l2.source.id) {
outEdges.push(l2);
}
}
}
}
}
d3.selectAll('circle.highlighted').classed('highlighted', false);
for (const e of inEdges) {
d3.select(e.el).classed("inedge", true);
if (!wasHighlighted) {
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
}
for (const e of outEdges) {
d3.select(e.el).classed("outedge", true);
}
}
});
})
// Kreise für die Knoten hinzufügen
node.append("circle")
@ -840,14 +756,14 @@ function draw(pathway, elem) {
function serializeSVG(svgElement) {
svgElement.querySelectorAll("path.link").forEach(line => {
svgElement.querySelectorAll("line.link").forEach(line => {
const style = getComputedStyle(line);
line.setAttribute("stroke", style.stroke);
line.setAttribute("stroke-width", style.strokeWidth);
line.setAttribute("fill", style.fill);
});
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
const style = getComputedStyle(line);
line.setAttribute("stroke", style.stroke);
line.setAttribute("stroke-width", style.strokeWidth);

View File

@ -5,7 +5,6 @@
x-data="{
isSubmitting: false,
reactionImageUrl: '',
pes: false,
reset() {
this.isSubmitting = false;
@ -16,30 +15,20 @@
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
const productsSelect = document.getElementById('add_pathway_edge_products');
const pesLinks = [];
const substrates = [];
for (const option of substratesSelect.selectedOptions) {
substrates.push(option.dataset.smiles);
if (option.dataset.pes === 'true') {
pesLinks.push(option.dataset.pes);
}
}
const products = [];
for (const option of productsSelect.selectedOptions) {
products.push(option.dataset.smiles);
if (option.dataset.pes === 'true') {
pesLinks.push(option.dataset.pes);
}
}
if (substrates.length > 0 && products.length > 0) {
const reaction = substrates.join('.') + '>>' + products.join('.');
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
this.pes = pesLinks.length > 0;
} else {
this.pes = false;
this.reactionImageUrl = '';
}
},
@ -117,7 +106,6 @@
{% for n in pathway.nodes %}
<option
data-smiles="{{ n.default_node_label.smiles }}"
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
value="{{ n.url }}"
>
{{ n.default_node_label.name|safe }}
@ -144,7 +132,6 @@
{% for n in pathway.nodes %}
<option
data-smiles="{{ n.default_node_label.smiles }}"
data-pes="{% if n.default_node_label.pes_link %}true{% else %}false{% endif %}"
value="{{ n.url }}"
>
{{ n.default_node_label.name|safe }}
@ -157,9 +144,6 @@
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
<div x-show="pes">
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
</div>
</div>
</form>
</div>

View File

@ -6,6 +6,7 @@
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
@modal-opened.window="
const links = d3.selectAll('path.highlighted');
if (!links.empty()) {
const el = links.node();
const selectElement = document.getElementById('delete_pathway_edge_edges');
@ -17,7 +18,9 @@
break;
}
}
selectElement.dispatchEvent(new Event('change'));
}
"
@close="reset()"

View File

@ -6,14 +6,17 @@
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
@modal-opened.window="
const el = d3.select('circle.highlighted').node();
if (el !== null) {
const selectElement = document.getElementById('delete_pathway_node_nodes');
for (let option of selectElement.options) {
if (option.value === el.__data__.url) {
option.selected = true;
break;
}
}
selectElement.dispatchEvent(new Event('change'));
}
"

View File

@ -82,9 +82,6 @@
<div class="collapse-content">
<div class="flex justify-center">{{ edge.edge_label.as_svg|safe }}</div>
</div>
{% if edge.edge_label.contains_pes %}
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
{% endif %}
</div>
<!-- Reaction Description -->
@ -95,7 +92,7 @@
<div class="flex flex-wrap items-center justify-center gap-4">
{% for educt in edge.start_nodes.all %}
<a href="{{ educt.url }}" class="btn btn-outline btn-sm"
>{{ educt.get_name }}</a
>{{ educt.name }}</a
>
{% endfor %}
<svg
@ -115,7 +112,7 @@
</svg>
{% for product in edge.end_nodes.all %}
<a href="{{ product.url }}" class="btn btn-outline btn-sm"
>{{ product.get_name }}</a
>{{ product.name }}</a
>
{% endfor %}
</div>

View File

@ -56,10 +56,8 @@
<ul class="menu bg-base-200 rounded-box">
{% for um in group.user_member.all %}
<li>
<a
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
class="hover:bg-base-300"
>{{ um.username }}
<a href="{% if not user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
>{{ um.username }}
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
>
</li>

View File

@ -55,15 +55,6 @@
</div>
</div>
<!-- Description -->
{% if node.description and node.description != "no description" %}
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Description</div>
<div class="collapse-content">{{ node.description }}</div>
</div>
{% endif %}
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
{% for tpl in viz_templates %}

View File

@ -21,7 +21,6 @@
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
/* marker-end: url(#arrow); */
}
@ -73,15 +72,6 @@
stroke: red;
stroke-width: 3px;
}
.inedge {
stroke: red;
stroke-width: 3px;
}
.outedge {
stroke: green;
stroke-width: 3px;
}
</style>
<script src="{% static 'js/pw.js' %}"></script>
@ -121,7 +111,7 @@
<div class="collapse-title text-xl font-medium">
Graphical Representation
</div>
<div class="collapse-content">
<div class="collapse-content ">
<div class="bg-base-100 mb-2 rounded-lg p-2">
<div class="navbar bg-base-100 rounded-lg">
<div class="flex-1">
@ -178,52 +168,6 @@
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
>
<li>
<a id="compound-names-toggle-button" class="cursor-pointer">
<svg
id="compound-names-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Compound Names
</a>
</li>
<li>
<a id="reaction-names-toggle-button" class="cursor-pointer">
<svg
id="reaction-names-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Reaction Names
</a>
</li>
{% if pathway.setting.model.app_domain %}
<li>
<a id="app-domain-toggle-button" class="cursor-pointer">
@ -249,7 +193,6 @@
</a>
</li>
{% endif %}
{% if 1 == 0 %}
<li>
<a id="timeseries-toggle-button" class="cursor-pointer">
<svg
@ -300,7 +243,6 @@
Show Predicted Properties
</a>
</li>
{% endif %}
</ul>
</div>
</div>
@ -544,10 +486,6 @@
{{ pathway.d3_json|json_script:"pathway" }}
<script>
// Global switch for compound names view
var compoundNamesViewEnabled = false;
// Gloabl switch for reaction names view
var reactionNamesViewEnabled = false;
// Global switch for app domain view
var appDomainViewEnabled = false;
// Global switch for timeseries view
@ -583,67 +521,6 @@
descContent.innerHTML = newDesc;
}
const compoundNamesBtn = document.getElementById("compound-names-toggle-button");
if (compoundNamesBtn) {
compoundNamesBtn.addEventListener("click", function () {
compoundNamesViewEnabled = !compoundNamesViewEnabled;
const icon = document.getElementById("compound-names-icon");
if (compoundNamesViewEnabled) {
// Change to eye-off icon
icon.innerHTML =
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
nodes.forEach((x) => {
if (x.name) {
d3.select(x.el)
.append("text")
.text(d => d.name)
.attr("text-anchor", "middle")
.attr("y", -20)
.style("font-size", "4px");
}
});
} else {
// Change back to eye icon
icon.innerHTML =
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
nodes.forEach((x) => {
d3.select(x.el)
.select("text")
.remove()
})
}
});
}
const reactionNamesBtn = document.getElementById("reaction-names-toggle-button");
if (reactionNamesBtn) {
reactionNamesBtn.addEventListener("click", function () {
reactionNamesViewEnabled = !reactionNamesViewEnabled;
const icon = document.getElementById("reaction-names-icon");
if (reactionNamesViewEnabled) {
// Change to eye-off icon
icon.innerHTML =
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
d3.selectAll(".link-label")
.style("display", null);
} else {
// Change back to eye icon
icon.innerHTML =
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
d3.selectAll(".link-label")
.style("display", "none");
}
});
}
// App domain toggle
const appDomainBtn = document.getElementById("app-domain-toggle-button");
if (appDomainBtn) {

View File

@ -79,9 +79,6 @@
<div class="collapse-content">
<div class="flex justify-center">{{ reaction.as_svg|safe }}</div>
</div>
{% if reaction.contains_pes %}
<span class='alert alert-info alert-soft'>The reaction contains a partially elucidated structure!</br>For visualization the representative structure is used. The pathway itself will show the actual partially elucidated structure.</span>
{% endif %}
</div>
<!-- Reaction Description -->

View File

@ -24,76 +24,6 @@
<td>Setting Name</td>
<td>{{ setting_to_render.name }}</td>
</tr>
<tr>
<td>Setting URL</td>
<td>
<a href="{{ setting_to_render.url }}" class="link link-primary">{{ setting_to_render.url }}</a>
<div
x-data="{
value: '{{ setting_to_render.url }}',
copied: false,
async copy() {
await navigator.clipboard.writeText(this.value)
this.copied = true
setTimeout(() => {
this.copied = false
}, 1500)
}
}"
class="join"
>
<button
type="button"
class="btn btn-ghost"
:data-tip="copied ? 'Copied!' : 'Copy'"
@click="copy"
aria-label="Copy to clipboard"
>
<svg
x-show="!copied"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2"
/>
<rect
width="12"
height="12"
x="8"
y="8"
rx="2"
ry="2"
/>
</svg>
<svg
x-show="copied"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</button>
</div>
</td>
</tr>
{% if setting_to_render.description %}
<tr>
<td>Setting Description</td>

View File

@ -212,13 +212,7 @@
e.preventDefault();
const button = this;
button.disabled = true;
// Set text depending on mode
if (document.getElementById("predict-submit-button").innerText === "Build") {
button.textContent = "Building...";
} else {
button.textContent = "Predicting...";
}
button.textContent = "Predicting...";
// Get SMILES from either input or Ketcher
const smilesInput = document.getElementById("predict-smiles");

View File

@ -1,7 +1,5 @@
from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.exceptions import InvalidSMILESException
from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure
@ -35,13 +33,13 @@ class CompoundTest(TestCase):
self.assertEqual(c.description, "No Desc")
def test_missing_smiles(self):
with self.assertRaises(InvalidSMILESException):
with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException):
with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException):
with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
def test_smiles_are_trimmed(self):
@ -98,7 +96,7 @@ class CompoundTest(TestCase):
self.assertEqual(len(self.package.compounds), 1)
def test_wrong_smiles(self):
with self.assertRaises(InvalidSMILESException):
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",

View File

@ -1,7 +1,6 @@
from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.exceptions import InvalidSMILESException
from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction, Rule
@ -164,7 +163,7 @@ class ReactionTest(TestCase):
self.assertEqual(len(self.package.reactions), 1)
def test_wrong_smiles(self):
with self.assertRaises(InvalidSMILESException):
with self.assertRaises(ValueError):
_ = Reaction.create(
package=self.package,
name="Eawag BBD reaction r0001",

View File

@ -46,14 +46,6 @@ class ModelViewTest(TestCase):
)
expected = [
{
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
"probability": 0.75,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
"name": "bt0001-3568",
},
},
{
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
"probability": 0.25,
@ -70,6 +62,14 @@ class ModelViewTest(TestCase):
"name": "bt0243-4301",
},
},
{
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
"probability": 0.75,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
"name": "bt0001-3568",
},
},
]
actual = response.json()["pred"]

File diff suppressed because it is too large Load Diff

2
uv.lock generated
View File

@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
[[package]]
name = "envipy-additional-information"
version = "0.4.2"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#ad825570480bbe2f1a35c04923fede756c450751" }
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
dependencies = [
{ name = "pydantic" },
]