diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index 93af425f..0936ee10 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -1620,7 +1620,7 @@ class PathwayNode(Schema): image: str = Field(None, alias="image") imageSize: int = Field(None, alias="image_size") name: str = Field(None, alias="name") - proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate") + proposed: List[Dict[str, str]] = [] smiles: str = Field(None, alias="smiles") pseudo: bool = Field(False, alias="pseudo") @@ -1910,7 +1910,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]): else: node_depth = -1 - n = Node.create( + node = Node.create( pw, n.nodeAsSmiles, node_depth, @@ -1919,7 +1919,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]): description=n.nodeReason, ) - return redirect(n.url) + return redirect(node.url) except ValueError: return 403, {"message": "Adding node failed!"} diff --git a/epdb/logic.py b/epdb/logic.py index d6f974ad..f11e6858 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -1008,8 +1008,6 @@ class PackageManager(object): data: Dict[str, Any], owner: User, preserve_uuids=False, - add_import_timestamp=True, - trust_reviewed=False, ) -> Package: importer = PackageImporter(data, preserve_uuids) imported_package = importer.do_import() @@ -1830,6 +1828,7 @@ class SPathway(object): if edge.rule: e["rule"] = edge.rule.simple_json() + if edge.probability: e["probability"] = edge.probability diff --git a/epdb/models.py b/epdb/models.py index e6cc7d68..04387347 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -894,34 +894,46 @@ class Compound( if parsed is None: raise InvalidSMILESException("Given SMILES is invalid") + if name is not None: + # Clean for potential XSS + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True) + qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package) + # Check if we find a direct match for a given SMILES - if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists(): - return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound + if qs.exists(): + found_structure = qs.first() + found_compound = found_structure.compound + + if name: + found_structure.add_alias(name) + found_compound.add_alias(name) + + return found_compound + + qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package) # Check if we can find the standardized one - if CompoundStructure.objects.filter( - smiles=standardized_smiles, compound__package=package - ).exists(): - found_c = CompoundStructure.objects.get( - smiles=standardized_smiles, compound__package=package - ).compound + if qs.exists(): + found_structure = qs.first() + found_compound = found_structure.compound - # As a direct match wasn't found add a new structure - # TODO return newly created structure - _ = found_c.add_structure(smiles, molfile=molfile, name=name, description=description) + # We've only found the standardized one, create the very structure + _ = found_compound.add_structure( + smiles, molfile=molfile, name=name, description=description + ) - return found_c + if name: + found_compound.add_alias(name) + + return found_compound # Generate Compound c = Compound() c.package = package - if name is not None: - # Clean for potential XSS - name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() - if name is None or name == "": name = f"Compound {Compound.objects.filter(package=package).count() + 1}" @@ -1218,8 +1230,15 @@ class CompoundStructure( # Overwrite SMILES from molfile smiles = FormatConverter.to_smiles(mol) + # Clean for potential XSS + if name is not None: + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + if CompoundStructure.objects.filter(compound=compound, smiles=smiles).exists(): - found_cs = CompoundStructure.objects.get(smiles=smiles, compound=compound) + found_cs = CompoundStructure.objects.get(compound=compound, smiles=smiles) + + if name: + found_cs.add_alias(name) if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""): logger.info( @@ -1236,16 +1255,17 @@ class CompoundStructure( cs = CompoundStructure() # Clean for potential XSS if name is not None: - cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + cs.name = name - if description is not None: + # We have a default here only set the value if it carries some payload + if description is not None and description.strip() != "": cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() cs.compound = compound cs.smiles = smiles - # Check if molfile is present and valid - if molfile is not None and molfile.strip() != "": + # If molfile is not None, it hase to be a valid Molfile as we've survived the parsing check + if molfile is not None: cs.molfile = molfile if "normalized_structure" in kwargs: @@ -1464,6 +1484,9 @@ class SimpleAmbitRule(SimpleRule): if not FormatConverter.is_valid_smirks(smirks): raise ValueError(f'SMIRKS "{smirks}" is invalid!') + if name is not None: + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks) if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "": @@ -1475,14 +1498,17 @@ class SimpleAmbitRule(SimpleRule): if query.exists(): if query.count() > 1: logger.error(f"More than one rule matched this one! {query}") - return query.first() + + found_rule = query.first() + + if name: + found_rule.add_alias(name) + + return found_rule r = SimpleAmbitRule() r.package = package - if name is not None: - name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() - if name is None or name == "": name = f"Rule {Rule.objects.filter(package=package).count() + 1}" @@ -1615,6 +1641,9 @@ class ParallelRule(Rule): f"Simple rule {sr.uuid} does not belong to package {package.uuid}!" ) + if name is not None: + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + # Deduplication check query = ParallelRule.objects.annotate( srs_count=Count("simple_rules", filter=Q(simple_rules__in=simple_rules), distinct=True) @@ -1626,15 +1655,19 @@ class ParallelRule(Rule): if existing_rule_qs.exists(): if existing_rule_qs.count() > 1: - logger.error(f"Found more than one reaction for given input! {existing_rule_qs}") - return existing_rule_qs.first() + logger.error( + f"Found more than one ParallelRule for given input! {existing_rule_qs}" + ) + + found_rule = existing_rule_qs.first() + if name: + found_rule.add_alias(name) + + return found_rule r = ParallelRule() r.package = package - if name is not None: - name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() - if name is None or name == "": name = f"Rule {Rule.objects.filter(package=package).count() + 1}" @@ -1730,6 +1763,10 @@ class Reaction( rules: Union[Rule | List[Rule]] = None, multi_step: bool = False, ): + # Clean for potential XSS + if name is not None and name.strip() != "": + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + _educts = [] _products = [] @@ -1784,14 +1821,21 @@ class Reaction( logger.error( f"Found more than one reaction for given input! {existing_reaction_qs}" ) - return existing_reaction_qs.first() + + found_reaction = existing_reaction_qs.first() + + if name: + found_reaction.add_alias(name) + + return found_reaction r = Reaction() r.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 name is None or name == "": + name = f"Reaction {Reaction.objects.filter(package=package).count() + 1}" + + r.name = name if description is not None and description.strip() != "": r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() @@ -2420,6 +2464,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin) }, "predicted_properties": predicted_properties, "is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False), + "proposed": self.get_proposed_info(), "timeseries": self.get_timeseries_data(), **structure_data, } @@ -2516,7 +2561,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin) return res - def is_proposed_intermediate(self): + def get_proposed_info(self): collected = defaultdict(dict) for ai in self.additional_information.filter( type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"], diff --git a/templates/objects/compound.html b/templates/objects/compound.html index c9f6c060..21ee205a 100644 --- a/templates/objects/compound.html +++ b/templates/objects/compound.html @@ -134,7 +134,7 @@ {% if compound.related_reactions %}
- +
Reactions