[Fix] UI bugs, Registrations Mail, BTRules Popup, Legacy API fixes (#309)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#309
This commit is contained in:
2026-01-29 11:13:34 +13:00
parent ab0b5a5186
commit 5565b9cb9e
14 changed files with 391 additions and 154 deletions

View File

@ -48,11 +48,6 @@ runs:
shell: bash
run: |
uv run python scripts/pnpm_wrapper.py install
cat << 'EOF' > pnpm-workspace.yaml
onlyBuiltDependencies:
- '@parcel/watcher'
- '@tailwindcss/oxide'
EOF
uv run python scripts/pnpm_wrapper.py run build
- name: Wait for Postgres

View File

@ -212,7 +212,7 @@ def get_user(request, user_uuid):
class GroupMember(Schema):
id: str = Field(None, alias="url")
id: str
identifier: str
name: str
@ -228,7 +228,7 @@ class GroupSchema(Schema):
members: List[GroupMember] = Field([], alias="members")
name: str = Field(None, alias="name")
ownerid: str = Field(None, alias="owner.url")
ownername: str = Field(None, alias="owner.name")
ownername: str = Field(None, alias="owner.get_name")
packages: List["SimplePackage"] = Field([], alias="packages")
readers: List[GroupMember] = Field([], alias="readers")
writers: List[GroupMember] = Field([], alias="writers")
@ -237,10 +237,10 @@ class GroupSchema(Schema):
def resolve_members(obj: Group):
res = []
for member in obj.user_member.all():
res.append(GroupMember(id=member.url, identifier="usermember", name=member.username))
res.append(GroupMember(id=member.url, identifier="usermember", name=member.get_name()))
for member in obj.group_member.all():
res.append(GroupMember(id=member.url, identifier="groupmember", name=member.name))
res.append(GroupMember(id=member.url, identifier="groupmember", name=member.get_name()))
return res
@ -374,7 +374,7 @@ class PackageSchema(Schema):
).values_list("user", flat=True)
).distinct()
return [{u.id: u.name} for u in users]
return [{u.id: u.get_name()} for u in users]
@staticmethod
def resolve_writers(obj: Package):
@ -384,7 +384,7 @@ class PackageSchema(Schema):
).values_list("user", flat=True)
).distinct()
return [{u.id: u.name} for u in users]
return [{u.id: u.get_name()} for u in users]
@staticmethod
def resolve_review_comment(obj):
@ -966,7 +966,12 @@ def create_package_simple_rule(
raise ValueError("Not yet implemented!")
else:
sr = SimpleAmbitRule.create(
p, r.name, r.description, r.smirks, r.reactantFilterSmarts, r.productFilterSmarts
p,
r.name,
r.description,
r.smirks,
r.reactantFilterSmarts,
r.productFilterSmarts,
)
return redirect(sr.url)
@ -1119,7 +1124,7 @@ class ReactionSchema(Schema):
name: str = Field(None, alias="name")
pathways: List["SimplePathway"] = Field([], alias="related_pathways")
products: List["ReactionCompoundStructure"] = Field([], alias="products")
references: List[Dict[str, List[str]]] = Field([], alias="references")
references: Dict[str, List[str]] = Field({}, alias="references")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
smirks: str = Field("", alias="smirks")
@ -1135,8 +1140,12 @@ class ReactionSchema(Schema):
@staticmethod
def resolve_references(obj: Reaction):
# TODO
return []
rhea_refs = []
for rhea in obj.get_rhea_identifiers():
rhea_refs.append(f"{rhea.identifier_value}")
# TODO UniProt
return {"rheaReferences": rhea_refs, "uniprotCount": []}
@staticmethod
def resolve_medline_references(obj: Reaction):
@ -1715,7 +1724,7 @@ class EdgeSchema(Schema):
id: str = Field(None, alias="url")
identifier: str = "edge"
name: str = Field(None, alias="name")
reactionName: str = Field(None, alias="edge_label.name")
reactionName: str = Field(None, alias="edge_label.get_name")
reactionURI: str = Field(None, alias="edge_label.url")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
@ -1764,7 +1773,7 @@ class CreateEdge(Schema):
@router.post(
"/package/{uuid:package_uuid}/üathway/{uuid:pathway_uuid}/edge",
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge",
response={200: str | Any, 403: Error},
)
def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
@ -1783,10 +1792,26 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
if e.edgeAsSmirks:
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
educts.append(Node.objects.get(pathway=pw, default_node_label__smiles=ed))
stand_ed = FormatConverter.standardize(ed, remove_stereo=True)
educts.append(
Node.objects.get(
pathway=pw,
default_node_label=CompoundStructure.objects.get(
compound__package=p, smiles=stand_ed
).compound.default_structure,
)
)
for pr in e.edgeAsSmirks.split(">>")[1].split("\\."):
products.append(Node.objects.get(pathway=pw, default_node_label__smiles=pr))
stand_pr = FormatConverter.standardize(pr, remove_stereo=True)
products.append(
Node.objects.get(
pathway=pw,
default_node_label=CompoundStructure.objects.get(
compound__package=p, smiles=stand_pr
).compound.default_structure,
)
)
else:
for ed in e.educts.split(","):
educts.append(Node.objects.get(pathway=pw, url=ed.strip()))
@ -1799,7 +1824,7 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
start_nodes=educts,
end_nodes=products,
rule=None,
name=e.name,
name=None,
description=e.edgeReason,
)
@ -1936,7 +1961,7 @@ def get_model(request, package_uuid, model_uuid, c: Query[Classify]):
if pr.rule:
res["id"] = pr.rule.url
res["identifier"] = pr.rule.get_rule_identifier()
res["name"] = pr.rule.name
res["name"] = pr.rule.get_name()
res["reviewStatus"] = (
"reviewed" if pr.rule.package.reviewed else "unreviewed"
)

View File

@ -8,7 +8,6 @@ from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager
from epdb.models import (
UserSettingPermission,
MLRelativeReasoning,
EnviFormer,
Permission,
User,
ExternalDatabase,
@ -231,7 +230,6 @@ class Command(BaseCommand):
package=pack,
rule_packages=[mapping["EAWAG-BBD"]],
data_packages=[mapping["EAWAG-BBD"]],
eval_packages=[],
threshold=0.5,
name="ECC - BBD - T0.5",
description="ML Relative Reasoning",
@ -239,7 +237,3 @@ class Command(BaseCommand):
ml_model.build_dataset()
ml_model.build_model()
# If available, create EnviFormerModel
if s.ENVIFORMER_PRESENT:
EnviFormer.create(pack, "EnviFormer - T0.5", "EnviFormer Model with Threshold 0.5", 0.5)

View File

@ -47,7 +47,7 @@ class Command(BaseCommand):
"description": model.description,
"kv": model.kv,
"data_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"eval_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"eval_packages_uuids": [str(p.uuid) for p in model.eval_packages.all()],
"threshold": model.threshold,
"eval_results": model.eval_results,
"multigen_eval": model.multigen_eval,

View File

@ -77,6 +77,9 @@ class User(AbstractUser):
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
def get_name(self):
return self.username
def save(self, *args, **kwargs):
if not self.url:
self.url = self._url()
@ -208,7 +211,10 @@ class Group(TimeStampedModel):
)
def __str__(self):
return f"{self.name} (pk={self.pk})"
return f"{self.get_name()} (pk={self.pk})"
def get_name(self):
return self.name
def save(self, *args, **kwargs):
if not self.url:
@ -596,7 +602,7 @@ class EnviPathModel(TimeStampedModel):
res = {
"url": self.url,
"uuid": str(self.uuid),
"name": self.name,
"name": self.get_name(),
}
if include_description:
@ -609,11 +615,14 @@ class EnviPathModel(TimeStampedModel):
return self.kv.get(k, default)
return default
def get_name(self):
return self.name
class Meta:
abstract = True
def __str__(self):
return f"{self.name} (pk={self.pk})"
return f"{self.get_name()} (pk={self.pk})"
class AliasMixin(models.Model):
@ -624,7 +633,7 @@ class AliasMixin(models.Model):
@transaction.atomic
def add_alias(self, new_alias, set_as_default=False):
if set_as_default:
self.aliases.append(self.name)
self.aliases.append(self.get_name())
self.name = new_alias
if new_alias in self.aliases:
@ -765,15 +774,17 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
logger.debug(
f"#Structures: {num_structs} - #Standardized SMILES: {len(stand_smiles)}"
)
logger.debug(f"Couldn't infer normalized structure for {self.name} - {self.url}")
logger.debug(
f"Couldn't infer normalized structure for {self.get_name()} - {self.url}"
)
raise ValueError(
f"Couldn't find nor infer normalized structure for {self.name} ({self.url})"
f"Couldn't find nor infer normalized structure for {self.get_name()} ({self.url})"
)
else:
cs = CompoundStructure.create(
self,
stand_smiles.pop(),
name="Normalized structure of {}".format(self.name),
name="Normalized structure of {}".format(self.get_name()),
description="{} (in its normalized form)".format(self.description),
normalized_structure=True,
)
@ -848,8 +859,10 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
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}"
c.name = name
# We have a default here only set the value if it carries some payload
@ -978,7 +991,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
cs = CompoundStructure.create(
existing_normalized_compound,
structure.smiles,
name=structure.name,
name=structure.get_name(),
description=structure.description,
normalized_structure=structure.normalized_structure,
)
@ -989,13 +1002,13 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
else:
raise ValueError(
f"Found a CompoundStructure for {default_structure_smiles} but not for {normalized_structure_smiles} in target package {target.name}"
f"Found a CompoundStructure for {default_structure_smiles} but not for {normalized_structure_smiles} in target package {target.get_name()}"
)
else:
# Here we can safely use Compound.objects.create as we won't end up in a duplicate
new_compound = Compound.objects.create(
package=target,
name=self.name,
name=self.get_name(),
description=self.description,
kv=self.kv.copy() if self.kv else {},
)
@ -1011,7 +1024,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
canonical_smiles=structure.canonical_smiles,
inchikey=structure.inchikey,
normalized_structure=structure.normalized_structure,
name=structure.name,
name=structure.get_name(),
description=structure.description,
kv=structure.kv.copy() if structure.kv else {},
)
@ -1050,11 +1063,8 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
def half_lifes(self):
hls: Dict[Scenario, List[HalfLife]] = defaultdict(list)
for n in self.related_nodes:
for scen in n.scenarios.all().order_by("name"):
for ai in scen.get_additional_information():
if isinstance(ai, HalfLife):
hls[scen].append(ai)
for cs in self.structures:
hls.update(cs.half_lifes())
return dict(hls)
@ -1104,6 +1114,7 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
# Clean for potential XSS
if name is not None:
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()
@ -1147,6 +1158,21 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
def is_default_structure(self):
return self.compound.default_structure == self
@property
def related_nodes(self):
return Node.objects.filter(node_labels__in=[self], pathway__package=self.compound.package)
def half_lifes(self):
hls: Dict[Scenario, List[HalfLife]] = defaultdict(list)
for n in self.related_nodes:
for scen in n.scenarios.all().order_by("name"):
for ai in scen.get_additional_information():
if isinstance(ai, HalfLife):
hls[scen].append(ai)
return dict(hls)
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
@ -1218,7 +1244,7 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
if rule_type == SimpleAmbitRule:
new_rule = SimpleAmbitRule.create(
package=target,
name=self.name,
name=self.get_name(),
description=self.description,
smirks=self.smirks,
reactant_filter_smarts=self.reactant_filter_smarts,
@ -1232,7 +1258,7 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
elif rule_type == SimpleRDKitRule:
new_rule = SimpleRDKitRule.create(
package=target,
name=self.name,
name=self.get_name(),
description=self.description,
reaction_smarts=self.reaction_smarts,
)
@ -1250,7 +1276,7 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
new_rule = ParallelRule.create(
package=target,
simple_rules=new_srs,
name=self.name,
name=self.get_name(),
description=self.description,
)
@ -1624,9 +1650,11 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r = Reaction()
r.package = package
# Clean for potential XSS
if name is not None and name.strip() != "":
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and name.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
@ -1674,7 +1702,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
new_reaction = Reaction.create(
package=target,
name=self.name,
name=self.get_name(),
description=self.description,
educts=copied_reaction_educts,
products=copied_reaction_products,
@ -1830,7 +1858,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
# We shouldn't lose or make up nodes...
if len(nodes) != len(self.nodes):
logger.debug(f"{self.name}: Num Nodes {len(nodes)} vs. DB Nodes {len(self.nodes)}")
logger.debug(
f"{self.get_name()}: Num Nodes {len(nodes)} vs. DB Nodes {len(self.nodes)}"
)
links = [e.d3_json() for e in self.edges]
@ -1898,7 +1928,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
"isIncremental": self.kv.get("mode") == "incremental",
"isPredicted": self.kv.get("mode") == "predicted",
"lastModified": self.modified.strftime("%Y-%m-%d %H:%M:%S"),
"pathwayName": self.name,
"pathwayName": self.get_name(),
"reviewStatus": "reviewed" if self.package.reviewed else "unreviewed",
"scenarios": [],
"upToDate": True,
@ -1941,14 +1971,14 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
if include_pathway_url:
row.append(n.pathway.url)
row += [cs.smiles, cs.name, n.depth]
row += [cs.smiles, cs.get_name(), n.depth]
edges = self.edges.filter(end_nodes__in=[n])
if len(edges):
for e in edges:
_row = row.copy()
_row.append(e.kv.get("probability"))
_row.append(",".join([r.name for r in e.edge_label.rules.all()]))
_row.append(",".join([r.get_name() for r in e.edge_label.rules.all()]))
_row.append(",".join([r.url for r in e.edge_label.rules.all()]))
_row.append(e.start_nodes.all()[0].default_node_label.smiles)
rows.append(_row)
@ -1979,12 +2009,14 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
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"Pathway {Pathway.objects.filter(package=package).count() + 1}"
pw.name = name
if description is not None and description.strip() != "":
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
pw.predicted = predicted
pw.save()
@ -2009,7 +2041,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
# deduplicated
new_pathway = Pathway.objects.create(
package=target,
name=self.name,
name=self.get_name(),
description=self.description,
setting=self.setting, # TODO copy settings?
kv=self.kv.copy() if self.kv else {},
@ -2039,7 +2071,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
pathway=new_pathway,
default_node_label=copied_structure,
depth=node.depth,
name=node.name,
name=node.get_name(),
description=node.description,
kv=node.kv.copy() if node.kv else {},
)
@ -2063,7 +2095,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
new_edge = Edge.objects.create(
pathway=new_pathway,
edge_label=copied_reaction,
name=edge.name,
name=edge.get_name(),
description=edge.description,
kv=edge.kv.copy() if edge.kv else {},
)
@ -2123,6 +2155,18 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
def _url(self):
return "{}/node/{}".format(self.pathway.url, self.uuid)
def get_name(self):
non_generic_name = True
if self.name == "no name":
non_generic_name = False
return (
self.name
if non_generic_name
else f"{self.default_node_label.name} (taken from underlying structure)"
)
def d3_json(self):
app_domain_data = self.get_app_domain_assessment_data()
@ -2135,9 +2179,9 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
"image_svg": IndigoUtils.mol_to_svg(
self.default_node_label.smiles, width=40, height=40
),
"name": self.default_node_label.name,
"name": self.get_name(),
"smiles": self.default_node_label.smiles,
"scenarios": [{"name": s.name, "url": s.url} for s in self.scenarios.all()],
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
"app_domain": {
"inside_app_domain": app_domain_data["assessment"]["inside_app_domain"]
if app_domain_data
@ -2205,7 +2249,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
res = super().simple_json()
name = res.get("name", None)
if name == "no name":
res["name"] = self.default_node_label.name
res["name"] = self.default_node_label.get_name()
return res
@ -2229,18 +2273,24 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
def d3_json(self):
edge_json = {
"name": self.name,
"name": self.get_name(),
"id": self.url,
"url": self.url,
"image": self.url + "?image=svg",
"reaction": {"name": self.edge_label.name, "url": self.edge_label.url}
"reaction": {
"name": self.edge_label.get_name(),
"url": self.edge_label.url,
"rules": [
{"name": r.get_name(), "url": r.url} for r in self.edge_label.rules.all()
],
}
if self.edge_label
else None,
"multi_step": self.edge_label.multi_step if self.edge_label else False,
"reaction_probability": self.kv.get("probability"),
"start_node_urls": [x.url for x in self.start_nodes.all()],
"end_node_urls": [x.url for x in self.end_nodes.all()],
"scenarios": [{"name": s.name, "url": s.url} for s in self.scenarios.all()],
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
}
for n in self.start_nodes.all():
@ -2329,10 +2379,22 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
res = super().simple_json()
name = res.get("name", None)
if name == "no name":
res["name"] = self.edge_label.name
res["name"] = self.edge_label.get_name()
return res
def get_name(self):
non_generic_name = True
if self.name == "no name":
non_generic_name = False
return (
self.name
if non_generic_name
else f"{self.edge_label.name} (taken from underlying reaction)"
)
class EPModel(PolymorphicModel, EnviPathModel):
package = models.ForeignKey(
@ -2613,7 +2675,7 @@ class PackageBasedModel(EPModel):
root_compounds.append(pw.root_nodes[0].default_node_label)
else:
logger.info(
f"Skipping MG Eval of Pathway {pw.name} ({pw.uuid}) as it has no root compounds!"
f"Skipping MG Eval of Pathway {pw.get_name()} ({pw.uuid}) as it has no root compounds!"
)
# As we need a Model Instance in our setting, get a fresh copy from db, overwrite the serialized mode and
@ -2739,7 +2801,7 @@ class PackageBasedModel(EPModel):
pathways.append(pathway)
else:
logging.warning(
f"No root compound in pathway {pathway.name}, excluding from multigen evaluation"
f"No root compound in pathway {pathway.get_name()}, excluding from multigen evaluation"
)
# build lookup reaction -> {uuid1, uuid2} for overlap check
@ -3071,7 +3133,7 @@ class ApplicabilityDomain(EnviPathModel):
ad = ApplicabilityDomain()
ad.model = mlrr
# ad.uuid = mlrr.uuid
ad.name = f"AD for {mlrr.name}"
ad.name = f"AD for {mlrr.get_name()}"
ad.num_neighbours = num_neighbours
ad.reliability_threshold = reliability_threshold
ad.local_compatibilty_threshold = local_compatibility_threshold
@ -3355,7 +3417,7 @@ class EnviFormer(PackageBasedModel):
)
for smiles in smiles_list
]
logger.info(f"Submitting {canon_smiles} to {self.name}")
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
start = datetime.now()
products_list = self.model.predict_batch(canon_smiles)
end = datetime.now()
@ -3512,7 +3574,7 @@ class EnviFormer(PackageBasedModel):
root_node = p.root_nodes
if len(root_node) > 1:
logging.warning(
f"Pathway {p.name} has more than one root compound, only {root_node[0]} will be used"
f"Pathway {p.get_name()} has more than one root compound, only {root_node[0]} will be used"
)
root_node = ".".join(
[
@ -3632,7 +3694,7 @@ class EnviFormer(PackageBasedModel):
pathways.append(pathway)
else:
logging.warning(
f"No root compound in pathway {pathway.name}, excluding from multigen evaluation"
f"No root compound in pathway {pathway.get_name()}, excluding from multigen evaluation"
)
# build lookup reaction -> {uuid1, uuid2} for overlap check
@ -4038,6 +4100,6 @@ class JobLog(TimeStampedModel):
return self.task_result
def is_result_downloadable(self):
downloadable = ["batch_predict"]
downloadable = ["batch_predict", "identify_missing_rules"]
return self.job_name in downloadable

View File

@ -7,6 +7,7 @@ from uuid import uuid4
from celery import shared_task
from celery.utils.functional import LRUCache
from django.conf import settings as s
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone
from epdb.logic import SPathway
@ -73,7 +74,31 @@ def predict_simple(model_pk: int, smiles: str):
@shared_task(queue="background")
def send_registration_mail(user_pk: int):
pass
u = User.objects.get(id=user_pk)
tpl = """Welcome {username}!,
Thank you for your interest in enviPath.
The public system is intended for non-commercial use only.
We will review your account details and usually activate your account within 24 hours.
Once activated, you will be notified by email.
If we have any questions, we will contact you at this email address.
Best regards,
enviPath team"""
msg = EmailMultiAlternatives(
"Your enviPath account",
tpl.format(username=u.username),
"admin@envipath.org",
[u.email],
bcc=["admin@envipath.org"],
)
msg.send(fail_silently=False)
@shared_task(bind=True, queue="model")

View File

@ -1,7 +1,7 @@
import json
import logging
from typing import Any, Dict, List
from datetime import datetime
from typing import Any, Dict, List
import nh3
from django.conf import settings as s
@ -13,6 +13,7 @@ from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource
from sentry_sdk import capture_exception
from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
@ -34,6 +35,7 @@ from .models import (
EnviFormer,
EnzymeLink,
EPModel,
ExpansionSchemeChoice,
ExternalDatabase,
ExternalIdentifier,
Group,
@ -51,7 +53,6 @@ from .models import (
SimpleAmbitRule,
User,
UserPackagePermission,
ExpansionSchemeChoice,
)
logger = logging.getLogger(__name__)
@ -238,6 +239,15 @@ def register(request):
try:
u = UserManager.create_user(username, email, password)
logger.info(f"Created user {u.username} ({u.pk})")
try:
from .tasks import send_registration_mail
send_registration_mail.delay(u.pk)
except Exception as e:
logger.error(f"Failed to send registration mail to {u.email}: {e}")
capture_exception(e)
except Exception:
context["message"] = "Registration failed! Couldn't create User Account."
return render(request, "static/login.html", context)
@ -339,7 +349,7 @@ def breadcrumbs(
{"Package": s.SERVER_URL + "/package"},
]
if first_level_object is not None:
bread.append({first_level_object.name: first_level_object.url})
bread.append({first_level_object.get_name(): first_level_object.url})
if second_level_namespace is not None:
bread.append(
@ -350,7 +360,7 @@ def breadcrumbs(
)
if second_level_object is not None:
bread.append({second_level_object.name: second_level_object.url})
bread.append({second_level_object.get_name(): second_level_object.url})
if third_level_namespace is not None:
bread.append(
@ -361,7 +371,7 @@ def breadcrumbs(
)
if third_level_object is not None:
bread.append({third_level_object.name: third_level_object.url})
bread.append({third_level_object.get_name(): third_level_object.url})
return bread
@ -462,7 +472,7 @@ def package_predict_pathway(request, package_uuid):
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Predict Pathway"
context["title"] = f"enviPath - {current_package.get_name()} - Predict Pathway"
context["meta"]["current_package"] = current_package
return render(request, "predict_pathway.html", context)
@ -475,6 +485,10 @@ def packages(request):
context = get_base_context(request)
context["title"] = "enviPath - Packages"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Package": s.SERVER_URL + "/package"},
]
# Context for paginated template
context["entity_type"] = "package"
@ -529,6 +543,10 @@ def compounds(request):
context = get_base_context(request)
context["title"] = "enviPath - Compounds"
context["meta"]["current_package"] = context["meta"]["user"].default_package
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Compound": s.SERVER_URL + "/compound"},
]
# Context for paginated template
context["entity_type"] = "compound"
@ -759,7 +777,7 @@ def package_models(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Models"
context["title"] = f"enviPath - {current_package.get_name()} - Models"
context["meta"]["current_package"] = current_package
context["object_type"] = "model"
@ -781,7 +799,7 @@ def package_models(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_model_qs if current_package.reviewed else unreviewed_model_qs
)
@ -931,7 +949,7 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_model.name}"
context["title"] = f"enviPath - {current_package.get_name()} - {current_model.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "model"
@ -1009,7 +1027,7 @@ def package(request, package_uuid):
if request.method == "GET":
if request.GET.get("export", False) == "true":
filename = f"{current_package.name.replace(' ', '_')}_{current_package.uuid}.json"
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
pack_json = PackageManager.export_package(
current_package, include_models=False, include_external_identifiers=False
)
@ -1019,7 +1037,7 @@ def package(request, package_uuid):
return response
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name}"
context["title"] = f"enviPath - {current_package.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "package"
@ -1056,7 +1074,7 @@ def package(request, package_uuid):
if current_user.default_package == current_package:
return error(
request,
f'Package "{current_package.name}" is the default and cannot be deleted!',
f'Package "{current_package.get_name()}" is the default and cannot be deleted!',
"You cannot delete the default package. If you want to delete this package you have to set another default package first.",
)
@ -1154,7 +1172,7 @@ def package_compounds(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Compounds"
context["title"] = f"enviPath - {current_package.get_name()} - Compounds"
context["meta"]["current_package"] = current_package
context["object_type"] = "compound"
@ -1179,7 +1197,7 @@ def package_compounds(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_compound_qs
if current_package.reviewed
@ -1216,7 +1234,9 @@ def package_compound(request, package_uuid, compound_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_compound.name}"
context["title"] = (
f"enviPath - {current_package.get_name()} - {current_compound.get_name()}"
)
context["meta"]["current_package"] = current_package
context["object_type"] = "compound"
@ -1300,7 +1320,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = (
f"enviPath - {current_package.name} - {current_compound.name} - Structures"
f"enviPath - {current_package.get_name()} - {current_compound.get_name()} - Structures"
)
context["meta"]["current_package"] = current_package
@ -1309,7 +1329,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
current_package, "compound", current_compound, "structure"
)
context["entity_type"] = "structure"
context["page_title"] = f"{current_compound.name} - Structures"
context["page_title"] = f"{current_compound.get_name()} - Structures"
context["api_endpoint"] = (
f"/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
)
@ -1362,7 +1382,7 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
context = get_base_context(request)
context["title"] = (
f"enviPath - {current_package.name} - {current_compound.name} - {current_structure.name}"
f"enviPath - {current_package.get_name()} - {current_compound.get_name()} - {current_structure.get_name()}"
)
context["meta"]["current_package"] = current_package
@ -1468,7 +1488,7 @@ def package_rules(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Rules"
context["title"] = f"enviPath - {current_package.get_name()} - Rules"
context["meta"]["current_package"] = current_package
context["object_type"] = "rule"
@ -1490,7 +1510,7 @@ def package_rules(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_rule_qs if current_package.reviewed else unreviewed_rule_qs
)
@ -1580,7 +1600,7 @@ def package_rule(request, package_uuid, rule_uuid):
content_type="image/svg+xml",
)
context["title"] = f"enviPath - {current_package.name} - {current_rule.name}"
context["title"] = f"enviPath - {current_package.get_name()} - {current_rule.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "rule"
@ -1653,7 +1673,7 @@ def package_rule_enzymelink(request, package_uuid, rule_uuid, enzymelink_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_rule.name}"
context["title"] = f"enviPath - {current_package.get_name()} - {current_rule.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "enzyme"
@ -1676,7 +1696,7 @@ def package_reactions(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Reactions"
context["title"] = f"enviPath - {current_package.get_name()} - Reactions"
context["meta"]["current_package"] = current_package
context["object_type"] = "reaction"
@ -1700,7 +1720,7 @@ def package_reactions(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_reaction_qs
if current_package.reviewed
@ -1741,7 +1761,9 @@ def package_reaction(request, package_uuid, reaction_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_reaction.name}"
context["title"] = (
f"enviPath - {current_package.get_name()} - {current_reaction.get_name()}"
)
context["meta"]["current_package"] = current_package
context["object_type"] = "reaction"
@ -1824,7 +1846,7 @@ def package_pathways(request, package_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Pathways"
context["title"] = f"enviPath - {current_package.get_name()} - Pathways"
context["meta"]["current_package"] = current_package
context["object_type"] = "pathway"
@ -1846,7 +1868,7 @@ def package_pathways(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_pathway_qs
if current_package.reviewed
@ -1953,7 +1975,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
)
if request.GET.get("download", False) == "true":
filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv"
filename = f"{current_pathway.get_name().replace(' ', '_')}_{current_pathway.uuid}.csv"
csv_pw = current_pathway.to_csv()
response = HttpResponse(csv_pw, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
@ -1973,7 +1995,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
current_user, identify_missing_rules, [current_pathway.pk], rule_package.pk
)
filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv"
filename = f"{current_pathway.get_name().replace(' ', '_')}_{current_pathway.uuid}.csv"
response = HttpResponse(res, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
@ -1996,7 +2018,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
).get(uuid=pathway_uuid)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_pathway.name}"
context["title"] = f"enviPath - {current_package.get_name()} - {current_pathway.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "pathway"
@ -2008,9 +2030,9 @@ def package_pathway(request, package_uuid, pathway_uuid):
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Package": s.SERVER_URL + "/package"},
{current_package.name: current_package.url},
{current_package.get_name(): current_package.url},
{"Pathway": current_package.url + "/pathway"},
{current_pathway.name: current_pathway.url},
{current_pathway.get_name(): current_pathway.url},
]
return render(request, "objects/pathway.html", context)
@ -2097,16 +2119,18 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_pathway.name} - Nodes"
context["title"] = (
f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - Nodes"
)
context["meta"]["current_package"] = current_package
context["object_type"] = "node"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Package": s.SERVER_URL + "/package"},
{current_package.name: current_package.url},
{current_package.get_name(): current_package.url},
{"Pathway": current_package.url + "/pathway"},
{current_pathway.name: current_pathway.url},
{current_pathway.get_name(): current_pathway.url},
{"Node": current_pathway.url + "/node"},
]
@ -2122,7 +2146,7 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_node_qs if current_package.reviewed else unreviewed_node_qs
)
@ -2196,7 +2220,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
return HttpResponse(svg_data, content_type="image/svg+xml")
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_pathway.name}"
context["title"] = f"enviPath - {current_package.get_name()} - {current_pathway.get_name()}"
context["meta"]["current_package"] = current_package
context["object_type"] = "pathway"
@ -2204,11 +2228,11 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Package": s.SERVER_URL + "/package"},
{current_package.name: current_package.url},
{current_package.get_name(): current_package.url},
{"Pathway": current_package.url + "/pathway"},
{current_pathway.name: current_pathway.url},
{current_pathway.get_name(): current_pathway.url},
{"Node": current_pathway.url + "/node"},
{current_node.name: current_node.url},
{current_node.get_name(): current_node.url},
]
context["node"] = current_node
@ -2261,16 +2285,18 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_pathway.name} - Edges"
context["title"] = (
f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - Edges"
)
context["meta"]["current_package"] = current_package
context["object_type"] = "edge"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Package": s.SERVER_URL + "/package"},
{current_package.name: current_package.url},
{current_package.get_name(): current_package.url},
{"Pathway": current_package.url + "/pathway"},
{current_pathway.name: current_pathway.url},
{current_pathway.get_name(): current_pathway.url},
{"Edge": current_pathway.url + "/edge"},
]
@ -2286,7 +2312,7 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_edge_qs if current_package.reviewed else unreviewed_edge_qs
)
@ -2338,7 +2364,7 @@ def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
context = get_base_context(request)
context["title"] = (
f"enviPath - {current_package.name} - {current_pathway.name} - {current_edge.edge_label.name}"
f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - {current_edge.edge_label.get_name()}"
)
context["meta"]["current_package"] = current_package
@ -2391,11 +2417,11 @@ def package_scenarios(request, package_uuid):
"all", False
):
scens = Scenario.objects.filter(package=current_package).order_by("name")
res = [{"name": s_.name, "url": s_.url, "uuid": s_.uuid} for s_ in scens]
res = [{"name": s_.get_name(), "url": s_.url, "uuid": s_.uuid} for s_ in scens]
return JsonResponse(res, safe=False)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Scenarios"
context["title"] = f"enviPath - {current_package.get_name()} - Scenarios"
context["meta"]["current_package"] = current_package
context["object_type"] = "scenario"
@ -2419,7 +2445,7 @@ def package_scenarios(request, package_uuid):
return JsonResponse(
{
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed}
{"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
for pw in (
reviewed_scenario_qs
if current_package.reviewed
@ -2511,7 +2537,9 @@ def package_scenario(request, package_uuid, scenario_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_scenario.name}"
context["title"] = (
f"enviPath - {current_package.get_name()} - {current_scenario.get_name()}"
)
context["meta"]["current_package"] = current_package
context["object_type"] = "scenario"
@ -2748,13 +2776,13 @@ def group(request, group_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_group.name}"
context["title"] = f"enviPath - {current_group.get_name()}"
context["object_type"] = "group"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Group": s.SERVER_URL + "/group"},
{current_group.name: current_group.url},
{current_group.get_name(): current_group.url},
]
context["group"] = current_group
@ -2909,13 +2937,13 @@ def setting(request, setting_uuid):
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_setting.name}"
context["title"] = f"enviPath - {current_setting.get_name()}"
context["object_type"] = "setting"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Setting": s.SERVER_URL + "/setting"},
{f"{current_setting.name}": current_setting.url},
{f"{current_setting.get_name()}": current_setting.url},
]
context["setting"] = current_setting
@ -2964,8 +2992,8 @@ def jobs(request):
target_package = PackageManager.create_package(
current_user,
f"Autogenerated Package for Pathway Engineering of {pathway_to_engineer.name}",
f"This Package was generated automatically for the engineering Task of {pathway_to_engineer.name}.",
f"Autogenerated Package for Pathway Engineering of {pathway_to_engineer.get_name()}",
f"This Package was generated automatically for the engineering Task of {pathway_to_engineer.get_name()}.",
)
from .tasks import dispatch, engineer_pathways
@ -3019,7 +3047,7 @@ def jobs(request):
"This Package was generated automatically for the batch prediction task.",
)
from .tasks import dispatch, batch_predict
from .tasks import batch_predict, dispatch
res = dispatch(
current_user,
@ -3057,6 +3085,8 @@ def job(request, job_uuid):
if job.job_name == "batch_predict":
filename = f"{job.job_name.replace(' ', '_')}_{job.task_id}.csv"
elif job.job_name == "identify_missing_rules":
filename = f"{job.job_name.replace(' ', '_')}_{job.task_id}.csv"
else:
raise BadRequest("Result is not downloadable!")

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- '@tailwindcss/oxide'

View File

@ -393,7 +393,14 @@ function draw(pathway, elem) {
}
function edge_popup(e) {
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br>";
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
if (e.reaction.rules) {
console.log(e.reaction.rules);
for (var rule of e.reaction.rules) {
popupContent += "Rule <a href='" + rule.url + "'>" + rule.name + "</a><br>";
}
}
if (e.app_domain) {
adcontent = "<p>";

View File

@ -176,8 +176,8 @@
href="#"
class="example-link hover:text-primary cursor-pointer"
title="load example"
@click.prevent="loadExample('CC(C)CC1=CC=C(C=C1)C(C)C(=O)O', $el)"
>Ibuprofen</a
@click.prevent="loadExample('COC(=O)[C@H](CC1=CC=CC=C1)NC(=O)[C@H](CC(=O)O)N', $el)"
>Aspartame</a
>
</div>
<a

View File

@ -85,6 +85,55 @@
</div>
{% endif %}
{% if compound_structure.half_lifes %}
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">Half-lives</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Scenario</th>
<th>Values</th>
</tr>
</thead>
<tbody>
{% for scenario, half_lifes in compound_structure.half_lifes.items %}
<tr>
<td>
<a href="{{ scenario.url }}" class="hover:bg-base-200"
>{{ scenario.name }}
<i>({{ scenario.package.name }})</i></a
>
</td>
<td>
<table class="table-zebra table">
<tbody>
<tr>
<td>Scenario Type</td>
<td>{{ scenario.scenario_type }}</td>
</tr>
<tr>
<td>Half-life (days)</td>
<td>{{ half_lifes.0.dt50 }}</td>
</tr>
<tr>
<td>Model</td>
<td>{{ half_lifes.0.model }}</td>
</tr>
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if compound_structure.scenarios.all %}
<!-- Scenarios -->
<div class="collapse-arrow bg-base-200 collapse">

View File

@ -14,7 +14,7 @@
<div class="card bg-base-100">
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-2xl">{{ edge.edge_label.name }}</h2>
<h2 class="card-title text-2xl">{{ edge.get_name }}</h2>
<div id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
@ -45,6 +45,11 @@
</ul>
</div>
</div>
<p class="mt-2">
The underlying reaction can be found
<a href="{{ edge.edge_label.url }}" class="link link-primary">here</a
>.
</p>
</div>
</div>

View File

@ -103,7 +103,7 @@
</ul>
</div>
</div>
{% elif job.job_name == 'batch_predict' %}
{% elif job.job_name == 'batch_predict' or job.job_name == 'identify_missing_rules' %}
<div
id="table-container"
class="overflow-x-auto overflow-y-auto max-h-96 border rounded-lg"

View File

@ -14,7 +14,7 @@
<div class="card bg-base-100">
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-2xl">{{ node.name }}</h2>
<h2 class="card-title text-2xl">{{ node.get_name }}</h2>
<div id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
@ -54,28 +54,6 @@
</div>
</div>
<!-- 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>
{% if node.aliases %}
<!-- Aliases -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Aliases</div>
<div class="collapse-content">
<ul class="menu bg-base-100 rounded-box">
{% for alias in node.aliases %}
<li><a class="hover:bg-base-200">{{ alias }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
@ -96,6 +74,70 @@
<div class="collapse-content">{{ node.default_node_label.smiles }}</div>
</div>
{% if node.default_node_label.aliases %}
<!-- Aliases -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Aliases</div>
<div class="collapse-content">
<ul class="menu bg-base-100 rounded-box">
{% for alias in node.default_node_label.aliases %}
<li><a class="hover:bg-base-200">{{ alias }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if node.default_node_label.half_lifes %}
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">Half-lives</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Scenario</th>
<th>Values</th>
</tr>
</thead>
<tbody>
{% for scenario, half_lifes in node.default_node_label.half_lifes.items %}
<tr>
<td>
<a href="{{ scenario.url }}" class="hover:bg-base-200"
>{{ scenario.name }}
<i>({{ scenario.package.name }})</i></a
>
</td>
<td>
<table class="table-zebra table">
<tbody>
<tr>
<td>Scenario Type</td>
<td>{{ scenario.scenario_type }}</td>
</tr>
<tr>
<td>Half-life (days)</td>
<td>{{ half_lifes.0.dt50 }}</td>
</tr>
<tr>
<td>Model</td>
<td>{{ half_lifes.0.model }}</td>
</tr>
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if node.scenarios.all %}
<!-- Scenarios -->
<div class="collapse-arrow bg-base-200 collapse">