From 1ddd8525c7e977cd340fb3e0612472d326bbc891 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 21 Sep 2018 14:23:47 +0200 Subject: [PATCH] Remove references to node from projects when the node is deleted. Removes node references in project fields header_node, nodes_blog, nodes_featured, nodes_latest. --- pillar/api/nodes/eve_hooks.py | 43 +++++++++++++++++++++++++++- tests/test_api/test_nodes.py | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/pillar/api/nodes/eve_hooks.py b/pillar/api/nodes/eve_hooks.py index d179fa81..a11f434a 100644 --- a/pillar/api/nodes/eve_hooks.py +++ b/pillar/api/nodes/eve_hooks.py @@ -1,14 +1,17 @@ +import collections import functools import logging import urllib.parse + from bson import ObjectId -from flask import current_app from werkzeug import exceptions as wz_exceptions +from pillar import current_app import pillar.markdown from pillar.api.activities import activity_subscribe, activity_object_add from pillar.api.file_storage_backends.gcs import update_file_name from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES +from pillar.api.utils import random_etag from pillar.api.utils.authorization import check_permissions log = logging.getLogger(__name__) @@ -243,6 +246,44 @@ def nodes_set_default_picture(nodes): def before_deleting_node(node: dict): check_permissions('nodes', node, 'DELETE') + remove_project_references(node) + + +def remove_project_references(node): + project_id = node.get('project') + if not project_id: + return + + node_id = node['_id'] + log.info('Removing references to node %s from project %s', node_id, project_id) + + projects_col = current_app.db('projects') + project = projects_col.find_one({'_id': project_id}) + updates = collections.defaultdict(dict) + + if project.get('header_node') == node_id: + updates['$unset']['header_node'] = node_id + + project_reference_lists = ('nodes_blog', 'nodes_featured', 'nodes_latest') + for list_name in project_reference_lists: + references = project.get(list_name) + if not references: + continue + try: + references.remove(node_id) + except ValueError: + continue + + updates['$set'][list_name] = references + + if not updates: + return + + updates['$set']['_etag'] = random_etag() + result = projects_col.update_one({'_id': project_id}, updates) + if result.modified_count != 1: + log.warning('Removing references to node %s from project %s resulted in %d modified documents (expected 1)', + node_id, project_id, result.modified_count) def after_deleting_node(item): diff --git a/tests/test_api/test_nodes.py b/tests/test_api/test_nodes.py index 420f95e9..4acf70e8 100644 --- a/tests/test_api/test_nodes.py +++ b/tests/test_api/test_nodes.py @@ -1,3 +1,4 @@ +import copy import json import typing from unittest import mock @@ -633,3 +634,56 @@ class TaggedNodesTest(AbstractPillarTest): resp = do_query() for node in resp: self.assertNotIn('view_progress', node) + + +class NodesReferencedByProjectTest(AbstractPillarTest): + def setUp(self, **kwargs): + super().setUp(**kwargs) + node = copy.deepcopy(ctd.EXAMPLE_NODE) + self.pid, self.project = self.ensure_project_exists( + project_overrides={'picture_header':None, + 'picture_square': None} + ) + self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token') + + node['project'] = self.pid + self.node_id = self.create_node(node) + self.node_etag = node['_etag'] + + with self.app.app_context(): + self.app.db('projects').update( + {'_id': self.pid}, + {'$set': { + 'header_node': self.node_id, + 'nodes_blog': [self.node_id], + 'nodes_featured': [self.node_id], + 'nodes_latest': [self.node_id], + }} + ) + + def test_delete_node(self): + with self.app.app_context(): + self.delete(f'/api/nodes/{self.node_id}', + auth_token='token', + headers={'If-Match': self.node_etag}, + expected_status=204) + + node_after = self.app.db('nodes').find_one(self.node_id) + self.assertTrue(node_after.get('_deleted')) + + project_after = self.app.db('projects').find_one(self.pid) + self.assertIsNone(project_after.get('header_node')) + self.assertNotEqual(self.project['_etag'], project_after['_etag']) + self.assertNotIn(self.node_id, project_after['nodes_blog']) + self.assertNotIn(self.node_id, project_after['nodes_featured']) + self.assertNotIn(self.node_id, project_after['nodes_latest']) + + # Verifying that the project is still valid + from pillar.api.utils import remove_private_keys + self.put(f'/api/projects/{self.pid}', json=remove_private_keys(project_after), + etag=project_after['_etag'], + auth_token='token') + + + +