diff --git a/arches/app/media/js/viewmodels/graph-settings.js b/arches/app/media/js/viewmodels/graph-settings.js index d681a234bd6..1d79e661d74 100644 --- a/arches/app/media/js/viewmodels/graph-settings.js +++ b/arches/app/media/js/viewmodels/graph-settings.js @@ -15,9 +15,12 @@ define([ self.resource_data = ko.observableArray([]); self.relatable_resources = ko.computed(function() { - return _.each(self.resource_data(), function(resource) { - resource.isRelatable = ko.observable(resource.is_relatable); - }).filter(resource => !resource.graph.source_identifier_id); + return _.each( + self.resource_data().sort((a, b) => a.graph.name.localeCompare(b.graph.name)), + function(resource) { + resource.isRelatable = ko.observable(resource.is_relatable); + } + ).filter(resource => !resource.graph.source_identifier_id); }); self.designerViewModel = params.designerViewModel; diff --git a/arches/app/models/card.py b/arches/app/models/card.py index 88fc304826d..c7fab63e460 100644 --- a/arches/app/models/card.py +++ b/arches/app/models/card.py @@ -207,9 +207,6 @@ def save(self): edge.ontologyproperty = self.ontologyproperty edge.save() - self.nodegroup.cardinality = self.cardinality - self.nodegroup.save() - super(Card, self).save() for widget in self.widgets: widget.save() diff --git a/arches/app/models/graph.py b/arches/app/models/graph.py index a531ee953de..ac1dfa780aa 100644 --- a/arches/app/models/graph.py +++ b/arches/app/models/graph.py @@ -733,7 +733,7 @@ def delete(self): except Graph.DoesNotExist: pass # no editable future graph to delete - for nodegroup in self.get_nodegroups(): + for nodegroup in self.get_nodegroups(force_recalculation=True): nodegroup.delete() for edge in self.edges.values(): @@ -1706,9 +1706,15 @@ def update_permissions(self, serialized_graph): "user_permissions" ].values(): for serialized_user_permission in serialized_user_permission_list: + serialized_user_permission["content_object"] = ( + models.NodeGroup.objects.get( + pk=serialized_user_permission["object_pk"] + ) + ) updated_user_permission = UserObjectPermission( **serialized_user_permission ) + updated_user_permission.save() if "group_permissions" in serialized_graph: @@ -1726,9 +1732,15 @@ def update_permissions(self, serialized_graph): "group_permissions" ].values(): for serialized_group_permission in serialized_group_permission_list: + serialized_group_permission["content_object"] = ( + models.NodeGroup.objects.get( + pk=serialized_group_permission["object_pk"] + ) + ) updated_group_permission = GroupObjectPermission( **serialized_group_permission ) + updated_group_permission.save() def get_user_permissions(self, force_recalculation=False): @@ -2301,22 +2313,17 @@ def validate_fieldname(fieldname, fieldnames): .exclude(source_identifier__isnull=False) .filter(slug=self.slug) ) - if ( - first_matching_graph := graphs_with_matching_slug.first() - ) and first_matching_graph.graphid != self.graphid: - if self.source_identifier_id: - if self.source_identifier_id != first_matching_graph.graphid: - raise GraphValidationError( - _( - "Another resource model already uses the slug '{self.slug}'" - ).format(**locals()), - 1007, - ) - else: + if (first_matching_graph := graphs_with_matching_slug.first()) and str( + first_matching_graph.graphid + ) != str(self.graphid): + if ( + not self.source_identifier_id + or self.source_identifier_id != first_matching_graph.graphid + ): raise GraphValidationError( _( - "Another resource model already uses the slug '{self.slug}'" - ).format(**locals()), + "Another resource model already uses the slug '{slug}'" + ).format(slug=self.slug), 1007, ) @@ -2412,406 +2419,159 @@ def create_editable_future_graph(self): return editable_future_graph - def update_from_editable_future_graph(self): + def update_from_editable_future_graph(self, editable_future_graph): """ Updates the graph with any changes made to the editable future graph, removes the editable future graph and related resources, then creates an editable future graph from the updated graph. """ - try: - editable_future_graph = Graph.objects.get(source_identifier_id=self.pk) - except: - raise Exception(_("No identifiable future Graph")) - - def _update_source_nodegroup_hierarchy(nodegroup): - if not nodegroup: - return None - - node = models.Node.objects.get(pk=nodegroup.pk) - if node.source_identifier_id: - source_nodegroup = models.NodeGroup.objects.get( - pk=node.source_identifier_id - ) - - source_nodegroup.cardinality = nodegroup.cardinality - source_nodegroup.legacygroupid = nodegroup.legacygroupid - - if nodegroup.parentnodegroup_id: - nodegroup_parent_node = models.Node.objects.get( - pk=nodegroup.parentnodegroup_id - ) - - if nodegroup_parent_node.source_identifier_id: - source_nodegroup.parentnodegroup_id = ( - nodegroup_parent_node.source_identifier_id - ) + serialized_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(self) + ) + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) - source_nodegroup.save() + node_id_to_node_source_identifier_id = { + node["nodeid"]: node["source_identifier_id"] + for node in serialized_editable_future_graph["nodes"] + if node["source_identifier_id"] + } - if nodegroup.parentnodegroup: - _update_source_nodegroup_hierarchy(nodegroup=nodegroup.parentnodegroup) + card_id_to_card_source_identifier_id = { + card["cardid"]: card["source_identifier_id"] + for card in serialized_editable_future_graph["cards"] + if card["source_identifier_id"] + } - with transaction.atomic(): - self.root.set_relatable_resources( - [ - node.pk - for node in editable_future_graph.root.get_relatable_resources() + # update cards_x_nodes_x_widgets + for serialized_card_x_node_x_widget in serialized_editable_future_graph[ + "cards_x_nodes_x_widgets" + ]: + if serialized_card_x_node_x_widget["source_identifier_id"]: + serialized_card_x_node_x_widget["id"] = serialized_card_x_node_x_widget[ + "source_identifier_id" ] - ) + serialized_card_x_node_x_widget["source_identifier_id"] = None - previous_card_ids = [str(card.pk) for card in self.cards.values()] - previous_node_ids = [str(node.pk) for node in self.nodes.values()] - previous_edge_ids = [str(edge.pk) for edge in self.edges.values()] - previous_widget_ids = [str(widget.pk) for widget in self.widgets.values()] - previous_nodegroup_ids = [ - str(nodegroup.pk) - for nodegroup in self.get_nodegroups(force_recalculation=True) - ] + updated_card_id = card_id_to_card_source_identifier_id.get( + serialized_card_x_node_x_widget["card_id"] + ) + if updated_card_id: + serialized_card_x_node_x_widget["card_id"] = updated_card_id - self.cards = {} - self.nodes = {} - self.edges = {} - self.widgets = {} - - # BEGIN update related models - # Iterates over cards, nodes, and edges of the editable_future_graph. If the item - # has a `source_identifier` attribute, it represents an item related to the source - # graph ( the graph mapped to `self` ); we iterate over the item attributes and map - # them to source item. If the item does not have a `source_identifier` attribute, it - # has been newly created; we update the `graph_id` to match the source graph. We are - # not saving in this block so updates can accur in any order. - for future_widget in list(editable_future_graph.widgets.values()): - source_widget = future_widget.source_identifier - - if future_widget.source_identifier_id: - for key in vars(source_widget).keys(): - if key not in [ - "_state", - "id", - "node_id", - "card_id", - "source_identifier_id", - ]: - setattr(source_widget, key, getattr(future_widget, key)) - - if future_widget.card.source_identifier_id: - source_widget.card_id = future_widget.card.source_identifier_id - if future_widget.node.source_identifier_id: - source_widget.node_id = future_widget.node.source_identifier_id - - self.widgets[source_widget.pk] = source_widget - else: # newly-created widget - future_widget.source_identifier_id = None - - if future_widget.card.source_identifier_id: - future_widget.card_id = future_widget.card.source_identifier_id - if future_widget.node.source_identifier_id: - future_widget.node_id = future_widget.node.source_identifier_id - - del editable_future_graph.widgets[future_widget.pk] - self.widgets[future_widget.pk] = future_widget - - for future_card in list(editable_future_graph.cards.values()): - future_card_nodegroup_node = models.Node.objects.get( - pk=future_card.nodegroup.pk - ) + updated_node_id = node_id_to_node_source_identifier_id.get( + serialized_card_x_node_x_widget["node_id"] + ) + if updated_node_id: + serialized_card_x_node_x_widget["node_id"] = updated_node_id - if future_card.source_identifier: - source_card = future_card.source_identifier - - for key in vars(source_card).keys(): - if key not in [ - "graph_id", - "cardid", - "nodegroup_id", - "source_identifier_id", - ]: - if ( - key == "config" - and str(future_card.component_id) - == "2f9054d8-de57-45cd-8a9c-58bbb1619030" - ): # grouping card - grouped_card_ids = [] - for grouped_card_id in future_card.config[ - "groupedCardIds" - ]: - grouped_card = Card.objects.get(pk=grouped_card_id) - grouped_card_ids.append( - str(grouped_card.source_identifier_id) - ) - - source_card.config["groupedCardIds"] = grouped_card_ids - - sorted_widget_ids = [] - for node_id in future_card.config["sortedWidgetIds"]: - sorted_widget = models.Node.objects.get(pk=node_id) - sorted_widget_ids.append( - str(sorted_widget.source_identifier_id) - ) - - source_card.config["sortedWidgetIds"] = ( - sorted_widget_ids - ) - else: - setattr(source_card, key, getattr(future_card, key)) + # update cards + for serialized_card in serialized_editable_future_graph["cards"]: + if serialized_card["source_identifier_id"]: + serialized_card["cardid"] = serialized_card["source_identifier_id"] + serialized_card["source_identifier_id"] = None - source_card.nodegroup_id = future_card_nodegroup_node.nodegroup_id - if future_card_nodegroup_node.source_identifier_id: - source_card.nodegroup_id = ( - future_card_nodegroup_node.source_identifier_id - ) + source_nodegroup_id = node_id_to_node_source_identifier_id.get( + serialized_card["nodegroup_id"] + ) + if source_nodegroup_id: + serialized_card["nodegroup_id"] = source_nodegroup_id - self.cards[source_card.pk] = source_card - else: # newly-created card - future_card.graph_id = self.pk - future_card.source_identifier_id = None + serialized_card["graph_id"] = serialized_source_graph["graphid"] - future_card.nodegroup_id = future_card_nodegroup_node.nodegroup_id - if future_card_nodegroup_node.source_identifier_id: - future_card.nodegroup_id = ( - future_card_nodegroup_node.source_identifier_id - ) + # update nodes + for serialized_node in serialized_editable_future_graph["nodes"]: + if serialized_node["source_identifier_id"]: + serialized_node["nodeid"] = serialized_node["source_identifier_id"] + serialized_node["source_identifier_id"] = None - del editable_future_graph.cards[future_card.pk] - self.cards[future_card.pk] = future_card - - _update_source_nodegroup_hierarchy(future_card.nodegroup) - - for future_edge in list(editable_future_graph.edges.values()): - if future_edge.source_identifier_id: - source_edge = future_edge.source_identifier - - for key in vars(source_edge).keys(): - if key not in [ - "domainnode_id", - "edgeid", - "graph_id", - "rangenode_id", - "source_identifier_id", - ]: - setattr(source_edge, key, getattr(future_edge, key)) - - source_edge.domainnode_id = future_edge.domainnode_id - if future_edge.domainnode.source_identifier: - source_edge.domainnode_id = ( - future_edge.domainnode.source_identifier.pk - ) - - source_edge.rangenode_id = future_edge.rangenode_id - if future_edge.rangenode.source_identifier: - source_edge.rangenode_id = ( - future_edge.rangenode.source_identifier.pk - ) + updated_nodegroup_id = node_id_to_node_source_identifier_id.get( + serialized_node["nodegroup_id"] + ) + if updated_nodegroup_id: + serialized_node["nodegroup_id"] = updated_nodegroup_id - self.edges[source_edge.pk] = source_edge - else: # newly-created edge - future_edge.graph_id = self.pk - future_edge.source_identfier_id = None + serialized_node["graph_id"] = serialized_source_graph["graphid"] - if future_edge.domainnode.source_identifier_id: - future_edge.domainnode_id = ( - future_edge.domainnode.source_identifier_id - ) + # update nodegroups + for serialized_nodegroup in serialized_editable_future_graph["nodegroups"]: + updated_nodegroup_id = node_id_to_node_source_identifier_id.get( + serialized_nodegroup["nodegroupid"] + ) + if updated_nodegroup_id: + serialized_nodegroup["nodegroupid"] = updated_nodegroup_id - if future_edge.rangenode.source_identifier_id: - future_edge.rangenode_id = ( - future_edge.rangenode.source_identifier_id - ) + updated_parent_nodegroup_id = node_id_to_node_source_identifier_id.get( + serialized_nodegroup["parentnodegroup_id"] + ) + if updated_nodegroup_id: + serialized_nodegroup["parentnodegroup_id"] = updated_parent_nodegroup_id - del editable_future_graph.edges[future_edge.pk] - self.edges[future_edge.pk] = future_edge + # update edges + for serialized_edge in serialized_editable_future_graph["edges"]: + if serialized_edge["source_identifier_id"]: + serialized_edge["edgeid"] = serialized_edge["source_identifier_id"] + serialized_edge["source_identifier_id"] = None - for future_node in list(editable_future_graph.nodes.values()): - future_node_nodegroup_node = ( - models.Node.objects.get(pk=future_node.nodegroup.pk) - if future_node.nodegroup - else None - ) + source_domain_node_id = node_id_to_node_source_identifier_id.get( + serialized_edge["domainnode_id"] + ) + if source_domain_node_id: + serialized_edge["domainnode_id"] = source_domain_node_id - if future_node.source_identifier: - source_node = future_node.source_identifier + source_range_node_id = node_id_to_node_source_identifier_id.get( + serialized_edge["rangenode_id"] + ) + if source_range_node_id: + serialized_edge["rangenode_id"] = source_range_node_id - for key in vars(source_node).keys(): - if key not in [ - "graph_id", - "nodeid", - "nodegroup_id", - "source_identifier_id", - "is_collector", - ]: - setattr(source_node, key, getattr(future_node, key)) + serialized_edge["graph_id"] = serialized_source_graph["graphid"] - source_node.nodegroup_id = future_node.nodegroup_id - if ( - future_node_nodegroup_node - and future_node_nodegroup_node.source_identifier_id - ): - source_node.nodegroup_id = ( - future_node_nodegroup_node.source_identifier_id - ) + # update root node + serialized_editable_future_graph["root"]["graph_id"] = serialized_source_graph[ + "graphid" + ] + serialized_editable_future_graph["root"]["nodeid"] = ( + serialized_editable_future_graph["root"]["source_identifier_id"] + ) + serialized_editable_future_graph["root"]["source_identifier_id"] = None - self.nodes[source_node.pk] = source_node - else: # newly-created node - future_node.graph_id = self.pk - future_node.source_identifier_id = None + # update graph data + serialized_editable_future_graph["graphid"] = serialized_source_graph["graphid"] + serialized_editable_future_graph["has_unpublished_changes"] = False + serialized_editable_future_graph["resource_instance_lifecycle_id"] = ( + serialized_source_graph["resource_instance_lifecycle_id"] + ) + serialized_editable_future_graph["source_identifier_id"] = None - if ( - future_node_nodegroup_node - and future_node_nodegroup_node.source_identifier_id - ): - future_node.nodegroup_id = ( - future_node_nodegroup_node.source_identifier_id - ) + # update permissions + serialized_editable_future_graph["group_permissions"] = { + key: value + for key, value in serialized_source_graph["group_permissions"].items() + if key in node_id_to_node_source_identifier_id.values() + } + serialized_editable_future_graph["user_permissions"] = { + key: value + for key, value in serialized_source_graph["user_permissions"].items() + if key in node_id_to_node_source_identifier_id.values() + } - del editable_future_graph.nodes[future_node.pk] - self.nodes[future_node.pk] = future_node - - _update_source_nodegroup_hierarchy(future_node.nodegroup) - # END update related models - - # BEGIN copy attrs from editable_future_graph to source_graph - for key, value in vars(editable_future_graph).items(): - if key not in [ - "_state", - "graphid", - "cards", - "nodes", - "edges", - "widgets", - "root", - "source_identifier", - "source_identifier_id", - "resource_instance_lifecycle", - "resource_instance_lifecycle_id", - "publication_id", - "_nodegroups_to_delete", - "_functions", - "_card_constraints", - "_constraints_x_nodes", - "serialized_graph", - ]: - setattr(self, key, value) - - self.root = self.nodes[self.root.pk] - # END copy attrs from editable_future_graph to source_graph - - # BEGIN delete superflous models - # Compares UUIDs between models related to the source graph and models related to - # the editable_future_graph. If the item related to the source graph exists, but the item - # related to the editable_future_graph does not exist, the item related to the source graph - # should be deleted. - updated_card_ids = [ - str(card.source_identifier_id) - for card in editable_future_graph.cards.values() - ] - updated_node_ids = [ - str(node.source_identifier_id) - for node in editable_future_graph.nodes.values() - ] - updated_edge_ids = [ - str(edge.source_identifier_id) - for edge in editable_future_graph.edges.values() - ] - updated_widget_ids = [ - str(widget.pk) for widget in editable_future_graph.widgets.values() + serialized_editable_future_graph["relatable_resource_model_ids"] = [ + ( + serialized_source_graph["graphid"] + if relatable_resource_model_id + == serialized_editable_future_graph["graphid"] + else relatable_resource_model_id + ) + for relatable_resource_model_id in serialized_editable_future_graph[ + "relatable_resource_model_ids" ] + ] - updated_node_ids.append(str(self.root.pk)) - - for previous_widget_id in previous_widget_ids: - if previous_widget_id not in updated_widget_ids: - try: - widget = models.CardXNodeXWidget.objects.get( - pk=previous_widget_id - ) - widget.delete() - except ObjectDoesNotExist: # already deleted - pass - - for previous_card_id in previous_card_ids: - if previous_card_id not in updated_card_ids: - try: - card = models.CardModel.objects.get(pk=previous_card_id) - card.delete() - except ObjectDoesNotExist: # already deleted - pass - - for previous_node_id in previous_node_ids: - if previous_node_id not in updated_node_ids: - try: - node = models.Node.objects.get(pk=previous_node_id) - node.delete() - except ObjectDoesNotExist: # already deleted - pass - - for previous_edge_id in previous_edge_ids: - if previous_edge_id not in updated_edge_ids: - try: - edge = models.Edge.objects.get(pk=previous_edge_id) - edge.delete() - except ObjectDoesNotExist: # already deleted - pass - - for previous_nodegroup_id in previous_nodegroup_ids: - try: - node = models.Node.objects.get(pk=previous_nodegroup_id) - except ( - ObjectDoesNotExist - ): # node has been moved, therefore empty Nodegroup - nodegroup = models.NodeGroup.objects.get(pk=previous_nodegroup_id) - nodegroup.delete() - # END delete superflous models - - # BEGIN save related models - # save order is _very_ important! - for widget in editable_future_graph.widgets.values(): - widget.delete() - for widget in self.widgets.values(): - try: - widget_from_database = models.CardXNodeXWidget.objects.get( - card_id=widget.card_id, - node_id=widget.node_id, - widget_id=widget.widget_id, - ) - widget_from_database.delete() - except models.CardXNodeXWidget.DoesNotExist: - pass - - widget.save() - - for card in editable_future_graph.cards.values(): - card.delete() - for card in self.cards.values(): - card.save() - - for edge in editable_future_graph.edges.values(): - edge.delete() - for edge in self.edges.values(): - edge.save() - - for node in editable_future_graph.nodes.values(): - node.delete() - for node in self.nodes.values(): - node.save() - # END save related models - - self.save(validate=False) - - # This ensures essential objects that have been re-assigned to the `source_graph` - # are NOT deleted via waterfall deletion when the `editable_future_graph` is deleted. - editable_future_graph.cards = {} - editable_future_graph.nodes = {} - editable_future_graph.edges = {} - editable_future_graph.widgets = {} - - editable_future_graph.delete() - - graph_from_database = type(self).objects.get( - pk=self.pk - ) # returns an updated copy of self - graph_from_database.create_editable_future_graph() - - return graph_from_database + return self.restore_state_from_serialized_graph( + serialized_editable_future_graph + ) def revert(self): """ @@ -2828,99 +2588,103 @@ def restore_state_from_serialized_graph(self, serialized_graph): Restores a Graph's state from a serialized graph, and creates a new editable_future_graph """ - models.NodeGroup.objects.filter( - pk__in=[ - nodegroup.pk - for nodegroup in self.get_nodegroups(force_recalculation=True) - ] - ).delete() - models.Node.objects.filter( - pk__in=[node.pk for node in self.nodes.values()] - ).delete() - models.Edge.objects.filter( - pk__in=[edge.pk for edge in self.edges.values()] - ).delete() - models.CardModel.objects.filter( - pk__in=[card.pk for card in self.cards.values()] - ).delete() - models.CardXNodeXWidget.objects.filter( - pk__in=[ - card_x_node_x_widget.pk - for card_x_node_x_widget in self.widgets.values() - ] - ).delete() + with transaction.atomic(): + editable_future_graph = Graph.objects.filter( + source_identifier_id=self.pk + ).first() + if editable_future_graph: + editable_future_graph.delete() - for serialized_nodegroup in serialized_graph["nodegroups"]: - for key, value in serialized_nodegroup.items(): - try: - serialized_nodegroup[key] = uuid.UUID(value) - except: - pass + # ensures any resources that were related to the source graph are not deleted + self.pk = uuid.uuid4() + self.delete() - nodegroup = models.NodeGroup(**serialized_nodegroup) - nodegroup.save() + for serialized_nodegroup in serialized_graph["nodegroups"]: + for key, value in serialized_nodegroup.items(): + try: + serialized_nodegroup[key] = uuid.UUID(value) + except: + pass - for serialized_node in serialized_graph["nodes"]: - for key, value in serialized_node.items(): - try: - serialized_node[key] = uuid.UUID(value) - except: - pass + nodegroup = models.NodeGroup(**serialized_nodegroup) + nodegroup.save() - del serialized_node["is_collector"] - del serialized_node["parentproperty"] + for serialized_node in serialized_graph["nodes"]: + for key, value in serialized_node.items(): + try: + serialized_node[key] = uuid.UUID(value) + except: + pass - node = models.Node(**serialized_node) - node.save() + del serialized_node["is_collector"] + del serialized_node["parentproperty"] - for serialized_edge in serialized_graph["edges"]: - for key, value in serialized_edge.items(): - try: - serialized_edge[key] = uuid.UUID(value) - except: - pass + node = models.Node(**serialized_node) + node.save() - edge = models.Edge(**serialized_edge) - edge.save() + for serialized_edge in serialized_graph["edges"]: + for key, value in serialized_edge.items(): + try: + serialized_edge[key] = uuid.UUID(value) + except: + pass - for serialized_card in serialized_graph["cards"]: - for key, value in serialized_card.items(): - try: - serialized_card[key] = uuid.UUID(value) - except: - pass + edge = models.Edge(**serialized_edge) + edge.save() - del serialized_card["constraints"] + for serialized_card in serialized_graph["cards"]: + for key, value in serialized_card.items(): + try: + serialized_card[key] = uuid.UUID(value) + except: + pass - if "is_editable" in serialized_card: - del serialized_card["is_editable"] + del serialized_card["constraints"] - card = Card(**serialized_card) - card.save() + if "is_editable" in serialized_card: + del serialized_card["is_editable"] - widget_dict = {} - for serialized_widget in serialized_graph.get( - "widgets", serialized_graph.get("cards_x_nodes_x_widgets") - ): - for key, value in serialized_widget.items(): - try: - serialized_widget[key] = uuid.UUID(value) - except: - pass + card = Card(**serialized_card) + card.save() + + widget_dict = {} + for serialized_widget in serialized_graph.get( + "widgets", serialized_graph.get("cards_x_nodes_x_widgets") + ): + for key, value in serialized_widget.items(): + try: + serialized_widget[key] = uuid.UUID(value) + except: + pass + + updated_widget = models.CardXNodeXWidget(**serialized_widget) + updated_widget.save() - updated_widget = models.CardXNodeXWidget(**serialized_widget) - updated_widget.save() + widget_dict[updated_widget.pk] = updated_widget - widget_dict[updated_widget.pk] = updated_widget + updated_graph = Graph(serialized_graph) + updated_graph.widgets = widget_dict + updated_graph.is_active = self.is_active - updated_graph = Graph(serialized_graph) - updated_graph.widgets = widget_dict - updated_graph.is_active = self.is_active + updated_graph.update_permissions(serialized_graph) + + relatable_resource_model_nodes = models.Node.objects.filter( + graph_id__in=serialized_graph["relatable_resource_model_ids"], + istopnode=True, + ) + updated_graph.root.set_relatable_resources( + list( + { + node.source_identifier.pk if node.source_identifier else node.pk + for node in relatable_resource_model_nodes + } + ) + ) - updated_graph.save() - updated_graph.create_editable_future_graph() + updated_graph.save() + updated_graph.create_editable_future_graph() - return Graph.objects.get(pk=updated_graph.pk) + return Graph.objects.get(pk=updated_graph.pk) def publish(self, user=None, notes=None): """ diff --git a/arches/app/models/migrations/11570_harden_editble_future_graphs.py b/arches/app/models/migrations/11570_harden_editble_future_graphs.py new file mode 100644 index 00000000000..2c1b8b7945c --- /dev/null +++ b/arches/app/models/migrations/11570_harden_editble_future_graphs.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.2 on 2024-11-13 18:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "10437_node_alias_not_null"), + ] + + operations = [ + migrations.AlterField( + model_name="resource2resourceconstraint", + name="resourceclassfrom", + field=models.ForeignKey( + blank=True, + db_column="resourceclassfrom", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="resxres_contstraint_classes_from", + to="models.node", + ), + ), + migrations.AlterField( + model_name="resource2resourceconstraint", + name="resourceclassto", + field=models.ForeignKey( + blank=True, + db_column="resourceclassto", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="resxres_contstraint_classes_to", + to="models.node", + ), + ), + ] diff --git a/arches/app/models/models.py b/arches/app/models/models.py index dcd7004292c..bd5217d1bf7 100644 --- a/arches/app/models/models.py +++ b/arches/app/models/models.py @@ -855,37 +855,63 @@ def is_collector(self): ) def get_relatable_resources(self): - return [ - ( - constraint.resourceclassto - if constraint.resourceclassfrom_id == self.pk - else constraint.resourceclassfrom - ) - for constraint in ( - self.resxres_contstraint_classes_from.filter( - resourceclassto__isnull=False + query_id = ( + self.source_identifier_id if self.source_identifier_id else self.nodeid + ) + + constraints = Resource2ResourceConstraint.objects.filter( + Q(resourceclassto_id=query_id) | Q(resourceclassfrom_id=query_id) + ).select_related("resourceclassfrom", "resourceclassto") + + filtered_constraints = set() + for r2r in constraints: + if r2r.resourceclassto_id == query_id and r2r.resourceclassfrom is not None: + filtered_constraints.add(r2r.resourceclassfrom) + elif ( + r2r.resourceclassfrom_id == query_id and r2r.resourceclassto is not None + ): + filtered_constraints.add(r2r.resourceclassto) + + return list(filtered_constraints) + + def set_relatable_resources(self, new_ids): + new_ids = set(new_ids) + + old_ids = set() + for res in self.get_relatable_resources(): + if res.source_identifier_id is not None: + old_ids.add(res.source_identifier_id) + if res.nodeid is not None: + old_ids.add(res.nodeid) + + self_ids = set( + id for id in (self.source_identifier_id, self.nodeid) if id is not None + ) + + ids_to_delete = old_ids - new_ids + ids_to_create = new_ids - old_ids + + if ids_to_delete and self_ids: + Resource2ResourceConstraint.objects.filter( + ( + Q(resourceclassto_id__in=self_ids) + & Q(resourceclassfrom_id__in=ids_to_delete) ) - | self.resxres_contstraint_classes_to.filter( - resourceclassfrom__isnull=False + | ( + Q(resourceclassto_id__in=ids_to_delete) + & Q(resourceclassfrom_id__in=self_ids) ) - ) - ] + ).delete() - def set_relatable_resources(self, new_ids): - old_ids = [res.nodeid for res in self.get_relatable_resources()] - for old_id in old_ids: - if old_id not in new_ids: - Resource2ResourceConstraint.objects.filter( - Q(resourceclassto_id=self.nodeid) - | Q(resourceclassfrom_id=self.nodeid), - Q(resourceclassto_id=old_id) | Q(resourceclassfrom_id=old_id), - ).delete() - for new_id in new_ids: - if new_id not in old_ids: - new_r2r = Resource2ResourceConstraint.objects.create( - resourceclassfrom_id=self.nodeid, resourceclassto_id=new_id + if ids_to_create: + new_constraints = [ + Resource2ResourceConstraint( + resourceclassfrom_id=self.source_identifier_id or self.nodeid, + resourceclassto_id=id_to_create, ) - new_r2r.save() + for id_to_create in ids_to_create + ] + Resource2ResourceConstraint.objects.bulk_create(new_constraints) def serialize(self, fields=None, exclude=None, **kwargs): ret = JSONSerializer().handle_model( @@ -1110,7 +1136,7 @@ class Resource2ResourceConstraint(models.Model): blank=True, null=True, related_name="resxres_contstraint_classes_from", - on_delete=models.SET_NULL, + on_delete=models.CASCADE, ) resourceclassto = models.ForeignKey( Node, @@ -1118,7 +1144,7 @@ class Resource2ResourceConstraint(models.Model): blank=True, null=True, related_name="resxres_contstraint_classes_to", - on_delete=models.SET_NULL, + on_delete=models.CASCADE, ) def __init__(self, *args, **kwargs): diff --git a/arches/app/views/graph.py b/arches/app/views/graph.py index bd5b1b7719c..417d6fbb2ca 100644 --- a/arches/app/views/graph.py +++ b/arches/app/views/graph.py @@ -685,12 +685,13 @@ def post(self, request, graphid): try: data = JSONDeserializer().deserialize(request.body) - source_graph.update_from_editable_future_graph() - source_graph.publish(notes=data.get("notes"), user=request.user) + updated_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + updated_graph.publish(notes=data.get("notes"), user=request.user) return JSONResponse( { - "graph": editable_future_graph, "title": _("Success!"), "message": _( "The graph has been updated. Please click the OK button to reload the page." @@ -709,7 +710,6 @@ def post(self, request, graphid): source_graph.revert() return JSONResponse( { - "graph": editable_future_graph, "title": _("Success!"), "message": _( "The graph has been reverted. Please click the OK button to reload the page." @@ -734,7 +734,6 @@ def post(self, request, graphid): return JSONResponse( { - "graph": source_graph, "title": _("Success!"), "message": _( "The published graphs have been successfully updated." @@ -756,7 +755,6 @@ def post(self, request, graphid): return JSONResponse( { - "graph": source_graph, "title": _("Success!"), "message": _("The graph has been successfully restored."), } diff --git a/tests/models/graph_tests.py b/tests/models/graph_tests.py index d2ff30cb5cd..a0f8cc35c5e 100644 --- a/tests/models/graph_tests.py +++ b/tests/models/graph_tests.py @@ -19,6 +19,7 @@ import uuid from django.contrib.auth.models import User +from guardian.models import GroupObjectPermission, UserObjectPermission from tests.base_test import ArchesTestCase from arches.app.models import models from arches.app.models.graph import Graph, GraphValidationError @@ -1208,217 +1209,6 @@ def test_appending_a_branch_with_an_invalid_ontology_class(self): with self.assertRaises(GraphValidationError) as cm: graph.save() - def test_update_empty_graph_from_editable_future_graph(self): - source_graph = Graph.new(name="TEST RESOURCE") - editable_future_graph = ( - source_graph.create_editable_future_graph() - ) # TODO: replace with db lookup after 9114 signal work - - editable_future_graph.append_branch( - "http://www.cidoc-crm.org/cidoc-crm/E1_Entity", graphid=source_graph.pk - ) - editable_future_graph.save() - serialized_editable_future_graph = JSONDeserializer().deserialize( - JSONSerializer().serialize(editable_future_graph) - ) - - updated_source_graph = source_graph.update_from_editable_future_graph() - serialized_updated_source_graph = JSONDeserializer().deserialize( - JSONSerializer().serialize(updated_source_graph) - ) - - for idx, editable_future_graph_serialized_card in enumerate( - serialized_editable_future_graph["cards"] - ): - updated_source_graph_serialized_card = serialized_updated_source_graph[ - "cards" - ][idx] - - # ensures all relevant values are equal between graphs - for key, value in editable_future_graph_serialized_card.items(): - if key not in [ - "graph_id", - "nodegroup_id", - "name", - "cardid", - "source_identifier_id", - ]: - if type(value) == "dict": - self.assertDictEqual( - value, updated_source_graph_serialized_card[key] - ) - else: - updated_value = updated_source_graph_serialized_card[key] - if ( - updated_value == '{"en": ""}' - ): # workaround for updated str default values - updated_value = "" - - self.assertEqual(value, updated_value) - - # ensures all superflous values relating to `editable_future_graph` have been deleted - try: - future_card_from_database = models.CardModel.objects.get( - pk=editable_future_graph_serialized_card["cardid"] - ) - self.assertEqual( - str(future_card_from_database.graph_id), - updated_source_graph_serialized_card["graph_id"], - ) - except models.CardModel.DoesNotExist: - pass # card has been successfully deleted - - for idx, editable_future_graph_serialized_node in enumerate( - serialized_editable_future_graph["nodes"] - ): - updated_source_graph_serialized_node = serialized_updated_source_graph[ - "nodes" - ][idx] - - # ensures all relevant values are equal between graphs - for key, value in editable_future_graph_serialized_node.items(): - if key not in [ - "graph_id", - "nodegroup_id", - "nodeid", - "source_identifier_id", - ]: - if type(value) == "dict": - self.assertDictEqual( - value, updated_source_graph_serialized_node[key] - ) - else: - updated_value = updated_source_graph_serialized_node[key] - if ( - updated_value == '{"en": ""}' - ): # workaround for updated str default values - updated_value = "" - - self.assertEqual(value, updated_value) - - # ensures all superflous values relating to `editable_future_graph` have been deleted - try: - future_node_from_database = models.Node.objects.get( - pk=editable_future_graph_serialized_node["nodeid"] - ) - self.assertEqual( - str(future_node_from_database.graph_id), - updated_source_graph_serialized_node["graph_id"], - ) - except models.Node.DoesNotExist: - pass # node has been successfully deleted - - for idx, editable_future_graph_serialized_edge in enumerate( - serialized_editable_future_graph["edges"] - ): - updated_source_graph_serialized_edge = serialized_updated_source_graph[ - "edges" - ][idx] - - # ensures all relevant values are equal between graphs - for key, value in editable_future_graph_serialized_edge.items(): - if key not in [ - "graph_id", - "domainnode_id", - "rangenode_id", - "edgeid", - "source_identifier_id", - ]: - if type(value) == "dict": - self.assertDictEqual( - value, updated_source_graph_serialized_edge[key] - ) - else: - self.assertEqual( - value, updated_source_graph_serialized_edge[key] - ) - - # ensures all superflous values relating to `editable_future_graph` have been deleted - try: - future_edge_from_database = models.Edge.objects.get( - pk=editable_future_graph_serialized_edge["edgeid"] - ) - self.assertEqual( - str(future_edge_from_database.graph_id), - updated_source_graph_serialized_edge["graph_id"], - ) - except models.Edge.DoesNotExist: - pass # edge has been successfully deleted - - for idx, editable_future_graph_serialized_nodegroup in enumerate( - serialized_editable_future_graph["nodegroups"] - ): - updated_source_graph_serialized_nodegroup = serialized_updated_source_graph[ - "nodegroups" - ][idx] - - # ensures all relevant values are equal between graphs - for key, value in editable_future_graph_serialized_nodegroup.items(): - if key not in ["parentnodegroup_id", "nodegroupid", "legacygroupid"]: - if type(value) == "dict": - self.assertDictEqual( - value, updated_source_graph_serialized_nodegroup[key] - ) - else: - self.assertEqual( - value, updated_source_graph_serialized_nodegroup[key] - ) - - for key, value in serialized_editable_future_graph.items(): - if key == "name": - self.assertEqual(value, serialized_updated_source_graph[key]) - elif key not in [ - "graphid", - "cards", - "nodes", - "edges", - "nodegroups", - "functions", - "root", - "widgets", - "resource_instance_lifecycle", - "resource_instance_lifecycle_id", - "source_identifier", - "source_identifier_id", - "publication_id", - ]: - if type(value) == "dict": - self.assertDictEqual(value, serialized_updated_source_graph[key]) - else: - self.assertEqual(value, serialized_updated_source_graph[key]) - - def test_update_graph_from_editable_future_graph_after_node_deletion(self): - source_graph = Graph.new(name="TEST RESOURCE") - - source_graph.append_branch( - "http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", - graphid=self.NODE_NODETYPE_GRAPHID, - ) - source_graph.save() - - self.assertEqual(len(source_graph.nodes), 3) - - editable_future_graph = ( - source_graph.create_editable_future_graph() - ) # TODO: replace with db lookup after 9114 signal work - - child_node = [node for node in editable_future_graph.nodes.values()][ - len(editable_future_graph.nodes.values()) - 1 - ] - child_node_source_identifier = child_node.source_identifier_id - - editable_future_graph.delete_node(child_node) - editable_future_graph = Graph.objects.get( - pk=editable_future_graph.pk - ) # updates from DB to refresh node list - - updated_source_graph = source_graph.update_from_editable_future_graph() - - with self.assertRaises(Exception): - models.Node.objects.get(pk=child_node_source_identifier) - - self.assertEqual(len(updated_source_graph.nodes), 2) - def test_add_resource_instance_lifecycle(self): resource_instance_lifecycle = { "id": "f7a0fd46-4c71-49cb-ae1e-778c96763440", @@ -1481,3 +1271,1058 @@ def test_add_resource_instance_lifecycle(self): # Verify the lifecycle contains the states self.assertIn(state1, resource_instance_lifecycle_states) self.assertIn(state2, resource_instance_lifecycle_states) + + +class EditableFutureGraphTests(ArchesTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.NODE_NODETYPE_GRAPHID = "22000000-0000-0000-0000-000000000001" + cls.SINGLE_NODE_GRAPHID = "22000000-0000-0000-0000-000000000000" + + # Node Branch + graph_dict = { + "author": "Arches", + "color": None, + "deploymentdate": None, + "deploymentfile": None, + "description": "Represents a single node in a graph", + "graphid": cls.SINGLE_NODE_GRAPHID, + "iconclass": "fa fa-circle", + "isresource": False, + "name": "Node", + "ontology_id": "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd", + "subtitle": "Represents a single node in a graph.", + "version": "v1", + } + models.GraphModel.objects.create(**graph_dict).save() + + node_dict = { + "config": None, + "datatype": "semantic", + "description": "Represents a single node in a graph", + "graph_id": cls.SINGLE_NODE_GRAPHID, + "isrequired": False, + "issearchable": True, + "istopnode": True, + "name": "Node", + "nodegroup_id": None, + "nodeid": "20000000-0000-0000-0000-100000000000", + "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity", + } + models.Node.objects.create(**node_dict).save() + + # Node/Node Type Branch + graph_dict = { + "author": "Arches", + "color": None, + "deploymentdate": None, + "deploymentfile": None, + "description": "Represents a node and node type pairing", + "graphid": cls.NODE_NODETYPE_GRAPHID, + "iconclass": "fa fa-angle-double-down", + "isresource": False, + "name": "Node/Node Type", + "ontology_id": "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd", + "subtitle": "Represents a node and node type pairing", + "version": "v1", + } + models.GraphModel.objects.create(**graph_dict).save() + + nodegroup_dict = { + "cardinality": "n", + "legacygroupid": "", + "nodegroupid": "20000000-0000-0000-0000-100000000001", + "parentnodegroup_id": None, + } + models.NodeGroup.objects.create(**nodegroup_dict).save() + + card_dict = { + "active": True, + "cardid": "bf9ea150-3eaa-11e8-8b2b-c3a348661f61", + "description": "Represents a node and node type pairing", + "graph_id": cls.NODE_NODETYPE_GRAPHID, + "helpenabled": False, + "helptext": None, + "helptitle": None, + "instructions": "", + "name": "Node/Node Type", + "nodegroup_id": "20000000-0000-0000-0000-100000000001", + "sortorder": None, + "visible": True, + } + models.CardModel.objects.create(**card_dict).save() + + nodes = [ + { + "config": None, + "datatype": "string", + "description": "", + "graph_id": cls.NODE_NODETYPE_GRAPHID, + "isrequired": False, + "issearchable": True, + "istopnode": True, + "name": "Node", + "nodegroup_id": "20000000-0000-0000-0000-100000000001", + "nodeid": "20000000-0000-0000-0000-100000000001", + "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity", + }, + { + "config": {"rdmCollection": None}, + "datatype": "concept", + "description": "", + "graph_id": cls.NODE_NODETYPE_GRAPHID, + "isrequired": False, + "issearchable": True, + "istopnode": False, + "name": "Node Type", + "nodegroup_id": "20000000-0000-0000-0000-100000000001", + "nodeid": "20000000-0000-0000-0000-100000000002", + "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E55_Type", + }, + ] + + for node in nodes: + models.Node.objects.create(**node).save() + + edges_dict = { + "description": None, + "domainnode_id": "20000000-0000-0000-0000-100000000001", + "edgeid": "22200000-0000-0000-0000-000000000001", + "graph_id": cls.NODE_NODETYPE_GRAPHID, + "name": None, + "ontologyproperty": "http://www.cidoc-crm.org/cidoc-crm/P2_has_type", + "rangenode_id": "20000000-0000-0000-0000-100000000002", + } + models.Edge.objects.create(**edges_dict).save() + + graph = Graph.new() + graph.name = "TEST GRAPH" + graph.subtitle = "ARCHES TEST GRAPH" + graph.author = "Arches" + graph.description = "ARCHES TEST GRAPH" + graph.ontology_id = "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd" + graph.version = "v1.0.0" + graph.iconclass = "fa fa-building" + graph.nodegroups = [] + graph.root.ontologyclass = "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity" + graph.save() + + graph.root.name = "ROOT NODE" + graph.root.description = "Test Root Node" + graph.root.datatype = "semantic" + graph.root.save() + + cls.source_graph = graph + cls.editable_future_graph = graph.create_editable_future_graph() + + cls.rootNode = graph.root + + def _compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + self, serialized_updated_source_graph, serialized_editable_future_graph + ): + def are_dicts_equal(dict_1, dict_2, ignore_keys): + def filter_and_sort(entity): + """Recursively filter out ignored keys from a entity and sort by key.""" + if isinstance(entity, dict): + return { + key: filter_and_sort(value) + for key, value in entity.items() + if key not in ignore_keys + } + elif isinstance(entity, list): + return [filter_and_sort(item) for item in entity] + else: + return entity + + dict_1_filtered = filter_and_sort(dict_1) + dict_2_filtered = filter_and_sort(dict_2) + + return dict_1_filtered == dict_2_filtered + + serialized_updated_source_nodes = { + ( + node["source_identifier_id"] + if node.get("source_identifier_id") not in [None, "None"] + else node["nodeid"] + ): node + for node in serialized_updated_source_graph["nodes"] + } + + serialized_editable_future_nodes = { + ( + node["source_identifier_id"] + if node.get("source_identifier_id") not in [None, "None"] + else node["nodeid"] + ): node + for node in serialized_editable_future_graph["nodes"] + } + + self.assertTrue( + are_dicts_equal( + serialized_updated_source_nodes, + serialized_editable_future_nodes, + [ + "graph_id", + "nodeid", + "nodegroup_id", + "source_identifier_id", + ], + ) + ) + + serialized_updated_source_edges = { + ( + edge["source_identifier_id"] + if edge.get("source_identifier_id") not in [None, "None"] + else edge["edgeid"] + ): edge + for edge in serialized_updated_source_graph["edges"] + } + + serialized_editable_future_edges = { + ( + edge["source_identifier_id"] + if edge.get("source_identifier_id") not in [None, "None"] + else edge["edgeid"] + ): edge + for edge in serialized_editable_future_graph["edges"] + } + + self.assertTrue( + are_dicts_equal( + serialized_updated_source_edges, + serialized_editable_future_edges, + [ + "graph_id", + "edgeid", + "domainnode_id", + "rangenode_id", + "source_identifier_id", + ], + ) + ) + + serialized_updated_source_cards = { + ( + card["source_identifier_id"] + if card.get("source_identifier_id") not in [None, "None"] + else card["cardid"] + ): card + for card in serialized_updated_source_graph["cards"] + } + + serialized_editable_future_cards = { + ( + card["source_identifier_id"] + if card.get("source_identifier_id") not in [None, "None"] + else card["cardid"] + ): card + for card in serialized_editable_future_graph["cards"] + } + + self.assertTrue( + are_dicts_equal( + serialized_updated_source_cards, + serialized_editable_future_cards, + [ + "graph_id", + "cardid", + "nodegroup_id", + "source_identifier_id", + ], + ) + ) + + serialized_updated_source_cards_x_nodes_x_widgets = { + ( + card_x_node_x_widget["source_identifier_id"] + if card_x_node_x_widget.get("source_identifier_id") + not in [None, "None"] + else card_x_node_x_widget["id"] + ): card_x_node_x_widget + for card_x_node_x_widget in serialized_updated_source_graph[ + "cards_x_nodes_x_widgets" + ] + } + + serialized_editable_future_cards_x_nodes_x_widgets = { + ( + card_x_node_x_widget["source_identifier_id"] + if card_x_node_x_widget.get("source_identifier_id") + not in [None, "None"] + else card_x_node_x_widget["id"] + ): card_x_node_x_widget + for card_x_node_x_widget in serialized_editable_future_graph[ + "cards_x_nodes_x_widgets" + ] + } + + self.assertTrue( + are_dicts_equal( + serialized_updated_source_cards_x_nodes_x_widgets, + serialized_editable_future_cards_x_nodes_x_widgets, + [ + "graph_id", + "id", + "card_id", + "node_id", + "source_identifier_id", + ], + ) + ) + + self.assertTrue( + are_dicts_equal( + serialized_updated_source_graph, + serialized_editable_future_graph, + [ + "graphid", + "cards", + "nodes", + "edges", + "nodegroups", + "functions", + "root", + "widgets", + "cards_x_nodes_x_widgets", + "resource_instance_lifecycle", + "resource_instance_lifecycle_id", + "source_identifier", + "source_identifier_id", + "publication_id", + ], + ) + ) + + def test_update_empty_graph_from_editable_future_graph(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + def test_update_graph_with_multiple_nodes_and_edges(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.SINGLE_NODE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + + serialized_updated_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_updated_editable_future_graph + ) + + def test_update_graph_with_permissions(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + + nodegroup = updated_source_graph.get_nodegroups()[:1][0] + + GroupObjectPermission.objects.create( + group_id=1, content_object=nodegroup, permission_id=93 + ) + UserObjectPermission.objects.create( + user_id=2, content_object=nodegroup, permission_id=94 + ) + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + def test_update_graph_with_relatable_resources(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.root.set_relatable_resources([source_graph.root.pk]) + editable_future_graph.root.save() + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + self.assertTrue(len(updated_source_graph.root.get_relatable_resources())) + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + def test_create_editable_future_graphs_does_not_pollute_database(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + updated_source_graph.create_editable_future_graph() + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_deleting_source_graph_deletes_editable_future_graph_and_all_related_models( + self, + ): + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + resource_2_resource_constraints_count_before = ( + models.Resource2ResourceConstraint.objects.count() + ) + + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + + updated_source_graph.delete() + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + resource_2_resource_constraints_count_after = ( + models.Resource2ResourceConstraint.objects.count() + ) + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + self.assertEqual( + resource_2_resource_constraints_count_before, + resource_2_resource_constraints_count_after, + ) + + def test_revert_editable_future_graph(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + source_graph.revert() + + editable_future_graph = models.Graph.objects.get( + source_identifier_id=source_graph.pk + ) + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_update_nodegroup(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + nodegroup = editable_future_graph.get_nodegroups()[:1][0] + nodegroup.cardinality = "1" + nodegroup.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + nodegroup = updated_source_graph.get_nodegroups()[:1][0] + self.assertEqual(nodegroup.cardinality, "1") + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_update_node(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + editable_future_graph.root.name = "UPDATED_NODE_NAME" + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + self.assertEqual(updated_source_graph.root.name, "UPDATED_NODE_NAME") + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_update_card(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + card = [card for card in editable_future_graph.cards.values()][0] + card.description = "UPDATED_CARD_DESCRIPTION" + card.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + updated_card = [card for card in updated_source_graph.cards.values()][0] + self.assertEqual( + updated_card.description.value, '{"en": "UPDATED_CARD_DESCRIPTION"}' + ) + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_update_widget(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + card = [card for card in editable_future_graph.cards.values()][0] + card_x_node_x_widget = models.CardXNodeXWidget.objects.create( + card=card, + node_id=card.nodegroup_id, + widget=models.Widget.objects.first(), + label="Widget name", + ) + + editable_future_graph.widgets[card_x_node_x_widget.pk] = card_x_node_x_widget + + editable_future_graph.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + nodegroup_count_before = models.NodeGroup.objects.count() + node_count_before = models.Node.objects.count() + edge_count_before = models.Edge.objects.count() + card_count_before = models.CardModel.objects.count() + card_x_node_x_widget_count_before = models.CardXNodeXWidget.objects.count() + + widget = [widget for widget in editable_future_graph.widgets.values()][0] + widget.label = "UPDATED_WIDGET_NAME" + widget.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + updated_widget = [widget for widget in editable_future_graph.widgets.values()][ + 0 + ] + self.assertEqual(updated_widget.label.value, '{"en": "UPDATED_WIDGET_NAME"}') + + nodegroup_count_after = models.NodeGroup.objects.count() + node_count_after = models.Node.objects.count() + edge_count_after = models.Edge.objects.count() + card_count_after = models.CardModel.objects.count() + card_x_node_x_widget_count_after = models.CardXNodeXWidget.objects.count() + + self.assertEqual(nodegroup_count_before, nodegroup_count_after) + self.assertEqual(node_count_before, node_count_after) + self.assertEqual(edge_count_before, edge_count_after) + self.assertEqual(card_count_before, card_count_after) + self.assertEqual( + card_x_node_x_widget_count_before, card_x_node_x_widget_count_after + ) + + def test_update_from_editable_future_graph_does_not_affect_resources(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + nodegroup = models.NodeGroup.objects.create() + string_node = models.Node.objects.create( + graph=source_graph, + nodegroup=nodegroup, + name="String Node", + datatype="string", + istopnode=False, + ) + resource_instance_node = models.Node.objects.create( + graph=source_graph, + nodegroup=nodegroup, + name="Resource Node", + datatype="resource-instance", + istopnode=False, + ) + + resource = models.ResourceInstance.objects.create(graph=source_graph) + tile = models.TileModel.objects.create( + nodegroup_id=nodegroup.pk, + resourceinstance=resource, + data={ + str(string_node.pk): { + "en": {"value": "test value", "direction": "ltr"}, + }, + str(resource_instance_node.pk): { + "resourceId": str(resource.pk), + "ontologyProperty": "", + "inverseOntologyProperty": "", + }, + }, + sortorder=0, + ) + + serialized_resource = JSONDeserializer().deserialize( + JSONSerializer().serialize(resource) + ) + serialized_tile = JSONDeserializer().deserialize( + JSONSerializer().serialize(tile) + ) + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + resource_from_database = models.ResourceInstance.objects.get(pk=resource.pk) + tile_from_database = models.TileModel.objects.get(pk=tile.pk) + + serialized_resource_from_database = JSONDeserializer().deserialize( + JSONSerializer().serialize(resource_from_database) + ) + serialized_tile_from_database = JSONDeserializer().deserialize( + JSONSerializer().serialize(tile_from_database) + ) + + self.assertEqual(serialized_resource, serialized_resource_from_database) + self.assertEqual(serialized_tile, serialized_tile_from_database) + + def test_placing_node_in_separate_card_does_not_pollute_database(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.append_branch( + "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", + graphid=self.NODE_NODETYPE_GRAPHID, + ) + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + node = [node for node in editable_future_graph.nodes.values()][2] + + # fixes flaky test + models.NodeGroup.objects.filter(pk=node.pk).delete() + + nodegroup_count_before = models.NodeGroup.objects.count() + + source_identifier_id = node.source_identifier_id + original_nodegroup_id = node.nodegroup_id + updated_nodegroup_id = node.pk + + models.NodeGroup.objects.create( + **{ + "cardinality": "n", + "legacygroupid": "", + "nodegroupid": str(updated_nodegroup_id), + "parentnodegroup_id": None, + } + ).save() + + node.nodegroup_id = updated_nodegroup_id + node.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + # a source_graph nodegroup and an editable_future_graph nodegroup have been created + self.assertEqual(nodegroup_count_before, models.NodeGroup.objects.count() - 2) + + node = [ + node + for node in editable_future_graph.nodes.values() + if node.source_identifier_id == source_identifier_id + ][0] + node.nodegroup_id = original_nodegroup_id + node.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + # the source_graph nodegroup and the editable_future_graph nodegroup have been deleted + self.assertEqual(nodegroup_count_before, models.NodeGroup.objects.count()) + + def test_can_update_graph_slug(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + # test adding slug + editable_future_graph.slug = "test-resource" + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self.assertEqual(serialized_updated_source_graph["slug"], "test-resource") + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + # test updating slug + editable_future_graph.slug = "test-resource-two" + editable_future_graph.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self.assertEqual(serialized_updated_source_graph["slug"], "test-resource-two") + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + ) + + def test_can_update_other_data_in_graph_with_slug(self): + source_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST") + source_graph.save() + editable_future_graph = source_graph.create_editable_future_graph() + + editable_future_graph.slug = "test-resource" + editable_future_graph.save() + + updated_source_graph = source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + editable_future_graph.name = "TEST RESOURCE TWO" + editable_future_graph.save() + + updated_source_graph = updated_source_graph.update_from_editable_future_graph( + editable_future_graph=editable_future_graph + ) + editable_future_graph = updated_source_graph.create_editable_future_graph() + + serialized_editable_future_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(editable_future_graph) + ) + serialized_updated_source_graph = JSONDeserializer().deserialize( + JSONSerializer().serialize(updated_source_graph) + ) + + self.assertEqual(serialized_updated_source_graph["name"], "TEST RESOURCE TWO") + + self._compare_serialized_updated_source_graph_and_serialized_editable_future_graph( + serialized_updated_source_graph, serialized_editable_future_graph + )