[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 shell: bash
run: | run: |
uv run python scripts/pnpm_wrapper.py install 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 uv run python scripts/pnpm_wrapper.py run build
- name: Wait for Postgres - name: Wait for Postgres

View File

@ -212,7 +212,7 @@ def get_user(request, user_uuid):
class GroupMember(Schema): class GroupMember(Schema):
id: str = Field(None, alias="url") id: str
identifier: str identifier: str
name: str name: str
@ -228,7 +228,7 @@ class GroupSchema(Schema):
members: List[GroupMember] = Field([], alias="members") members: List[GroupMember] = Field([], alias="members")
name: str = Field(None, alias="name") name: str = Field(None, alias="name")
ownerid: str = Field(None, alias="owner.url") 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") packages: List["SimplePackage"] = Field([], alias="packages")
readers: List[GroupMember] = Field([], alias="readers") readers: List[GroupMember] = Field([], alias="readers")
writers: List[GroupMember] = Field([], alias="writers") writers: List[GroupMember] = Field([], alias="writers")
@ -237,10 +237,10 @@ class GroupSchema(Schema):
def resolve_members(obj: Group): def resolve_members(obj: Group):
res = [] res = []
for member in obj.user_member.all(): 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(): 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 return res
@ -374,7 +374,7 @@ class PackageSchema(Schema):
).values_list("user", flat=True) ).values_list("user", flat=True)
).distinct() ).distinct()
return [{u.id: u.name} for u in users] return [{u.id: u.get_name()} for u in users]
@staticmethod @staticmethod
def resolve_writers(obj: Package): def resolve_writers(obj: Package):
@ -384,7 +384,7 @@ class PackageSchema(Schema):
).values_list("user", flat=True) ).values_list("user", flat=True)
).distinct() ).distinct()
return [{u.id: u.name} for u in users] return [{u.id: u.get_name()} for u in users]
@staticmethod @staticmethod
def resolve_review_comment(obj): def resolve_review_comment(obj):
@ -966,7 +966,12 @@ def create_package_simple_rule(
raise ValueError("Not yet implemented!") raise ValueError("Not yet implemented!")
else: else:
sr = SimpleAmbitRule.create( 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) return redirect(sr.url)
@ -1119,7 +1124,7 @@ class ReactionSchema(Schema):
name: str = Field(None, alias="name") name: str = Field(None, alias="name")
pathways: List["SimplePathway"] = Field([], alias="related_pathways") pathways: List["SimplePathway"] = Field([], alias="related_pathways")
products: List["ReactionCompoundStructure"] = Field([], alias="products") 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") reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios") scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
smirks: str = Field("", alias="smirks") smirks: str = Field("", alias="smirks")
@ -1135,8 +1140,12 @@ class ReactionSchema(Schema):
@staticmethod @staticmethod
def resolve_references(obj: Reaction): def resolve_references(obj: Reaction):
# TODO rhea_refs = []
return [] for rhea in obj.get_rhea_identifiers():
rhea_refs.append(f"{rhea.identifier_value}")
# TODO UniProt
return {"rheaReferences": rhea_refs, "uniprotCount": []}
@staticmethod @staticmethod
def resolve_medline_references(obj: Reaction): def resolve_medline_references(obj: Reaction):
@ -1715,7 +1724,7 @@ class EdgeSchema(Schema):
id: str = Field(None, alias="url") id: str = Field(None, alias="url")
identifier: str = "edge" identifier: str = "edge"
name: str = Field(None, alias="name") 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") reactionURI: str = Field(None, alias="edge_label.url")
reviewStatus: str = Field(None, alias="review_status") reviewStatus: str = Field(None, alias="review_status")
scenarios: List["SimpleScenario"] = Field([], alias="scenarios") scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
@ -1764,7 +1773,7 @@ class CreateEdge(Schema):
@router.post( @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}, response={200: str | Any, 403: Error},
) )
def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]): 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: if e.edgeAsSmirks:
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."): 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("\\."): 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: else:
for ed in e.educts.split(","): for ed in e.educts.split(","):
educts.append(Node.objects.get(pathway=pw, url=ed.strip())) 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, start_nodes=educts,
end_nodes=products, end_nodes=products,
rule=None, rule=None,
name=e.name, name=None,
description=e.edgeReason, description=e.edgeReason,
) )
@ -1936,7 +1961,7 @@ def get_model(request, package_uuid, model_uuid, c: Query[Classify]):
if pr.rule: if pr.rule:
res["id"] = pr.rule.url res["id"] = pr.rule.url
res["identifier"] = pr.rule.get_rule_identifier() res["identifier"] = pr.rule.get_rule_identifier()
res["name"] = pr.rule.name res["name"] = pr.rule.get_name()
res["reviewStatus"] = ( res["reviewStatus"] = (
"reviewed" if pr.rule.package.reviewed else "unreviewed" "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 ( from epdb.models import (
UserSettingPermission, UserSettingPermission,
MLRelativeReasoning, MLRelativeReasoning,
EnviFormer,
Permission, Permission,
User, User,
ExternalDatabase, ExternalDatabase,
@ -231,7 +230,6 @@ class Command(BaseCommand):
package=pack, package=pack,
rule_packages=[mapping["EAWAG-BBD"]], rule_packages=[mapping["EAWAG-BBD"]],
data_packages=[mapping["EAWAG-BBD"]], data_packages=[mapping["EAWAG-BBD"]],
eval_packages=[],
threshold=0.5, threshold=0.5,
name="ECC - BBD - T0.5", name="ECC - BBD - T0.5",
description="ML Relative Reasoning", description="ML Relative Reasoning",
@ -239,7 +237,3 @@ class Command(BaseCommand):
ml_model.build_dataset() ml_model.build_dataset()
ml_model.build_model() 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, "description": model.description,
"kv": model.kv, "kv": model.kv,
"data_packages_uuids": [str(p.uuid) for p in model.data_packages.all()], "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, "threshold": model.threshold,
"eval_results": model.eval_results, "eval_results": model.eval_results,
"multigen_eval": model.multigen_eval, "multigen_eval": model.multigen_eval,

View File

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

View File

@ -7,6 +7,7 @@ from uuid import uuid4
from celery import shared_task from celery import shared_task
from celery.utils.functional import LRUCache from celery.utils.functional import LRUCache
from django.conf import settings as s from django.conf import settings as s
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone from django.utils import timezone
from epdb.logic import SPathway from epdb.logic import SPathway
@ -73,7 +74,31 @@ def predict_simple(model_pk: int, smiles: str):
@shared_task(queue="background") @shared_task(queue="background")
def send_registration_mail(user_pk: int): 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") @shared_task(bind=True, queue="model")

View File

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

View File

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

View File

@ -85,6 +85,55 @@
</div> </div>
{% endif %} {% 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 %} {% if compound_structure.scenarios.all %}
<!-- Scenarios --> <!-- Scenarios -->
<div class="collapse-arrow bg-base-200 collapse"> <div class="collapse-arrow bg-base-200 collapse">

View File

@ -14,7 +14,7 @@
<div class="card bg-base-100"> <div class="card bg-base-100">
<div class="card-body"> <div class="card-body">
<div class="flex items-center justify-between"> <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 id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm"> <div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg <svg
@ -45,6 +45,11 @@
</ul> </ul>
</div> </div>
</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>
</div> </div>

View File

@ -103,7 +103,7 @@
</ul> </ul>
</div> </div>
</div> </div>
{% elif job.job_name == 'batch_predict' %} {% elif job.job_name == 'batch_predict' or job.job_name == 'identify_missing_rules' %}
<div <div
id="table-container" id="table-container"
class="overflow-x-auto overflow-y-auto max-h-96 border rounded-lg" 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 bg-base-100">
<div class="card-body"> <div class="card-body">
<div class="flex items-center justify-between"> <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 id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm"> <div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg <svg
@ -54,28 +54,6 @@
</div> </div>
</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 --> <!-- Image Representation -->
<div class="collapse-arrow bg-base-200 collapse"> <div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked /> <input type="checkbox" checked />
@ -96,6 +74,70 @@
<div class="collapse-content">{{ node.default_node_label.smiles }}</div> <div class="collapse-content">{{ node.default_node_label.smiles }}</div>
</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 %} {% if node.scenarios.all %}
<!-- Scenarios --> <!-- Scenarios -->
<div class="collapse-arrow bg-base-200 collapse"> <div class="collapse-arrow bg-base-200 collapse">