forked from enviPath/enviPy
[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:
144
epdb/models.py
144
epdb/models.py
@ -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
|
||||
|
||||
Reference in New Issue
Block a user