Skip to content

Commit

Permalink
feat(hooks): support removing relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
philtweir committed Apr 22, 2024
1 parent 04da08a commit 868ca01
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 89 deletions.
16 changes: 10 additions & 6 deletions arches_orm/arches_django/datatypes/semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ def get_child_values(svm):
for key, value in children.items():
value._parent_node = svm
if key in svm._child_values:
raise RuntimeError(
reason = (
"Semantic view model construction error - "
f"duplicate keys outside node list: {key}"
f"duplicate keys outside node list: {key}: %s"
)
_tile_loading_error(reason, RuntimeError(reason))
svm._child_values[key] = value

return children
Expand All @@ -82,15 +83,18 @@ def get_child_values(svm):
try:
svm.update(value)
except Exception as exc:
if not get_adapter().config.get("suppress-tile-loading-errors"):
raise exc
elif not get_adapter().config.get("silence-tile-loading-errors"):
logging.warning("Suppressed a tile loading error (tile: %s; node: %s): %s", str(tile), str(node), exc)
_tile_loading_error("Suppressed a tile loading error: %s (tile: %s; node: %s)", exc, str(tile), str(node))
svm.get_children()

return svm


def _tile_loading_error(reason, exc, *args):
if not get_adapter().config.get("suppress-tile-loading-errors"):
raise exc
elif not get_adapter().config.get("silence-tile-loading-errors"):
logging.warning(reason, exc, *args)

@semantic.as_tile_data
def sm_as_tile_data(semantic):
# Ensure all nodes have populated the tile
Expand Down
65 changes: 51 additions & 14 deletions arches_orm/arches_django/hooks.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,72 @@
from typing import Any
from django.dispatch import receiver
from django.db.models.signals import post_delete, post_save
from django.db.models.signals import post_delete, post_save, post_init
from arches.app.models.tile import Tile
from arches.app.models.models import ResourceXResource, ResourceInstance, GraphModel
from copy import deepcopy

from arches_orm.wkrm import get_well_known_resource_model_by_graph_id, attempt_well_known_resource_model


@receiver(post_init, sender=Tile)
def check_resource_instance_on_tile_capture(sender, instance, **kwargs):
instance._original_data = None
if instance.data:
instance._original_data = deepcopy(instance.data)
elif instance.tileid and isinstance(instance.tileid, dict) and "_original_data" in instance.tileid:
instance._original_data = deepcopy(instance.tileid["_original_data"])

@receiver(post_save, sender=Tile)
def check_resource_instance_on_tile_save(sender, instance, **kwargs):
"""Catch saves on tiles for resources."""
if instance.data:
seen = set()
if instance._original_data:
for key, original_values in instance._original_data.items():
if not isinstance(original_values, list):
original_values = [original_values]
for value in original_values:
if value and isinstance(value, dict):
# If resourceXresourceId is unset, then this may only be a temporary value, from
# arches/app/views/tile.py:136 Tile(data)
# TODO: confirm behaviour with save_crosses=True
if value.get("resourceXresourceId") and (rto_id := value.get("resourceId")):
seen.add((key, rto_id))

for key, values in instance.data.items():
if values:
if not isinstance(values, list):
values = [values]
for value in values:
if value and isinstance(value, dict):
# Should always be a resourceXresourceId when this is saved.
if (rXr_id := value.get("resourceXresourceId")) and (rto_id := value.get("resourceId")):
# TODO: inefficient
rto = ResourceInstance.objects.get(resourceinstanceid=rto_id)
if rto:
relationship = ResourceXResource(
resourcexid=rXr_id,
resourceinstanceidfrom=instance.resourceinstance,
resourceinstanceidto=rto,
resourceinstancefrom_graphid=instance.resourceinstance.graph,
resourceinstanceto_graphid=rto.graph
)
if relationship.resourceinstanceto_graphid:
check_related_to(sender, relationship, "relationship saved", tile=instance, nodeid=key, **kwargs)
if (key, rto_id) in seen:
seen.remove((key, rto_id))
else:
# TODO: inefficient
rto = ResourceInstance.objects.get(resourceinstanceid=rto_id)
if rto:
relationship = ResourceXResource(
resourcexid=rXr_id,
resourceinstanceidfrom=instance.resourceinstance,
resourceinstanceidto=rto,
resourceinstancefrom_graphid=instance.resourceinstance.graph,
resourceinstanceto_graphid=rto.graph
)
if relationship.resourceinstanceto_graphid:
check_related_to(sender, relationship, "relationship saved", tile=instance, nodeid=key, **kwargs)
for key, rto_id in seen:
rto = ResourceInstance.objects.get(resourceinstanceid=rto_id)
if rto:
relationship = ResourceXResource(
resourceinstanceidfrom=instance.resourceinstance,
resourceinstanceidto=rto,
resourceinstancefrom_graphid=instance.resourceinstance.graph,
resourceinstanceto_graphid=rto.graph
)
if relationship.resourceinstanceto_graphid:
check_related_to(sender, relationship, "relationship deleted", tile=instance, nodeid=key, **kwargs)
if instance.resourceinstance and instance.resourceinstance.resourceinstanceid:
check_resource_instance(sender, instance, "tile saved", **kwargs)

Expand Down Expand Up @@ -68,7 +105,7 @@ def check_related_to(sender: type[ResourceInstance], instance: ResourceXResource
model_cls_to = get_well_known_resource_model_by_graph_id(
graph_id_to
)
if (model_cls_to and model_cls_to.post_related_to.has_listeners()) or (model_cls_from and model_cls_from.post_related_to.has_listeners()):
if (model_cls_to and model_cls_to.post_related_to.has_listeners()) or (model_cls_from and model_cls_from.post_related_from.has_listeners()):
resource_instance_from = None
resource_instance_to = None
if model_cls_from and instance.resourceinstanceidfrom:
Expand Down
6 changes: 5 additions & 1 deletion arches_orm/arches_django/pseudo_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ def get_tile(self):
tile = self.tile if self.node.is_collector else None
return tile, relationships

def clear(self):
self._value = None
if self.tile and self.tile.data and str(self.node.nodeid) in self.tile.data:
del self.tile.data[str(self.node.nodeid)]

def _update_value(self):
if not self.tile:
if not self.node:
Expand All @@ -160,7 +165,6 @@ def _update_value(self):
else:
data = self._value

print("DATA", data, self.node.datatype, self.node.nodeid, self.tile.data)
self._value, self._as_tile_data, self._datatype, self._multiple = get_view_model_for_datatype(
self.tile,
self.node,
Expand Down
148 changes: 80 additions & 68 deletions arches_orm/arches_django/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,58 +131,64 @@ def to_resource(
# Don't think we actually need this if the resource gets saved, as postsave RI
# datatype handles it. We do for sqlite at the very least, and likely gathering
# for bulk.
if self.get_adapter().config.get("save_crosses", False):
crosses = {
str(cross.tileid): cross
for cross in ResourceXResource.objects.filter(
resourceinstanceidfrom=resource
_no_save = _no_save or not (self.get_adapter().config.get("save_crosses", False))
# TODO: fix expectation of one cross per tile

crosses = {}
for cross in ResourceXResource.objects.filter(
resourceinstanceidfrom=resource
):
crosses.setdefault(str(cross.tileid), [])
crosses[str(cross.tileid)].append(cross)
for tile_ix, nodegroup_id, nodeid, related in relationships:
value = tiles[nodegroup_id][tile_ix].data[nodeid]
tileid = str(tiles[nodegroup_id][tile_ix].tileid)
if not related.id:
if save_related_if_missing:
related.save()
else:
logger.warning("Not saving a related model as not requested")
continue
need_cross = not (tileid in crosses)
cross_resourcexid = None
if tileid in crosses:
for cross in crosses[tileid]:
if (
str(cross.resourceinstanceidto_id) == str(related.id) and
str(cross.resourceinstanceidfrom_id) == str(resource.id)
):
need_cross = False
cross_resourcexid = str(cross.resourcexid)
if need_cross:
cross = ResourceXResource(
resourceinstanceidfrom=resource,
resourceinstanceidto_id=related.id,
)
}
for tile_ix, nodegroup_id, nodeid, related in relationships:
value = tiles[nodegroup_id][tile_ix].data[nodeid]
tileid = str(tiles[nodegroup_id][tile_ix].tileid)
if not related.id:
if save_related_if_missing:
related.save()
else:
logger.warning("Not saving a related model as not requested")
continue
if _no_save:
self._pending_relationships.append((value, related, self))
elif tileid not in crosses:
cross = ResourceXResource(
resourceinstanceidfrom=resource,
resourceinstanceidto_id=related.id,
)
cross.save()
else:
cross = crosses[tileid]
save_cross = False
if str(cross.resourceinstanceidto_id) != str(related.id):
cross.resourceinstanceidto_id = related.id
save_cross = True
if str(cross.resourceinstanceidfrom_id) != str(resource.id):
cross.resourceinstanceidfrom_id = resource.id
save_cross = True
if save_cross:
cross.save()

cross_value = {
"resourceId": str(cross.resourceinstanceidto_id),
"ontologyProperty": "",
"resourceXresourceId": str(cross.resourcexid),
"inverseOntologyProperty": "",
}
if isinstance(value, list):
if not any(
entry["resourceXresourceId"]
== cross_value["resourceXresourceId"]
for entry in value
):
value.append(cross_value)
else:
value.update(cross_value)
do_final_save = True
cross.save()
cross_resourcexid = str(cross.resourcexid)

cross_value = {
"resourceId": str(cross.resourceinstanceidto_id),
"ontologyProperty": "",
"resourceXresourceId": cross_resourcexid,
"inverseOntologyProperty": "",
}
if isinstance(value, list):
if not any(
(entry["resourceId"] == cross_value["resourceId"]) or
(
entry["resourceXresourceId"] is not None and
entry["resourceXresourceId"] == cross_value["resourceXresourceId"]
)
for entry in value
):
value.append(cross_value)
else:
value.update(cross_value)
do_final_save = True
if do_final_save:
resource.save()

Expand Down Expand Up @@ -481,27 +487,31 @@ def remove(self):
if not self._cross_record:
raise NotImplementedError("This method is only implemented for relations")

from arches_orm.view_models.resources import RelatedResourceInstanceViewModelMixin, RelatedResourceInstanceListViewModel

wkfm = self._cross_record["wkriFrom"]
key = self._cross_record["wkriFromKey"]
pseudo_node_list = wkfm._values[key]
if not isinstance(pseudo_node_list, PseudoNodeList) and not isinstance(pseudo_node_list, list):
pseudo_node_list = [pseudo_node_list]

for pseudo_node in pseudo_node_list:
if isinstance(pseudo_node.value, RelatedResourceInstanceListViewModel):
pseudo_node.value.remove(self)
elif isinstance(pseudo_node.value, RelatedResourceInstanceViewModelMixin):
# if str(pseudo_node.value.resourceinstanceid) != str(self.id):
# raise RuntimeError(
# f"Mix-up when removing a related resource for {key} of {wkfm.id},"
# f" which should be {self.id} but is {pseudo_node.value.resourceinstanceid}"
# )
if str(pseudo_node.value.resourceinstanceid) == str(self.id):
pseudo_node.clear()
# else:
# raise RuntimeError(
# f"Mix-up when removing a related resource for {key} of {wkfm.id},"
# f" which should be a related resource, but is {type(pseudo_node.value)}"
# )
wkfm.save()
resource = wkfm.to_resource()
tile = resource.tiles
nodeid = str(wkfm._nodes()[key].nodeid)
nodegroupid = str(wkfm._nodes()[key].nodegroup_id)
for tile in resource.tiles:
if nodegroupid == str(tile.nodegroup_id):
ResourceXResource.objects.filter(
resourceinstanceidfrom=wkfm.resource,
resourceinstanceidto=self.resource,
).delete()
del tile.data[nodeid]

# This is required to avoid e.g. missing related models preventing
# saving (as we cannot import those via CSV on first step)
bypass = system_settings.BYPASS_REQUIRED_VALUE_TILE_VALIDATION
system_settings.BYPASS_REQUIRED_VALUE_TILE_VALIDATION = True
resource.save()
system_settings.BYPASS_REQUIRED_VALUE_TILE_VALIDATION = bypass

def append(self, _no_save=False):
"""When called via a relationship (dot), append to the relationship."""
Expand All @@ -525,14 +535,15 @@ def append(self, _no_save=False):
resourceinstanceidto=self.resource,
)
cross.save()
value = [
value = (tile.data or {}).get(nodeid, [])
value.append(
{
"resourceId": str(self.resource.resourceinstanceid),
"ontologyProperty": "",
"resourceXresourceId": str(cross.resourcexid),
"inverseOntologyProperty": "",
}
]
)
tile.data.update({nodeid: value})

# This is required to avoid e.g. missing related models preventing
Expand Down Expand Up @@ -567,6 +578,7 @@ def _add_node(node, tile):
key = node.alias
all_values.setdefault(key, [])
pseudo_node = cls._make_pseudo_node_cls(key, tile=tile, wkri=wkri)
# RMV: is this necessary?
if key == "full_name" and tile is not None:
pseudo_node.get_tile()
# We shouldn't have to take care of this case, as it should already
Expand Down
4 changes: 4 additions & 0 deletions arches_orm/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ def __init__(
for key, value in kwargs.items():
setattr(self, key, value)

@property
def resourceinstanceid(self):
return self.id

@classmethod
def create_bulk(cls, fields: list, do_index: bool = True):
raise NotImplementedError("The bulk_create module needs to be rewritten")
Expand Down

0 comments on commit 868ca01

Please sign in to comment.