forked from enviPath/enviPy
[Feature] Sync Features from Client Repo (#425)
- New PackageImporter - Fix persisting Molfile on Structure creation - Add NonPersistent Prediction - Pathway View Options - Edit Node Fix Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#425
This commit is contained in:
@ -87,6 +87,11 @@ def get_access_token_from_request(request, scopes=None):
|
|||||||
"""
|
"""
|
||||||
Get an access token from the request using MSAL token cache.
|
Get an access token from the request using MSAL token cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Check if auth via Access Token
|
||||||
|
if request.headers.get("Authorization"):
|
||||||
|
return {"access_token": request.headers.get("Authorization").split(" ")[1]}
|
||||||
|
|
||||||
if scopes is None:
|
if scopes is None:
|
||||||
scopes = s.MS_ENTRA_SCOPES
|
scopes = s.MS_ENTRA_SCOPES
|
||||||
|
|
||||||
|
|||||||
@ -4,3 +4,7 @@ class InvalidSMILESException(Exception):
|
|||||||
|
|
||||||
class InvalidMolfileException(Exception):
|
class InvalidMolfileException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PackageImportException(Exception):
|
||||||
|
pass
|
||||||
|
|||||||
@ -1635,24 +1635,10 @@ class PathwayNode(Schema):
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_engineered_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_image(obj: Node):
|
|
||||||
return f"{obj.default_node_label.url}?image=svg"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_image_size(obj: Node):
|
def resolve_image_size(obj: Node):
|
||||||
return 400
|
return 400
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_proposed_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class PathwaySchema(Schema):
|
class PathwaySchema(Schema):
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
@ -2282,3 +2268,26 @@ def get_setting(request, setting_uuid):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Util #
|
||||||
|
########
|
||||||
|
class NonPersistent(Schema):
|
||||||
|
smiles: str
|
||||||
|
setting_url: str = Field(..., alias="settingUri")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/util", response={200: Any, 403: Error})
|
||||||
|
def predict(request, np: Form[NonPersistent]):
|
||||||
|
try:
|
||||||
|
from epdb.logic import SPathway
|
||||||
|
|
||||||
|
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||||
|
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||||
|
spw.predict()
|
||||||
|
return spw.to_json()
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|||||||
@ -1011,7 +1011,7 @@ class PackageManager(object):
|
|||||||
add_import_timestamp=True,
|
add_import_timestamp=True,
|
||||||
trust_reviewed=False,
|
trust_reviewed=False,
|
||||||
) -> Package:
|
) -> Package:
|
||||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
importer = PackageImporter(data, preserve_uuids)
|
||||||
imported_package = importer.do_import()
|
imported_package = importer.do_import()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@ -1828,6 +1828,11 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if edge.rule:
|
||||||
|
e["rule"] = edge.rule.simple_json()
|
||||||
|
if edge.probability:
|
||||||
|
e["probability"] = edge.probability
|
||||||
|
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1241,8 +1241,12 @@ class CompoundStructure(
|
|||||||
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()
|
||||||
|
|
||||||
cs.smiles = smiles
|
|
||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
cs.smiles = smiles
|
||||||
|
|
||||||
|
# Check if molfile is present and valid
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
cs.molfile = molfile
|
||||||
|
|
||||||
if "normalized_structure" in kwargs:
|
if "normalized_structure" in kwargs:
|
||||||
cs.normalized_structure = kwargs["normalized_structure"]
|
cs.normalized_structure = kwargs["normalized_structure"]
|
||||||
@ -2023,6 +2027,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
# add links start -> pseudo
|
# add links start -> pseudo
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -2032,6 +2037,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"source": node_url_to_idx[link["start_node_urls"][0]],
|
"source": node_url_to_idx[link["start_node_urls"][0]],
|
||||||
"target": pseudo_idx,
|
"target": pseudo_idx,
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
|
"to_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2039,6 +2045,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
for target in link["end_node_urls"]:
|
for target in link["end_node_urls"]:
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -2049,6 +2056,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"target": node_url_to_idx[target],
|
"target": node_url_to_idx[target],
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
"multi_step": link["multi_step"],
|
"multi_step": link["multi_step"],
|
||||||
|
"from_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2359,17 +2367,19 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
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):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name is None or self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.default_node_label.name} (taken from underlying structure)"
|
if include_suffix:
|
||||||
)
|
return f"{self.default_node_label.name} (taken from underlying structure)"
|
||||||
|
else:
|
||||||
|
return self.default_node_label.name
|
||||||
|
|
||||||
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()
|
||||||
@ -2399,6 +2409,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
),
|
),
|
||||||
"image_type": "svg",
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"smiles": self.default_node_label.smiles,
|
"smiles": self.default_node_label.smiles,
|
||||||
"scenarios": [{"name": s.get_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": {
|
||||||
@ -2505,6 +2516,28 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def is_proposed_intermediate(self):
|
||||||
|
collected = defaultdict(dict)
|
||||||
|
for ai in self.additional_information.filter(
|
||||||
|
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"],
|
||||||
|
scenario__isnull=False,
|
||||||
|
):
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||||
|
|
||||||
|
if ai.type == "ProposedIntermediate":
|
||||||
|
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||||
|
|
||||||
|
if ai.type == "Confidence":
|
||||||
|
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||||
|
|
||||||
|
if ai.type == "TransformationProductImportance":
|
||||||
|
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
||||||
|
ai.get().importance.value
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(collected.values())
|
||||||
|
|
||||||
|
|
||||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||||
pathway = models.ForeignKey(
|
pathway = models.ForeignKey(
|
||||||
@ -2526,6 +2559,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
edge_json = {
|
edge_json = {
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"id": self.url,
|
"id": self.url,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"image": self.url + "?image=svg",
|
"image": self.url + "?image=svg",
|
||||||
@ -2640,17 +2674,19 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.edge_label.name} (taken from underlying reaction)"
|
if include_suffix:
|
||||||
)
|
return f"{self.edge_label.name} (taken from underlying reaction)"
|
||||||
|
else:
|
||||||
|
return self.edge_label.name
|
||||||
|
|
||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||||
|
|||||||
@ -775,13 +775,11 @@ def models(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.ENVIFORMER_PRESENT:
|
if s.ENVIFORMER_PRESENT:
|
||||||
context["model_types"]["EnviFormer"] = (
|
context["model_types"]["EnviFormer"] = {
|
||||||
{
|
"type": "enviformer",
|
||||||
"type": "enviformer",
|
"requires_rule_packages": False,
|
||||||
"requires_rule_packages": False,
|
"requires_data_packages": True,
|
||||||
"requires_data_packages": True,
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -2102,12 +2100,14 @@ def package_pathways(request, package_uuid):
|
|||||||
else:
|
else:
|
||||||
prediction_setting = current_user.prediction_settings()
|
prediction_setting = current_user.prediction_settings()
|
||||||
|
|
||||||
|
is_predict_mode = pw_mode in {"predict", "incremental"}
|
||||||
|
|
||||||
pw = Pathway.create(
|
pw = Pathway.create(
|
||||||
current_package,
|
current_package,
|
||||||
stand_smiles,
|
stand_smiles if is_predict_mode else smiles,
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
predicted=pw_mode in {"predict", "incremental"},
|
predicted=is_predict_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set mode
|
# set mode
|
||||||
@ -2466,7 +2466,26 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
return JsonResponse({"success": current_node.url})
|
return JsonResponse({"success": current_node.url})
|
||||||
|
|
||||||
return HttpResponseBadRequest()
|
new_node_name = request.POST.get("node-name")
|
||||||
|
new_node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
|
if any([new_node_name, new_node_description]):
|
||||||
|
if new_node_name is not None and new_node_name.strip() != "":
|
||||||
|
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
current_node.name = new_node_name
|
||||||
|
|
||||||
|
if new_node_description is not None and new_node_description.strip() != "":
|
||||||
|
new_node_description = nh3.clean(
|
||||||
|
new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
|
||||||
|
).strip()
|
||||||
|
current_node.description = new_node_description
|
||||||
|
|
||||||
|
current_node.save()
|
||||||
|
|
||||||
|
return redirect(current_node.url)
|
||||||
|
|
||||||
|
return error(request, "Node update failed!", "No changes were made to the node")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -39,3 +39,7 @@ select.select[multiple] {
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,126 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
|
||||||
|
const basePagination = (
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId
|
||||||
|
) => ({
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId,
|
||||||
|
|
||||||
|
get paginatedItems() {
|
||||||
|
return this.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingStart() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return (this.currentPage - 1) * this.perPage + 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingEnd() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||||
|
},
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.fetchPage(this.currentPage + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.fetchPage(this.currentPage - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
if (page >= 1 && page <= this.totalPages) {
|
||||||
|
this.fetchPage(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get pageNumbers() {
|
||||||
|
const pages = [];
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.currentPage;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||||
|
|
||||||
|
let rangeStart;
|
||||||
|
let rangeEnd;
|
||||||
|
|
||||||
|
if (current <= 4) {
|
||||||
|
rangeStart = 2;
|
||||||
|
rangeEnd = 5;
|
||||||
|
} else if (current >= total - 3) {
|
||||||
|
rangeStart = total - 4;
|
||||||
|
rangeEnd = total - 1;
|
||||||
|
} else {
|
||||||
|
rangeStart = current - 1;
|
||||||
|
rangeEnd = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart > 2) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd < total - 1) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Alpine.data('paginatedList',
|
||||||
|
(items, options = {}) => ({
|
||||||
|
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchPage(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPage(page) {
|
||||||
|
this.totalItems = this.items.length;
|
||||||
|
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||||
|
this.currentPage = page
|
||||||
|
|
||||||
|
const start = page * this.perPage - this.perPage;
|
||||||
|
const end = page * this.perPage;
|
||||||
|
return this.items.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||||
items: [],
|
items: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|||||||
123
static/js/pw.js
123
static/js/pw.js
@ -67,6 +67,9 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
const depthMap = new Map();
|
||||||
|
|
||||||
|
// Avoid leaving unconnected Nodes leaving the viewport
|
||||||
|
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||||
|
|
||||||
// Sort nodes by depth first to minimize crossings
|
// Sort nodes by depth first to minimize crossings
|
||||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||||
|
|
||||||
@ -154,6 +157,11 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
|
|
||||||
|
linkText
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
@ -256,7 +264,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait before showing popup (ms)
|
||||||
var popupWaitBeforeShow = 1000;
|
var popupWaitBeforeShow = 750;
|
||||||
|
|
||||||
// Custom popover element
|
// Custom popover element
|
||||||
let popoverTimeout = null;
|
let popoverTimeout = null;
|
||||||
@ -488,7 +496,7 @@ function draw(pathway, elem) {
|
|||||||
if (n.stereo_removed) {
|
if (n.stereo_removed) {
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||||
}
|
}
|
||||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
@ -528,6 +536,21 @@ function draw(pathway, elem) {
|
|||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||||
|
for (var prop in n.proposed) {
|
||||||
|
if (n.proposed[prop].scenarioId === s.url) {
|
||||||
|
if("proposed" in n.proposed[prop]){
|
||||||
|
popupContent += "This compound is a proposed intermediate" + "<br>";
|
||||||
|
}
|
||||||
|
if("Confidence" in n.proposed[prop]){
|
||||||
|
popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
|
||||||
|
}
|
||||||
|
if("Transformation product importance" in n.proposed[prop]){
|
||||||
|
popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContent += "<br>";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,7 +563,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edge_popup(e) {
|
function edge_popup(e) {
|
||||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";
|
||||||
|
|
||||||
if (e.reaction.rules) {
|
if (e.reaction.rules) {
|
||||||
for (var rule of e.reaction.rules) {
|
for (var rule of e.reaction.rules) {
|
||||||
@ -591,7 +614,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
// Add background rectangle FIRST to enable pan/zoom on empty space
|
||||||
// This must be inserted before zoomable group so it's behind everything
|
// This must be inserted before zoomable group so it's behind everything
|
||||||
svg.insert("rect", "#zoomable")
|
const rect = svg.insert("rect", "#zoomable")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
@ -600,6 +623,30 @@ function draw(pathway, elem) {
|
|||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
.style("cursor", "grab");
|
.style("cursor", "grab");
|
||||||
|
|
||||||
|
// Click on empty SVG space (not on a node or link)
|
||||||
|
rect.on("click", function(event) {
|
||||||
|
// Only treat it as a background click if the direct target is the rect itself
|
||||||
|
// (clicks on nodes/links bubble up but originate from different elements)
|
||||||
|
const target = event.target;
|
||||||
|
const isNode = target.closest && (
|
||||||
|
target.closest("circle") !== null ||
|
||||||
|
target.closest("image") !== null ||
|
||||||
|
target.closest(".node") !== null
|
||||||
|
);
|
||||||
|
const isLink = target.closest && (
|
||||||
|
target.closest("path.link") !== null ||
|
||||||
|
target.closest("path.link_no_arrow") !== null
|
||||||
|
);
|
||||||
|
if (!isNode && !isLink) {
|
||||||
|
// console.log("Clicked outside a node or link");
|
||||||
|
// Clear all highlights
|
||||||
|
d3.selectAll("circle").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom Funktion aktivieren
|
// Zoom Funktion aktivieren
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([0.5, 5])
|
.scaleExtent([0.5, 5])
|
||||||
@ -646,22 +693,32 @@ function draw(pathway, elem) {
|
|||||||
.force("center", d3.forceCenter(width / 2, height / 4))
|
.force("center", d3.forceCenter(width / 2, height / 4))
|
||||||
.on("tick", ticked);
|
.on("tick", ticked);
|
||||||
|
|
||||||
// Kanten zeichnen
|
// Draw Edges
|
||||||
const link = zoomable.append("g")
|
const linkGroup = zoomable.append("g")
|
||||||
.selectAll("path")
|
.selectAll("g")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter().append("path")
|
.enter()
|
||||||
// Check if target is pseudo and draw marker only if not pseudo
|
.append("g")
|
||||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
.attr("class", "link-group");
|
||||||
.attr("marker-end", d => {
|
|
||||||
if (d.target.pseudo) return '';
|
const link = linkGroup
|
||||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
.append("path")
|
||||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
})
|
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
.on("click", function(event, d) {
|
|
||||||
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
|
.attr("marker-end", d => {
|
||||||
|
if (d.target.pseudo) return '';
|
||||||
|
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||||
|
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.on("click", function(event, d) {
|
||||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||||
d3.selectAll("path").classed("highlighted", false);
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
if (!wasHighlighted) {
|
if (!wasHighlighted) {
|
||||||
const toHighlight = [];
|
const toHighlight = [];
|
||||||
toHighlight.push(d.el);
|
toHighlight.push(d.el);
|
||||||
@ -688,6 +745,32 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function reaction_name(d) {
|
||||||
|
const to_pseudo = d?.to_pseudo || false;
|
||||||
|
|
||||||
|
if (to_pseudo) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "";
|
||||||
|
if ("reaction_probability" in d && d["reaction_probability"] !== null) {
|
||||||
|
suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
|
||||||
|
}
|
||||||
|
return d.plain_name + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkText = linkGroup
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "link-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("dy", -6)
|
||||||
|
.style("font-size", "4px")
|
||||||
|
.style("pointer-events", "none")
|
||||||
|
.style("display", "none")
|
||||||
|
.text(d => reaction_name(d))
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
d.el = this; // attach the DOM element to the data object
|
d.el = this; // attach the DOM element to the data object
|
||||||
@ -695,7 +778,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
pop_add(link, "Reaction", edge_popup);
|
pop_add(link, "Reaction", edge_popup);
|
||||||
|
|
||||||
// Knoten zeichnen
|
// Draw Nodes
|
||||||
const node = zoomable.append("g")
|
const node = zoomable.append("g")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
|
|||||||
@ -55,6 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if node.description and node.description != "no description" %}
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
|
<div class="collapse-content">{{ node.description }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|
||||||
{% for tpl in viz_templates %}
|
{% for tpl in viz_templates %}
|
||||||
|
|||||||
@ -21,12 +21,13 @@
|
|||||||
.link {
|
.link {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
/* marker-end: url(#arrow); */
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link_no_arrow {
|
.link_no_arrow {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node image {
|
.node image {
|
||||||
@ -177,6 +178,52 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||||
>
|
>
|
||||||
|
<li>
|
||||||
|
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="compound-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Compound Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="reaction-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Reaction Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if pathway.setting.model.app_domain %}
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
@ -495,6 +542,10 @@
|
|||||||
{{ pathway.d3_json|json_script:"pathway" }}
|
{{ pathway.d3_json|json_script:"pathway" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Global switch for compound names view
|
||||||
|
var compoundNamesViewEnabled = false;
|
||||||
|
// Gloabl switch for reaction names view
|
||||||
|
var reactionNamesViewEnabled = false;
|
||||||
// Global switch for app domain view
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
// Global switch for timeseries view
|
// Global switch for timeseries view
|
||||||
@ -530,6 +581,58 @@
|
|||||||
descContent.innerHTML = newDesc;
|
descContent.innerHTML = newDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compoundNamesBtn = document.getElementById(
|
||||||
|
"compound-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (compoundNamesBtn) {
|
||||||
|
compoundNamesBtn.addEventListener("click", function () {
|
||||||
|
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("compound-names-icon");
|
||||||
|
if (compoundNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
if (x.name) {
|
||||||
|
d3.select(x.el)
|
||||||
|
.append("text")
|
||||||
|
.text((d) => d.plain_name)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("y", -20)
|
||||||
|
.style("font-size", "4px");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
d3.select(x.el).select("text").remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const reactionNamesBtn = document.getElementById(
|
||||||
|
"reaction-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (reactionNamesBtn) {
|
||||||
|
reactionNamesBtn.addEventListener("click", function () {
|
||||||
|
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("reaction-names-icon");
|
||||||
|
if (reactionNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", null);
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", "none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// App domain toggle
|
// App domain toggle
|
||||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||||
if (appDomainBtn) {
|
if (appDomainBtn) {
|
||||||
|
|||||||
@ -110,8 +110,6 @@
|
|||||||
<div
|
<div
|
||||||
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
class="text-base-content/50 flex items-center justify-center space-x-6 text-sm"
|
||||||
>
|
>
|
||||||
<a href="/legal" class="link link-hover">Legal</a>
|
|
||||||
<span class="text-base-content/30">•</span>
|
|
||||||
<a href="/terms" class="link link-hover">Terms of Use</a>
|
<a href="/terms" class="link link-hover">Terms of Use</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
<a href="/privacy" class="link link-hover">Privacy Policy</a>
|
||||||
|
|||||||
@ -72,8 +72,10 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
def test_import_package(self):
|
def test_import_package(self):
|
||||||
file = SimpleUploadedFile(
|
file = SimpleUploadedFile(
|
||||||
"Fixture_Package.json",
|
"EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json",
|
||||||
open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(),
|
open(
|
||||||
|
s.FIXTURE_DIRS[0] / "EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json", "rb"
|
||||||
|
).read(),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,10 +88,11 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
p = Package.objects.get(url=package_url)
|
p = Package.objects.get(url=package_url)
|
||||||
|
|
||||||
self.assertEqual(p.pathways.count(), 22)
|
self.assertEqual(p.pathways.count(), 219)
|
||||||
self.assertEqual(p.rules.count(), 45)
|
self.assertEqual(p.rules.count(), 498)
|
||||||
self.assertEqual(p.compounds.count(), 223)
|
self.assertEqual(p.compounds.count(), 1399)
|
||||||
self.assertEqual(p.reactions.count(), 212)
|
self.assertEqual(p.reactions.count(), 1480)
|
||||||
|
self.assertEqual(p.scenarios.count(), 1914)
|
||||||
|
|
||||||
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
||||||
self.assertEqual(upp.permission, Permission.ALL[0])
|
self.assertEqual(upp.permission, Permission.ALL[0])
|
||||||
|
|||||||
@ -9,32 +9,24 @@ from datetime import datetime
|
|||||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.db import transaction
|
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import HttpUrl
|
from pydantic import HttpUrl, ValidationError
|
||||||
|
|
||||||
|
from epdb.exceptions import PackageImportException
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
EnviFormer,
|
|
||||||
EPModel,
|
|
||||||
ExternalDatabase,
|
|
||||||
ExternalIdentifier,
|
|
||||||
License,
|
License,
|
||||||
MLRelativeReasoning,
|
|
||||||
Node,
|
Node,
|
||||||
ParallelRule,
|
ParallelRule,
|
||||||
Pathway,
|
Pathway,
|
||||||
Reaction,
|
Reaction,
|
||||||
Rule,
|
Rule,
|
||||||
RuleBasedRelativeReasoning,
|
|
||||||
Scenario,
|
Scenario,
|
||||||
Setting,
|
Setting,
|
||||||
SimpleAmbitRule,
|
SimpleAmbitRule,
|
||||||
SimpleRDKitRule,
|
|
||||||
SimpleRule,
|
|
||||||
)
|
)
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
@ -88,6 +80,9 @@ class RefPathwayExportSchema(RefExportSchema): ...
|
|||||||
class RefScenarioExportSchema(RefExportSchema): ...
|
class RefScenarioExportSchema(RefExportSchema): ...
|
||||||
|
|
||||||
|
|
||||||
|
class RefEnzymeExportSchema(RefExportSchema): ...
|
||||||
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# Compound #
|
# Compound #
|
||||||
############
|
############
|
||||||
@ -128,7 +123,28 @@ class ReactionExportSchema(RefReactionExportSchema):
|
|||||||
#########
|
#########
|
||||||
# Rules #
|
# Rules #
|
||||||
#########
|
#########
|
||||||
class RuleExportSchema(RefRuleExportSchema):
|
class EnzymeExportSchema(RefEnzymeExportSchema):
|
||||||
|
ec_number: str
|
||||||
|
classification_level: int
|
||||||
|
linking_method: str
|
||||||
|
reaction_evidence: List[RefReactionExportSchema]
|
||||||
|
edge_evidence: List[RefEdgeExportSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
||||||
|
enzymes: List[EnzymeExportSchema] | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_enzymes(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
res = []
|
||||||
|
for e in obj.get("enzymes", []):
|
||||||
|
res.append(EnzymeExportSchema.model_validate(e))
|
||||||
|
return res
|
||||||
|
return obj.enzymelink_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
class RuleExportSchema(EnzymeRuleExportSchema):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
aliases: List[str]
|
aliases: List[str]
|
||||||
@ -138,7 +154,7 @@ class RuleExportSchema(RefRuleExportSchema):
|
|||||||
scenarios: List[RefScenarioExportSchema]
|
scenarios: List[RefScenarioExportSchema]
|
||||||
|
|
||||||
|
|
||||||
class ParallelRuleExportSchema(RefRuleExportSchema):
|
class ParallelRuleExportSchema(EnzymeRuleExportSchema):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
aliases: List[str]
|
aliases: List[str]
|
||||||
@ -210,7 +226,7 @@ class PackageExportSchema(Schema):
|
|||||||
description: str
|
description: str
|
||||||
uuid: str
|
uuid: str
|
||||||
reviewed: bool
|
reviewed: bool
|
||||||
license: LicenseExportSchema | None
|
license: LicenseExportSchema | None = None
|
||||||
compounds: List[CompoundExportSchema]
|
compounds: List[CompoundExportSchema]
|
||||||
reactions: List[ReactionExportSchema]
|
reactions: List[ReactionExportSchema]
|
||||||
simple_rules: List[RuleExportSchema]
|
simple_rules: List[RuleExportSchema]
|
||||||
@ -224,6 +240,15 @@ class PackageExportSchema(Schema):
|
|||||||
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_compounds(obj):
|
||||||
|
res = []
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for c in obj.get("compounds", []):
|
||||||
|
res.append(CompoundExportSchema.model_validate(c))
|
||||||
|
return res
|
||||||
|
return obj.compounds.all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_simple_rules(obj):
|
def resolve_simple_rules(obj):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
@ -277,51 +302,239 @@ class PackageExporter:
|
|||||||
|
|
||||||
|
|
||||||
class PackageImporter:
|
class PackageImporter:
|
||||||
"""
|
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
||||||
Imports package data from JSON export.
|
|
||||||
Handles object creation, relationship mapping, and dependency resolution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
package: Dict[str, Any],
|
|
||||||
preserve_uuids: bool = False,
|
|
||||||
add_import_timestamp=True,
|
|
||||||
trust_reviewed=False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initialize the importer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
preserve_uuids: If True, preserve original UUIDs. If False, generate new ones.
|
|
||||||
"""
|
|
||||||
self.preserve_uuids = preserve_uuids
|
self.preserve_uuids = preserve_uuids
|
||||||
self.add_import_timestamp = add_import_timestamp
|
|
||||||
self.trust_reviewed = trust_reviewed
|
|
||||||
self.uuid_mapping = {}
|
|
||||||
self.object_cache = {}
|
|
||||||
self._raw_package = package
|
self._raw_package = package
|
||||||
|
self._cache = {}
|
||||||
def _get_or_generate_uuid(self, original_uuid: str) -> str:
|
|
||||||
"""Get mapped UUID or generate new one if not preserving UUIDs."""
|
|
||||||
if self.preserve_uuids:
|
|
||||||
return original_uuid
|
|
||||||
|
|
||||||
if original_uuid not in self.uuid_mapping:
|
|
||||||
self.uuid_mapping[original_uuid] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
return self.uuid_mapping[original_uuid]
|
|
||||||
|
|
||||||
def _cache_object(self, model_name: str, uuid_str: str, obj):
|
|
||||||
"""Cache a created object for later reference."""
|
|
||||||
self.object_cache[(model_name, uuid_str)] = obj
|
|
||||||
|
|
||||||
def _get_cached_object(self, model_name: str, uuid_str: str):
|
|
||||||
"""Get a cached object by model name and UUID."""
|
|
||||||
return self.object_cache.get((model_name, uuid_str))
|
|
||||||
|
|
||||||
def do_import(self) -> Package:
|
def do_import(self) -> Package:
|
||||||
return self._import_package_from_json(self._raw_package)
|
return self._import_package_from_json()
|
||||||
|
|
||||||
|
def _import_compounds(
|
||||||
|
self,
|
||||||
|
package: Package,
|
||||||
|
compounds: List[CompoundExportSchema],
|
||||||
|
):
|
||||||
|
for c in compounds:
|
||||||
|
new_c = Compound()
|
||||||
|
new_c.uuid = str(uuid.uuid4()) if not self.preserve_uuids else c.uuid
|
||||||
|
new_c.package = package
|
||||||
|
new_c.name = c.name
|
||||||
|
new_c.description = c.description
|
||||||
|
new_c.aliases = c.aliases
|
||||||
|
new_c.save()
|
||||||
|
|
||||||
|
self._cache[c.uuid] = new_c
|
||||||
|
|
||||||
|
for cs in c.structures:
|
||||||
|
new_cs = CompoundStructure()
|
||||||
|
new_cs.uuid = str(uuid.uuid4()) if not self.preserve_uuids else cs.uuid
|
||||||
|
new_cs.compound = new_c
|
||||||
|
new_cs.name = cs.name
|
||||||
|
new_cs.description = cs.description
|
||||||
|
new_cs.aliases = cs.aliases
|
||||||
|
new_cs.smiles = cs.smiles
|
||||||
|
new_cs.molfile = cs.molfile
|
||||||
|
|
||||||
|
new_cs.normalized_structure = cs.normalized_structure
|
||||||
|
new_cs.save()
|
||||||
|
|
||||||
|
self._cache[cs.uuid] = new_cs
|
||||||
|
|
||||||
|
# Now we can assigne the default_structure to the compound
|
||||||
|
new_c.default_structure = self._cache[c.default_structure.uuid]
|
||||||
|
new_c.save()
|
||||||
|
|
||||||
|
def _import_simple_rules(self, package: Package, rules: List[RuleExportSchema]):
|
||||||
|
for r in rules:
|
||||||
|
new_r = SimpleAmbitRule()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.smirks = r.smirks
|
||||||
|
new_r.reactant_filter_smarts = r.reactant_filter_smarts
|
||||||
|
new_r.product_filter_smarts = r.product_filter_smarts
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_composite_rules(self, package: Package, rules: List[ParallelRuleExportSchema]):
|
||||||
|
for r in rules:
|
||||||
|
new_r = ParallelRule()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
for sr in r.simple_rules:
|
||||||
|
new_r.simple_rules.add(self._cache[sr.uuid])
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_reactions(
|
||||||
|
self,
|
||||||
|
package: Package,
|
||||||
|
reactions: List[ReactionExportSchema],
|
||||||
|
):
|
||||||
|
for r in reactions:
|
||||||
|
new_r = Reaction()
|
||||||
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
||||||
|
new_r.package = package
|
||||||
|
new_r.name = r.name
|
||||||
|
new_r.description = r.description
|
||||||
|
new_r.aliases = r.aliases
|
||||||
|
new_r.multi_step = r.multi_step
|
||||||
|
new_r.medline_references = r.medline_references
|
||||||
|
new_r.save()
|
||||||
|
|
||||||
|
for educt in r.educts:
|
||||||
|
new_r.educts.add(self._cache[educt.uuid])
|
||||||
|
|
||||||
|
for product in r.products:
|
||||||
|
new_r.products.add(self._cache[product.uuid])
|
||||||
|
|
||||||
|
for rule in r.rules:
|
||||||
|
new_r.rules.add(self._cache[rule.uuid])
|
||||||
|
|
||||||
|
self._cache[r.uuid] = new_r
|
||||||
|
|
||||||
|
def _import_pathways(self, package: Package, pathways: List[PathwayExportSchema]):
|
||||||
|
for pw in pathways:
|
||||||
|
new_pw = Pathway()
|
||||||
|
new_pw.uuid = str(uuid.uuid4()) if not self.preserve_uuids else pw.uuid
|
||||||
|
new_pw.package = package
|
||||||
|
new_pw.name = pw.name
|
||||||
|
new_pw.description = pw.description
|
||||||
|
new_pw.aliases = pw.aliases
|
||||||
|
new_pw.predicted = pw.predicted
|
||||||
|
new_pw.save()
|
||||||
|
|
||||||
|
self._cache[pw.uuid] = new_pw
|
||||||
|
|
||||||
|
for n in pw.nodes:
|
||||||
|
new_n = Node()
|
||||||
|
new_n.uuid = str(uuid.uuid4()) if not self.preserve_uuids else n.uuid
|
||||||
|
new_n.pathway = new_pw
|
||||||
|
new_n.name = n.name
|
||||||
|
new_n.description = n.description
|
||||||
|
new_n.aliases = n.aliases
|
||||||
|
new_n.default_node_label = self._cache[n.default_node_label.uuid]
|
||||||
|
new_n.depth = n.depth
|
||||||
|
new_n.stereo_removed = n.stereo_removed
|
||||||
|
new_n.save()
|
||||||
|
|
||||||
|
for nl in n.node_labels:
|
||||||
|
new_n.node_labels.add(self._cache[nl.uuid])
|
||||||
|
|
||||||
|
self._cache[n.uuid] = new_n
|
||||||
|
|
||||||
|
for e in pw.edges:
|
||||||
|
new_e = Edge()
|
||||||
|
new_e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else e.uuid
|
||||||
|
new_e.pathway = new_pw
|
||||||
|
new_e.name = e.name
|
||||||
|
new_e.description = e.description
|
||||||
|
new_e.aliases = e.aliases
|
||||||
|
new_e.edge_label = self._cache[e.edge_label.uuid]
|
||||||
|
new_e.save()
|
||||||
|
|
||||||
|
for sn in e.start_nodes:
|
||||||
|
new_e.start_nodes.add(self._cache[sn.uuid])
|
||||||
|
|
||||||
|
for en in e.end_nodes:
|
||||||
|
new_e.end_nodes.add(self._cache[en.uuid])
|
||||||
|
|
||||||
|
self._cache[e.uuid] = new_e
|
||||||
|
|
||||||
|
def _import_scenarios(self, package: Package, scenarios: List[ScenarioExportSchema]):
|
||||||
|
for scen in scenarios:
|
||||||
|
new_s = Scenario()
|
||||||
|
new_s.uuid = str(uuid.uuid4()) if not self.preserve_uuids else scen.uuid
|
||||||
|
new_s.package = package
|
||||||
|
new_s.name = scen.name
|
||||||
|
new_s.description = scen.description
|
||||||
|
new_s.scenario_date = scen.scenario_date
|
||||||
|
new_s.scenario_type = scen.scenario_type
|
||||||
|
new_s.save()
|
||||||
|
|
||||||
|
self._cache[scen.uuid] = new_s
|
||||||
|
|
||||||
|
def _import_additional_information(
|
||||||
|
self, package: Package, additional_information: List[AdditionalInformationExportSchema]
|
||||||
|
):
|
||||||
|
for ai in additional_information:
|
||||||
|
new_ai = AdditionalInformation()
|
||||||
|
new_ai.uuid = str(uuid.uuid4()) if not self.preserve_uuids else ai.uuid
|
||||||
|
new_ai.package = package
|
||||||
|
new_ai.type = ai.type
|
||||||
|
new_ai.data = ai.data
|
||||||
|
|
||||||
|
if ai.scenario:
|
||||||
|
new_ai.scenario = self._cache[ai.scenario.uuid]
|
||||||
|
|
||||||
|
if ai.attach_object:
|
||||||
|
new_ai.content_object = self._cache[ai.attach_object.uuid]
|
||||||
|
|
||||||
|
new_ai.save()
|
||||||
|
|
||||||
|
self._cache[ai.uuid] = new_ai
|
||||||
|
|
||||||
|
def _link_scenarios_after_import(self, data: PackageExportSchema):
|
||||||
|
collections = [
|
||||||
|
data.compounds,
|
||||||
|
data.simple_rules,
|
||||||
|
data.composite_rules,
|
||||||
|
data.reactions,
|
||||||
|
data.pathways,
|
||||||
|
[n for pw in data.pathways for n in pw.nodes],
|
||||||
|
[e for pw in data.pathways for e in pw.edges],
|
||||||
|
]
|
||||||
|
|
||||||
|
for coll in collections:
|
||||||
|
for elem in coll:
|
||||||
|
elem_obj = self._cache[elem.uuid]
|
||||||
|
for scen in elem.scenarios:
|
||||||
|
elem_obj.scenarios.add(self._cache[scen.uuid])
|
||||||
|
|
||||||
|
def _import_package_from_json(
|
||||||
|
self,
|
||||||
|
) -> Package:
|
||||||
|
try:
|
||||||
|
parsed = PackageExportSchema.model_validate(self._raw_package)
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(f"Error validating package data: {e}")
|
||||||
|
raise PackageImportException("Deserialization failed")
|
||||||
|
|
||||||
|
package_uuid = str(uuid.uuid4())
|
||||||
|
if self.preserve_uuids:
|
||||||
|
package_uuid = parsed.uuid
|
||||||
|
|
||||||
|
package_license = License.objects.get(link=parsed.license.link) if parsed.license else None
|
||||||
|
|
||||||
|
package = Package()
|
||||||
|
package.uuid = package_uuid
|
||||||
|
package.name = f"{parsed.name} - Imported at {datetime.now()}"
|
||||||
|
package.description = parsed.description
|
||||||
|
package.reviewed = False
|
||||||
|
package.license = package_license
|
||||||
|
|
||||||
|
package.save()
|
||||||
|
|
||||||
|
# Import Elements
|
||||||
|
self._import_compounds(package, parsed.compounds)
|
||||||
|
self._import_simple_rules(package, parsed.simple_rules)
|
||||||
|
self._import_composite_rules(package, parsed.composite_rules)
|
||||||
|
self._import_reactions(package, parsed.reactions)
|
||||||
|
self._import_pathways(package, parsed.pathways)
|
||||||
|
self._import_scenarios(package, parsed.scenarios)
|
||||||
|
self._import_additional_information(package, parsed.additional_information)
|
||||||
|
self._link_scenarios_after_import(parsed)
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
|
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
|
||||||
@ -339,510 +552,6 @@ class PackageImporter:
|
|||||||
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
||||||
return hmac.compare_digest(signature, expected)
|
return hmac.compare_digest(signature, expected)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def _import_package_from_json(self, package_data: Dict[str, Any]) -> Package:
|
|
||||||
"""
|
|
||||||
Import a complete package from JSON data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package_data: Dictionary containing the package export data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Package instance
|
|
||||||
"""
|
|
||||||
print(f"Starting import of package: {package_data['name']}")
|
|
||||||
|
|
||||||
# Create the main package
|
|
||||||
package = self._create_package(package_data)
|
|
||||||
|
|
||||||
# Import in dependency order
|
|
||||||
self._import_compounds(package, package_data.get("compounds", []))
|
|
||||||
self._import_structures(package, package_data.get("structures", []))
|
|
||||||
self._import_rules(package, package_data.get("rules", {}))
|
|
||||||
self._import_reactions(package, package_data.get("reactions", []))
|
|
||||||
self._import_pathways(package, package_data.get("pathways", []))
|
|
||||||
self._import_nodes(package, package_data.get("nodes", []))
|
|
||||||
self._import_edges(package, package_data.get("edges", []))
|
|
||||||
self._import_scenarios(package, package_data.get("scenarios", []))
|
|
||||||
|
|
||||||
if package_data.get("models"):
|
|
||||||
self._import_models(package, package_data["models"])
|
|
||||||
|
|
||||||
# Set default structures for compounds (after all structures are created)
|
|
||||||
self._set_default_structures(package_data.get("compounds", []))
|
|
||||||
|
|
||||||
print(f"Package import completed: {package.name}")
|
|
||||||
return package
|
|
||||||
|
|
||||||
def _create_package(self, package_data: Dict[str, Any]) -> Package:
|
|
||||||
"""Create the main package object."""
|
|
||||||
package_uuid = self._get_or_generate_uuid(package_data["uuid"])
|
|
||||||
|
|
||||||
# Handle license
|
|
||||||
license_obj = None
|
|
||||||
if package_data.get("license"):
|
|
||||||
license_data = package_data["license"]
|
|
||||||
license_obj, _ = License.objects.get_or_create(
|
|
||||||
name=license_data["name"],
|
|
||||||
defaults={
|
|
||||||
"cc_string": license_data.get("cc_string", ""),
|
|
||||||
"link": license_data.get("link", ""),
|
|
||||||
"image_link": license_data.get("image_link", ""),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
new_name = package_data.get("name")
|
|
||||||
if self.add_import_timestamp:
|
|
||||||
new_name = f"{new_name} - Imported at {datetime.now()}"
|
|
||||||
|
|
||||||
new_reviewed = False
|
|
||||||
if self.trust_reviewed:
|
|
||||||
new_reviewed = package_data.get("reviewed", False)
|
|
||||||
|
|
||||||
package = Package.objects.create(
|
|
||||||
uuid=package_uuid,
|
|
||||||
name=new_name,
|
|
||||||
description=package_data["description"],
|
|
||||||
kv=package_data.get("kv", {}),
|
|
||||||
reviewed=new_reviewed,
|
|
||||||
license=license_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cache_object("Package", package_data["uuid"], package)
|
|
||||||
print(f"Created package: {package.name}")
|
|
||||||
return package
|
|
||||||
|
|
||||||
def _import_compounds(self, package: Package, compounds_data: List[Dict[str, Any]]):
|
|
||||||
"""Import compounds."""
|
|
||||||
print(f"Importing {len(compounds_data)} compounds...")
|
|
||||||
|
|
||||||
for compound_data in compounds_data:
|
|
||||||
compound_uuid = self._get_or_generate_uuid(compound_data["uuid"])
|
|
||||||
|
|
||||||
compound = Compound.objects.create(
|
|
||||||
uuid=compound_uuid,
|
|
||||||
package=package,
|
|
||||||
name=compound_data["name"],
|
|
||||||
description=compound_data["description"],
|
|
||||||
kv=compound_data.get("kv", {}),
|
|
||||||
# default_structure will be set later
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if compound_data.get("aliases"):
|
|
||||||
compound.aliases = compound_data["aliases"]
|
|
||||||
compound.save()
|
|
||||||
|
|
||||||
self._cache_object("Compound", compound_data["uuid"], compound)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
compound, compound_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_structures(self, package: Package, structures_data: List[Dict[str, Any]]):
|
|
||||||
"""Import compound structures."""
|
|
||||||
print(f"Importing {len(structures_data)} compound structures...")
|
|
||||||
|
|
||||||
for structure_data in structures_data:
|
|
||||||
structure_uuid = self._get_or_generate_uuid(structure_data["uuid"])
|
|
||||||
compound_uuid = structure_data["compound"]["uuid"]
|
|
||||||
compound = self._get_cached_object("Compound", compound_uuid)
|
|
||||||
|
|
||||||
if not compound:
|
|
||||||
print(f"Warning: Compound with UUID {compound_uuid} not found for structure")
|
|
||||||
continue
|
|
||||||
|
|
||||||
structure = CompoundStructure.objects.create(
|
|
||||||
uuid=structure_uuid,
|
|
||||||
compound=compound,
|
|
||||||
name=structure_data["name"],
|
|
||||||
description=structure_data["description"],
|
|
||||||
kv=structure_data.get("kv", {}),
|
|
||||||
smiles=structure_data["smiles"],
|
|
||||||
canonical_smiles=structure_data["canonical_smiles"],
|
|
||||||
inchikey=structure_data["inchikey"],
|
|
||||||
normalized_structure=structure_data.get("normalized_structure", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if structure_data.get("aliases"):
|
|
||||||
structure.aliases = structure_data["aliases"]
|
|
||||||
structure.save()
|
|
||||||
|
|
||||||
self._cache_object("CompoundStructure", structure_data["uuid"], structure)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
structure, structure_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_rules(self, package: Package, rules_data: Dict[str, Any]):
|
|
||||||
"""Import all types of rules."""
|
|
||||||
print("Importing rules...")
|
|
||||||
|
|
||||||
# Import simple rules first
|
|
||||||
simple_rules_data = rules_data.get("simple_rules", [])
|
|
||||||
print(f"Importing {len(simple_rules_data)} simple rules...")
|
|
||||||
|
|
||||||
for rule_data in simple_rules_data:
|
|
||||||
self._create_simple_rule(package, rule_data)
|
|
||||||
|
|
||||||
# Import parallel rules
|
|
||||||
parallel_rules_data = rules_data.get("parallel_rules", [])
|
|
||||||
print(f"Importing {len(parallel_rules_data)} parallel rules...")
|
|
||||||
|
|
||||||
for rule_data in parallel_rules_data:
|
|
||||||
self._create_parallel_rule(package, rule_data)
|
|
||||||
|
|
||||||
def _create_simple_rule(self, package: Package, rule_data: Dict[str, Any]):
|
|
||||||
"""Create a simple rule (SimpleAmbitRule or SimpleRDKitRule)."""
|
|
||||||
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
|
|
||||||
rule_type = rule_data.get("rule_type", "SimpleRule")
|
|
||||||
|
|
||||||
common_fields = {
|
|
||||||
"uuid": rule_uuid,
|
|
||||||
"package": package,
|
|
||||||
"name": rule_data["name"],
|
|
||||||
"description": rule_data["description"],
|
|
||||||
"kv": rule_data.get("kv", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule_type == "SimpleAmbitRule":
|
|
||||||
rule = SimpleAmbitRule.objects.create(
|
|
||||||
**common_fields,
|
|
||||||
smirks=rule_data.get("smirks", ""),
|
|
||||||
reactant_filter_smarts=rule_data.get("reactant_filter_smarts", ""),
|
|
||||||
product_filter_smarts=rule_data.get("product_filter_smarts", ""),
|
|
||||||
)
|
|
||||||
elif rule_type == "SimpleRDKitRule":
|
|
||||||
rule = SimpleRDKitRule.objects.create(
|
|
||||||
**common_fields, reaction_smarts=rule_data.get("reaction_smarts", "")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
rule = SimpleRule.objects.create(**common_fields)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if rule_data.get("aliases"):
|
|
||||||
rule.aliases = rule_data["aliases"]
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
self._cache_object("SimpleRule", rule_data["uuid"], rule)
|
|
||||||
return rule
|
|
||||||
|
|
||||||
def _create_parallel_rule(self, package: Package, rule_data: Dict[str, Any]):
|
|
||||||
"""Create a parallel rule."""
|
|
||||||
rule_uuid = self._get_or_generate_uuid(rule_data["uuid"])
|
|
||||||
|
|
||||||
rule = ParallelRule.objects.create(
|
|
||||||
uuid=rule_uuid,
|
|
||||||
package=package,
|
|
||||||
name=rule_data["name"],
|
|
||||||
description=rule_data["description"],
|
|
||||||
kv=rule_data.get("kv", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if rule_data.get("aliases"):
|
|
||||||
rule.aliases = rule_data["aliases"]
|
|
||||||
rule.save()
|
|
||||||
|
|
||||||
# Add simple rules
|
|
||||||
for simple_rule_ref in rule_data.get("simple_rules", []):
|
|
||||||
simple_rule = self._get_cached_object("SimpleRule", simple_rule_ref["uuid"])
|
|
||||||
if simple_rule:
|
|
||||||
rule.simple_rules.add(simple_rule)
|
|
||||||
|
|
||||||
self._cache_object("ParallelRule", rule_data["uuid"], rule)
|
|
||||||
return rule
|
|
||||||
|
|
||||||
def _import_reactions(self, package: Package, reactions_data: List[Dict[str, Any]]):
|
|
||||||
"""Import reactions."""
|
|
||||||
print(f"Importing {len(reactions_data)} reactions...")
|
|
||||||
|
|
||||||
for reaction_data in reactions_data:
|
|
||||||
reaction_uuid = self._get_or_generate_uuid(reaction_data["uuid"])
|
|
||||||
|
|
||||||
reaction = Reaction.objects.create(
|
|
||||||
uuid=reaction_uuid,
|
|
||||||
package=package,
|
|
||||||
name=reaction_data["name"],
|
|
||||||
description=reaction_data["description"],
|
|
||||||
kv=reaction_data.get("kv", {}),
|
|
||||||
multi_step=reaction_data.get("multi_step", False),
|
|
||||||
medline_references=reaction_data.get("medline_references", []),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if reaction_data.get("aliases"):
|
|
||||||
reaction.aliases = reaction_data["aliases"]
|
|
||||||
reaction.save()
|
|
||||||
|
|
||||||
# Add educts and products
|
|
||||||
for educt_ref in reaction_data.get("educts", []):
|
|
||||||
compound = self._get_cached_object("CompoundStructure", educt_ref["uuid"])
|
|
||||||
if compound:
|
|
||||||
reaction.educts.add(compound)
|
|
||||||
|
|
||||||
for product_ref in reaction_data.get("products", []):
|
|
||||||
compound = self._get_cached_object("CompoundStructure", product_ref["uuid"])
|
|
||||||
if compound:
|
|
||||||
reaction.products.add(compound)
|
|
||||||
|
|
||||||
# Add rules
|
|
||||||
for rule_ref in reaction_data.get("rules", []):
|
|
||||||
# Try to find rule in different caches
|
|
||||||
rule = self._get_cached_object(
|
|
||||||
"SimpleRule", rule_ref["uuid"]
|
|
||||||
) or self._get_cached_object("ParallelRule", rule_ref["uuid"])
|
|
||||||
if rule:
|
|
||||||
reaction.rules.add(rule)
|
|
||||||
|
|
||||||
self._cache_object("Reaction", reaction_data["uuid"], reaction)
|
|
||||||
|
|
||||||
# Handle external identifiers
|
|
||||||
self._create_external_identifiers(
|
|
||||||
reaction, reaction_data.get("external_identifiers", [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _import_pathways(self, package: Package, pathways_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathways."""
|
|
||||||
print(f"Importing {len(pathways_data)} pathways...")
|
|
||||||
|
|
||||||
for pathway_data in pathways_data:
|
|
||||||
pathway_uuid = self._get_or_generate_uuid(pathway_data["uuid"])
|
|
||||||
|
|
||||||
pathway = Pathway.objects.create(
|
|
||||||
uuid=pathway_uuid,
|
|
||||||
package=package,
|
|
||||||
name=pathway_data["name"],
|
|
||||||
description=pathway_data["description"],
|
|
||||||
kv=pathway_data.get("kv", {}),
|
|
||||||
# setting will be handled separately if needed
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if pathway_data.get("aliases"):
|
|
||||||
pathway.aliases = pathway_data["aliases"]
|
|
||||||
pathway.save()
|
|
||||||
|
|
||||||
self._cache_object("Pathway", pathway_data["uuid"], pathway)
|
|
||||||
|
|
||||||
def _import_nodes(self, package: Package, nodes_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathway nodes."""
|
|
||||||
print(f"Importing {len(nodes_data)} nodes...")
|
|
||||||
|
|
||||||
for node_data in nodes_data:
|
|
||||||
node_uuid = self._get_or_generate_uuid(node_data["uuid"])
|
|
||||||
pathway_uuid = node_data["pathway"]["uuid"]
|
|
||||||
pathway = self._get_cached_object("Pathway", pathway_uuid)
|
|
||||||
|
|
||||||
if not pathway:
|
|
||||||
print(f"Warning: Pathway with UUID {pathway_uuid} not found for node")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# For now, we'll set default_node_label to None and handle it later
|
|
||||||
# as it requires compound structures to be fully imported
|
|
||||||
node = Node.objects.create(
|
|
||||||
uuid=node_uuid,
|
|
||||||
pathway=pathway,
|
|
||||||
name=node_data["name"],
|
|
||||||
description=node_data["description"],
|
|
||||||
kv=node_data.get("kv", {}),
|
|
||||||
depth=node_data.get("depth", 0),
|
|
||||||
default_node_label=self._get_cached_object(
|
|
||||||
"CompoundStructure", node_data["default_node_label"]["uuid"]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if node_data.get("aliases"):
|
|
||||||
node.aliases = node_data["aliases"]
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
self._cache_object("Node", node_data["uuid"], node)
|
|
||||||
# Store node_data for later processing of relationships
|
|
||||||
node._import_data = node_data
|
|
||||||
|
|
||||||
def _import_edges(self, package: Package, edges_data: List[Dict[str, Any]]):
|
|
||||||
"""Import pathway edges."""
|
|
||||||
print(f"Importing {len(edges_data)} edges...")
|
|
||||||
|
|
||||||
for edge_data in edges_data:
|
|
||||||
edge_uuid = self._get_or_generate_uuid(edge_data["uuid"])
|
|
||||||
pathway_uuid = edge_data["pathway"]["uuid"]
|
|
||||||
pathway = self._get_cached_object("Pathway", pathway_uuid)
|
|
||||||
|
|
||||||
if not pathway:
|
|
||||||
print(f"Warning: Pathway with UUID {pathway_uuid} not found for edge")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# For now, we'll set edge_label to None and handle it later
|
|
||||||
edge = Edge.objects.create(
|
|
||||||
uuid=edge_uuid,
|
|
||||||
pathway=pathway,
|
|
||||||
name=edge_data["name"],
|
|
||||||
description=edge_data["description"],
|
|
||||||
kv=edge_data.get("kv", {}),
|
|
||||||
edge_label=self._get_cached_object("Reaction", edge_data["edge_label"]["uuid"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if edge_data.get("aliases"):
|
|
||||||
edge.aliases = edge_data["aliases"]
|
|
||||||
edge.save()
|
|
||||||
|
|
||||||
# Add start and end nodes
|
|
||||||
for start_node_ref in edge_data.get("start_nodes", []):
|
|
||||||
node = self._get_cached_object("Node", start_node_ref["uuid"])
|
|
||||||
if node:
|
|
||||||
edge.start_nodes.add(node)
|
|
||||||
|
|
||||||
for end_node_ref in edge_data.get("end_nodes", []):
|
|
||||||
node = self._get_cached_object("Node", end_node_ref["uuid"])
|
|
||||||
if node:
|
|
||||||
edge.end_nodes.add(node)
|
|
||||||
|
|
||||||
self._cache_object("Edge", edge_data["uuid"], edge)
|
|
||||||
|
|
||||||
def _import_scenarios(self, package: Package, scenarios_data: List[Dict[str, Any]]):
|
|
||||||
"""Import scenarios."""
|
|
||||||
print(f"Importing {len(scenarios_data)} scenarios...")
|
|
||||||
|
|
||||||
# First pass: create scenarios without parent relationships
|
|
||||||
for scenario_data in scenarios_data:
|
|
||||||
scenario_uuid = self._get_or_generate_uuid(scenario_data["uuid"])
|
|
||||||
|
|
||||||
scenario_date = None
|
|
||||||
if scenario_data.get("scenario_date"):
|
|
||||||
scenario_date = scenario_data["scenario_date"]
|
|
||||||
|
|
||||||
scenario = Scenario.objects.create(
|
|
||||||
uuid=scenario_uuid,
|
|
||||||
package=package,
|
|
||||||
name=scenario_data["name"],
|
|
||||||
description=scenario_data["description"],
|
|
||||||
kv=scenario_data.get("kv", {}),
|
|
||||||
scenario_date=scenario_date,
|
|
||||||
scenario_type=scenario_data.get("scenario_type"),
|
|
||||||
additional_information=scenario_data.get("additional_information", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cache_object("Scenario", scenario_data["uuid"], scenario)
|
|
||||||
# Store scenario_data for later processing of parent relationships
|
|
||||||
scenario._import_data = scenario_data
|
|
||||||
|
|
||||||
# Second pass: set parent relationships
|
|
||||||
for scenario_data in scenarios_data:
|
|
||||||
if scenario_data.get("parent"):
|
|
||||||
scenario = self._get_cached_object("Scenario", scenario_data["uuid"])
|
|
||||||
parent = self._get_cached_object("Scenario", scenario_data["parent"]["uuid"])
|
|
||||||
if scenario and parent:
|
|
||||||
scenario.parent = parent
|
|
||||||
scenario.save()
|
|
||||||
|
|
||||||
def _import_models(self, package: Package, models_data: List[Dict[str, Any]]):
|
|
||||||
"""Import EPModels."""
|
|
||||||
print(f"Importing {len(models_data)} models...")
|
|
||||||
|
|
||||||
for model_data in models_data:
|
|
||||||
model_uuid = self._get_or_generate_uuid(model_data["uuid"])
|
|
||||||
model_type = model_data.get("model_type", "EPModel")
|
|
||||||
|
|
||||||
common_fields = {
|
|
||||||
"uuid": model_uuid,
|
|
||||||
"package": package,
|
|
||||||
"name": model_data["name"],
|
|
||||||
"description": model_data["description"],
|
|
||||||
"kv": model_data.get("kv", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add PackageBasedModel fields if present
|
|
||||||
if "threshold" in model_data:
|
|
||||||
common_fields.update(
|
|
||||||
{
|
|
||||||
"threshold": model_data.get("threshold"),
|
|
||||||
"eval_results": model_data.get("eval_results", {}),
|
|
||||||
"model_status": model_data.get("model_status", "INITIAL"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the appropriate model type
|
|
||||||
if model_type == "RuleBasedRelativeReasoning":
|
|
||||||
model = RuleBasedRelativeReasoning.objects.create(
|
|
||||||
**common_fields,
|
|
||||||
min_count=model_data.get("min_count", 1),
|
|
||||||
max_count=model_data.get("max_count", 10),
|
|
||||||
)
|
|
||||||
elif model_type == "MLRelativeReasoning":
|
|
||||||
model = MLRelativeReasoning.objects.create(**common_fields)
|
|
||||||
elif model_type == "EnviFormer":
|
|
||||||
model = EnviFormer.objects.create(**common_fields)
|
|
||||||
else:
|
|
||||||
model = EPModel.objects.create(**common_fields)
|
|
||||||
|
|
||||||
# Set aliases if present
|
|
||||||
if model_data.get("aliases"):
|
|
||||||
model.aliases = model_data["aliases"]
|
|
||||||
model.save()
|
|
||||||
|
|
||||||
# Add package relationships for PackageBasedModel
|
|
||||||
if hasattr(model, "rule_packages"):
|
|
||||||
for pkg_ref in model_data.get("rule_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.rule_packages.add(pkg)
|
|
||||||
|
|
||||||
for pkg_ref in model_data.get("data_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.data_packages.add(pkg)
|
|
||||||
|
|
||||||
for pkg_ref in model_data.get("eval_packages", []):
|
|
||||||
pkg = self._get_cached_object("Package", pkg_ref["uuid"])
|
|
||||||
if pkg:
|
|
||||||
model.eval_packages.add(pkg)
|
|
||||||
|
|
||||||
self._cache_object("EPModel", model_data["uuid"], model)
|
|
||||||
|
|
||||||
def _set_default_structures(self, compounds_data: List[Dict[str, Any]]):
|
|
||||||
"""Set default structures for compounds after all structures are created."""
|
|
||||||
print("Setting default structures for compounds...")
|
|
||||||
|
|
||||||
for compound_data in compounds_data:
|
|
||||||
if compound_data.get("default_structure"):
|
|
||||||
compound = self._get_cached_object("Compound", compound_data["uuid"])
|
|
||||||
structure = self._get_cached_object(
|
|
||||||
"CompoundStructure", compound_data["default_structure"]["uuid"]
|
|
||||||
)
|
|
||||||
if compound and structure:
|
|
||||||
compound.default_structure = structure
|
|
||||||
compound.save()
|
|
||||||
|
|
||||||
def _create_external_identifiers(self, obj, identifiers_data: List[Dict[str, Any]]):
|
|
||||||
"""Create external identifiers for an object."""
|
|
||||||
for identifier_data in identifiers_data:
|
|
||||||
# Get or create the external database
|
|
||||||
db_data = identifier_data["database"]
|
|
||||||
database, _ = ExternalDatabase.objects.get_or_create(
|
|
||||||
name=db_data["name"],
|
|
||||||
defaults={
|
|
||||||
"base_url": db_data.get("base_url", ""),
|
|
||||||
"full_name": db_data.get("name", ""),
|
|
||||||
"description": "",
|
|
||||||
"is_active": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the external identifier
|
|
||||||
ExternalIdentifier.objects.create(
|
|
||||||
content_object=obj,
|
|
||||||
database=database,
|
|
||||||
identifier_value=identifier_data["identifier_value"],
|
|
||||||
url=identifier_data.get("url", ""),
|
|
||||||
is_primary=identifier_data.get("is_primary", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PathwayUtils:
|
class PathwayUtils:
|
||||||
def __init__(self, pathway: "Pathway"):
|
def __init__(self, pathway: "Pathway"):
|
||||||
|
|||||||
Reference in New Issue
Block a user