"""Unit/Functional tests""" import datetime import os from django.contrib.admin.sites import AdminSite from django.contrib.admin.views.main import ChangeList from django.contrib.auth.models import User, AnonymousUser from django.contrib.messages.storage.fallback import FallbackStorage from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver from django.template import Template, Context from django.test import TestCase from django.test.client import RequestFactory from django.templatetags.static import static from django.contrib.admin.options import TO_FIELD_VAR from django import VERSION as DJANGO_VERSION import pytest from treebeard import numconv from treebeard.admin import admin_factory from treebeard.exceptions import ( InvalidPosition, InvalidMoveToDescendant, PathOverflow, MissingNodeOrderBy, NodeAlreadySaved, ) from treebeard.forms import movenodeform_factory from treebeard.tests import models from treebeard.tests.admin import register_all as admin_register_all admin_register_all() BASE_DATA = [ {"data": {"desc": "1"}}, { "data": {"desc": "2"}, "children": [ {"data": {"desc": "21"}}, {"data": {"desc": "22"}}, { "data": {"desc": "23"}, "children": [ {"data": {"desc": "231"}}, ], }, {"data": {"desc": "24"}}, ], }, {"data": {"desc": "3"}}, { "data": {"desc": "4"}, "children": [ {"data": {"desc": "41"}}, ], }, ] UNCHANGED = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] @pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS) def model(request): request.param.load_bulk(BASE_DATA) return request.param @pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS) def model_without_data(request): return request.param @pytest.fixture(scope="function", params=models.BASE_MODELS) def model_without_proxy(request): request.param.load_bulk(BASE_DATA) return request.param @pytest.fixture(scope="function", params=models.UNICODE_MODELS) def model_with_unicode(request): return request.param @pytest.fixture(scope="function", params=models.SORTED_MODELS) def sorted_model(request): return request.param @pytest.fixture(scope="function", params=models.RELATED_MODELS) def related_model(request): return request.param @pytest.fixture(scope="function", params=models.INHERITED_MODELS) def inherited_model(request): return request.param @pytest.fixture(scope="function", params=models.MP_SHORTPATH_MODELS) def mpshort_model(request): return request.param @pytest.fixture(scope="function", params=[models.MP_TestNodeShortPath]) def mpshortnotsorted_model(request): return request.param @pytest.fixture(scope="function", params=[models.MP_TestNodeAlphabet]) def mpalphabet_model(request): return request.param @pytest.fixture(scope="function", params=[models.MP_TestNodeSortedAutoNow]) def mpsortedautonow_model(request): return request.param @pytest.fixture(scope="function", params=[models.MP_TestNodeSmallStep]) def mpsmallstep_model(request): return request.param @pytest.fixture(scope="function", params=[models.MP_TestManyToManyWithUser]) def mpm2muser_model(request): return request.param # Compat helper, and be dropped after Django 3.2 is dropped def get_changelist_args(*args): new_args = list(args) if DJANGO_VERSION > (4,): new_args.append("") # New search_help_text arg return new_args class TestTreeBase: def got(self, model): if model in [models.NS_TestNode, models.NS_TestNode_Proxy]: # this slows down nested sets tests quite a bit, but it has the # advantage that we'll check the node edges are correct d = {} for tree_id, lft, rgt in model.objects.values_list("tree_id", "lft", "rgt"): d.setdefault(tree_id, []).extend([lft, rgt]) for tree_id, got_edges in d.items(): assert len(got_edges) == max(got_edges) good_edges = list(range(1, len(got_edges) + 1)) assert sorted(got_edges) == good_edges return [ (o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree() ] def _assert_get_annotated_list(self, model, expected, parent=None): results = model.get_annotated_list(parent) got = [ (obj[0].desc, obj[1]["open"], obj[1]["close"], obj[1]["level"]) for obj in results ] assert expected == got assert all([type(obj[0]) == model for obj in results]) @pytest.mark.django_db class TestEmptyTree(TestTreeBase): def test_load_bulk_empty(self, model_without_data): ids = model_without_data.load_bulk(BASE_DATA) got_descs = [obj.desc for obj in model_without_data.objects.filter(pk__in=ids)] expected_descs = [x[0] for x in UNCHANGED] assert sorted(got_descs) == sorted(expected_descs) assert self.got(model_without_data) == UNCHANGED def test_dump_bulk_empty(self, model_without_data): assert model_without_data.dump_bulk() == [] def test_add_root_empty(self, model_without_data): model_without_data.add_root(desc="1") expected = [("1", 1, 0)] assert self.got(model_without_data) == expected def test_get_root_nodes_empty(self, model_without_data): got = model_without_data.get_root_nodes() expected = [] assert [node.desc for node in got] == expected def test_get_first_root_node_empty(self, model_without_data): got = model_without_data.get_first_root_node() assert got is None def test_get_last_root_node_empty(self, model_without_data): got = model_without_data.get_last_root_node() assert got is None def test_get_tree(self, model_without_data): got = list(model_without_data.get_tree()) assert got == [] def test_get_annotated_list(self, model_without_data): expected = [] self._assert_get_annotated_list(model_without_data, expected) def test_add_multiple_root_nodes_adds_sibling_leaves(self, model_without_data): model_without_data.add_root(desc="1") model_without_data.add_root(desc="2") model_without_data.add_root(desc="3") model_without_data.add_root(desc="4") # these are all sibling root nodes (depth=1), and leaf nodes (children=0) expected = [("1", 1, 0), ("2", 1, 0), ("3", 1, 0), ("4", 1, 0)] assert self.got(model_without_data) == expected class TestNonEmptyTree(TestTreeBase): pass @pytest.mark.django_db class TestClassMethods(TestNonEmptyTree): def test_load_bulk_existing(self, model): # inserting on an existing node node = model.objects.get(desc="231") ids = model.load_bulk(BASE_DATA, node) expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 4), ("1", 4, 0), ("2", 4, 4), ("21", 5, 0), ("22", 5, 0), ("23", 5, 1), ("231", 6, 0), ("24", 5, 0), ("3", 4, 0), ("4", 4, 1), ("41", 5, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] expected_descs = ["1", "2", "21", "22", "23", "231", "24", "3", "4", "41"] got_descs = [obj.desc for obj in model.objects.filter(pk__in=ids)] assert sorted(got_descs) == sorted(expected_descs) assert self.got(model) == expected def test_get_tree_all(self, model): nodes = model.get_tree() got = [(o.desc, o.get_depth(), o.get_children_count()) for o in nodes] assert got == UNCHANGED assert all([type(o) == model for o in nodes]) def test_dump_bulk_all(self, model): assert model.dump_bulk(keep_ids=False) == BASE_DATA def test_get_tree_node(self, model): node = model.objects.get(desc="231") model.load_bulk(BASE_DATA, node) # the tree was modified by load_bulk, so we reload our node object node = model.objects.get(pk=node.pk) nodes = model.get_tree(node) got = [(o.desc, o.get_depth(), o.get_children_count()) for o in nodes] expected = [ ("231", 3, 4), ("1", 4, 0), ("2", 4, 4), ("21", 5, 0), ("22", 5, 0), ("23", 5, 1), ("231", 6, 0), ("24", 5, 0), ("3", 4, 0), ("4", 4, 1), ("41", 5, 0), ] assert got == expected assert all([type(o) == model for o in nodes]) def test_get_tree_leaf(self, model): node = model.objects.get(desc="1") assert 0 == node.get_children_count() nodes = model.get_tree(node) got = [(o.desc, o.get_depth(), o.get_children_count()) for o in nodes] expected = [("1", 1, 0)] assert got == expected assert all([type(o) == model for o in nodes]) def test_get_annotated_list_all(self, model): expected = [ ("1", True, [], 0), ("2", False, [], 0), ("21", True, [], 1), ("22", False, [], 1), ("23", False, [], 1), ("231", True, [0], 2), ("24", False, [0], 1), ("3", False, [], 0), ("4", False, [], 0), ("41", True, [0, 1], 1), ] self._assert_get_annotated_list(model, expected) def test_get_annotated_list_node(self, model): node = model.objects.get(desc="2") expected = [ ("2", True, [], 0), ("21", True, [], 1), ("22", False, [], 1), ("23", False, [], 1), ("231", True, [0], 2), ("24", False, [0, 1], 1), ] self._assert_get_annotated_list(model, expected, node) def test_get_annotated_list_leaf(self, model): node = model.objects.get(desc="1") expected = [("1", True, [0], 0)] self._assert_get_annotated_list(model, expected, node) def test_dump_bulk_node(self, model): node = model.objects.get(desc="231") model.load_bulk(BASE_DATA, node) # the tree was modified by load_bulk, so we reload our node object node = model.objects.get(pk=node.pk) got = model.dump_bulk(node, False) expected = [{"data": {"desc": "231"}, "children": BASE_DATA}] assert got == expected def test_load_and_dump_bulk_keeping_ids(self, model): exp = model.dump_bulk(keep_ids=True) model.objects.all().delete() model.load_bulk(exp, None, True) got = model.dump_bulk(keep_ids=True) assert got == exp # do we really have an unchanged tree after the dump/delete/load? got = [ (o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree() ] assert got == UNCHANGED def test_load_and_dump_bulk_with_fk(self, related_model): # https://bitbucket.org/tabo/django-treebeard/issue/48/ related_model.objects.all().delete() related, created = models.RelatedModel.objects.get_or_create( desc="Test %s" % related_model.__name__ ) related_data = [ {"data": {"desc": "1", "related": related.pk}}, { "data": {"desc": "2", "related": related.pk}, "children": [ {"data": {"desc": "21", "related": related.pk}}, {"data": {"desc": "22", "related": related.pk}}, { "data": {"desc": "23", "related": related.pk}, "children": [ {"data": {"desc": "231", "related": related.pk}}, ], }, {"data": {"desc": "24", "related": related.pk}}, ], }, {"data": {"desc": "3", "related": related.pk}}, { "data": {"desc": "4", "related": related.pk}, "children": [ {"data": {"desc": "41", "related": related.pk}}, ], }, ] related_model.load_bulk(related_data) got = related_model.dump_bulk(keep_ids=False) assert got == related_data def test_get_root_nodes(self, model): got = model.get_root_nodes() expected = ["1", "2", "3", "4"] assert [node.desc for node in got] == expected assert all([type(node) == model for node in got]) def test_get_first_root_node(self, model): got = model.get_first_root_node() assert got.desc == "1" assert type(got) == model def test_get_last_root_node(self, model): got = model.get_last_root_node() assert got.desc == "4" assert type(got) == model def test_add_root(self, model): obj = model.add_root(desc="5") assert obj.get_depth() == 1 got = model.get_last_root_node() assert got.desc == "5" assert type(got) == model def test_add_root_with_passed_instance(self, model): obj = model(desc="5") result = model.add_root(instance=obj) assert result == obj got = model.get_last_root_node() assert got.desc == "5" assert type(got) == model def test_add_root_with_already_saved_instance(self, model): obj = model.objects.get(desc="4") with pytest.raises(NodeAlreadySaved): model.add_root(instance=obj) @pytest.mark.django_db class TestSimpleNodeMethods(TestNonEmptyTree): def test_is_root(self, model): data = [ ("2", True), ("1", True), ("4", True), ("21", False), ("24", False), ("22", False), ("231", False), ] for desc, expected in data: got = model.objects.get(desc=desc).is_root() assert got == expected def test_is_leaf(self, model): data = [ ("2", False), ("23", False), ("231", True), ] for desc, expected in data: got = model.objects.get(desc=desc).is_leaf() assert got == expected def test_get_root(self, model): data = [ ("2", "2"), ("1", "1"), ("4", "4"), ("21", "2"), ("24", "2"), ("22", "2"), ("231", "2"), ] for desc, expected in data: node = model.objects.get(desc=desc).get_root() assert node.desc == expected assert type(node) == model def test_get_parent(self, model): data = [ ("2", None), ("1", None), ("4", None), ("21", "2"), ("24", "2"), ("22", "2"), ("231", "23"), ] data = dict(data) objs = {} for desc, expected in data.items(): node = model.objects.get(desc=desc) parent = node.get_parent() if expected: assert parent.desc == expected assert type(parent) == model else: assert parent is None objs[desc] = node # corrupt the objects' parent cache node._parent_obj = "CORRUPTED!!!" for desc, expected in data.items(): node = objs[desc] # asking get_parent to not use the parent cache (since we # corrupted it in the previous loop) parent = node.get_parent(True) if expected: assert parent.desc == expected assert type(parent) == model else: assert parent is None def test_get_children(self, model): data = [ ("2", ["21", "22", "23", "24"]), ("23", ["231"]), ("231", []), ] for desc, expected in data: children = model.objects.get(desc=desc).get_children() assert [node.desc for node in children] == expected assert all([type(node) == model for node in children]) def test_get_children_count(self, model): data = [ ("2", 4), ("23", 1), ("231", 0), ] for desc, expected in data: got = model.objects.get(desc=desc).get_children_count() assert got == expected def test_get_siblings(self, model): data = [ ("2", ["1", "2", "3", "4"]), ("21", ["21", "22", "23", "24"]), ("231", ["231"]), ] for desc, expected in data: siblings = model.objects.get(desc=desc).get_siblings() assert [node.desc for node in siblings] == expected assert all([type(node) == model for node in siblings]) def test_get_first_sibling(self, model): data = [ ("2", "1"), ("1", "1"), ("4", "1"), ("21", "21"), ("24", "21"), ("22", "21"), ("231", "231"), ] for desc, expected in data: node = model.objects.get(desc=desc).get_first_sibling() assert node.desc == expected assert type(node) == model def test_get_prev_sibling(self, model): data = [ ("2", "1"), ("1", None), ("4", "3"), ("21", None), ("24", "23"), ("22", "21"), ("231", None), ] for desc, expected in data: node = model.objects.get(desc=desc).get_prev_sibling() if expected is None: assert node is None else: assert node.desc == expected assert type(node) == model def test_get_next_sibling(self, model): data = [ ("2", "3"), ("1", "2"), ("4", None), ("21", "22"), ("24", None), ("22", "23"), ("231", None), ] for desc, expected in data: node = model.objects.get(desc=desc).get_next_sibling() if expected is None: assert node is None else: assert node.desc == expected assert type(node) == model def test_get_last_sibling(self, model): data = [ ("2", "4"), ("1", "4"), ("4", "4"), ("21", "24"), ("24", "24"), ("22", "24"), ("231", "231"), ] for desc, expected in data: node = model.objects.get(desc=desc).get_last_sibling() assert node.desc == expected assert type(node) == model def test_get_first_child(self, model): data = [ ("2", "21"), ("21", None), ("23", "231"), ("231", None), ] for desc, expected in data: node = model.objects.get(desc=desc).get_first_child() if expected is None: assert node is None else: assert node.desc == expected assert type(node) == model def test_get_last_child(self, model): data = [ ("2", "24"), ("21", None), ("23", "231"), ("231", None), ] for desc, expected in data: node = model.objects.get(desc=desc).get_last_child() if expected is None: assert node is None else: assert node.desc == expected assert type(node) == model def test_get_ancestors(self, model): data = [ ("2", []), ("21", ["2"]), ("231", ["2", "23"]), ] for desc, expected in data: nodes = model.objects.get(desc=desc).get_ancestors() assert [node.desc for node in nodes] == expected assert all([type(node) == model for node in nodes]) def test_get_descendants(self, model): data = [ ("2", ["21", "22", "23", "231", "24"]), ("23", ["231"]), ("231", []), ("1", []), ("4", ["41"]), ] for desc, expected in data: nodes = model.objects.get(desc=desc).get_descendants() assert [node.desc for node in nodes] == expected assert all([type(node) == model for node in nodes]) def test_get_descendant_count(self, model): data = [ ("2", 5), ("23", 1), ("231", 0), ("1", 0), ("4", 1), ] for desc, expected in data: got = model.objects.get(desc=desc).get_descendant_count() assert got == expected def test_is_sibling_of(self, model): data = [ ("2", "2", True), ("2", "1", True), ("21", "2", False), ("231", "2", False), ("22", "23", True), ("231", "23", False), ("231", "231", True), ] for desc1, desc2, expected in data: node1 = model.objects.get(desc=desc1) node2 = model.objects.get(desc=desc2) assert node1.is_sibling_of(node2) == expected def test_is_child_of(self, model): data = [ ("2", "2", False), ("2", "1", False), ("21", "2", True), ("231", "2", False), ("231", "23", True), ("231", "231", False), ] for desc1, desc2, expected in data: node1 = model.objects.get(desc=desc1) node2 = model.objects.get(desc=desc2) assert node1.is_child_of(node2) == expected def test_is_descendant_of(self, model): data = [ ("2", "2", False), ("2", "1", False), ("21", "2", True), ("231", "2", True), ("231", "23", True), ("231", "231", False), ] for desc1, desc2, expected in data: node1 = model.objects.get(desc=desc1) node2 = model.objects.get(desc=desc2) assert node1.is_descendant_of(node2) == expected @pytest.mark.django_db class TestAddChild(TestNonEmptyTree): def test_add_child_to_leaf(self, model): model.objects.get(desc="231").add_child(desc="2311") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 1), ("2311", 4, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_child_to_node(self, model): model.objects.get(desc="2").add_child(desc="25") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("25", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_child_with_passed_instance(self, model): child = model(desc="2311") result = model.objects.get(desc="231").add_child(instance=child) assert result == child expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 1), ("2311", 4, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_child_with_already_saved_instance(self, model): child = model.objects.get(desc="21") with pytest.raises(NodeAlreadySaved): model.objects.get(desc="2").add_child(instance=child) def test_add_child_with_pk_set(self, model): """ If the model is using a natural primary key then it will be already set when the instance is inserted. """ child = model(pk=999999, desc="natural key") result = model.objects.get(desc="2").add_child(instance=child) assert result == child def test_add_child_post_save(self, model): try: @receiver(post_save, dispatch_uid="test_add_child_post_save") def on_post_save(instance, **kwargs): parent = instance.get_parent() parent.refresh_from_db() assert parent.get_descendant_count() == 1 # It's important that we're testing a leaf node parent = model.objects.get(desc="231") assert parent.is_leaf() parent.add_child(desc="2311") finally: post_save.disconnect(dispatch_uid="test_add_child_post_save") @pytest.mark.django_db class TestAddSibling(TestNonEmptyTree): def test_add_sibling_invalid_pos(self, model): with pytest.raises(InvalidPosition): model.objects.get(desc="231").add_sibling("invalid_pos") def test_add_sibling_missing_nodeorderby(self, model): node_wchildren = model.objects.get(desc="2") with pytest.raises(MissingNodeOrderBy): node_wchildren.add_sibling("sorted-sibling", desc="aaa") def test_add_sibling_last_root(self, model): node_wchildren = model.objects.get(desc="2") obj = node_wchildren.add_sibling("last-sibling", desc="5") assert obj.get_depth() == 1 assert node_wchildren.get_last_sibling().desc == "5" def test_add_sibling_last(self, model): node = model.objects.get(desc="231") obj = node.add_sibling("last-sibling", desc="232") assert obj.get_depth() == 3 assert node.get_last_sibling().desc == "232" def test_add_sibling_first_root(self, model): node_wchildren = model.objects.get(desc="2") obj = node_wchildren.add_sibling("first-sibling", desc="new") assert obj.get_depth() == 1 expected = [ ("new", 1, 0), ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_first(self, model): node_wchildren = model.objects.get(desc="23") obj = node_wchildren.add_sibling("first-sibling", desc="new") assert obj.get_depth() == 2 expected = [ ("1", 1, 0), ("2", 1, 5), ("new", 2, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_left_root(self, model): node_wchildren = model.objects.get(desc="2") obj = node_wchildren.add_sibling("left", desc="new") assert obj.get_depth() == 1 expected = [ ("1", 1, 0), ("new", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_left(self, model): node_wchildren = model.objects.get(desc="23") obj = node_wchildren.add_sibling("left", desc="new") assert obj.get_depth() == 2 expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("new", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_left_noleft_root(self, model): node = model.objects.get(desc="1") obj = node.add_sibling("left", desc="new") assert obj.get_depth() == 1 expected = [ ("new", 1, 0), ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_left_noleft(self, model): node = model.objects.get(desc="231") obj = node.add_sibling("left", desc="new") assert obj.get_depth() == 3 expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 2), ("new", 3, 0), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_right_root(self, model): node_wchildren = model.objects.get(desc="2") obj = node_wchildren.add_sibling("right", desc="new") assert obj.get_depth() == 1 expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("new", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_right(self, model): node_wchildren = model.objects.get(desc="23") obj = node_wchildren.add_sibling("right", desc="new") assert obj.get_depth() == 2 expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("new", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_right_noright_root(self, model): node = model.objects.get(desc="4") obj = node.add_sibling("right", desc="new") assert obj.get_depth() == 1 expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ("new", 1, 0), ] assert self.got(model) == expected def test_add_sibling_right_noright(self, model): node = model.objects.get(desc="231") obj = node.add_sibling("right", desc="new") assert obj.get_depth() == 3 expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 2), ("231", 3, 0), ("new", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_add_sibling_with_passed_instance(self, model): node_wchildren = model.objects.get(desc="2") obj = model(desc="5") result = node_wchildren.add_sibling("last-sibling", instance=obj) assert result == obj assert obj.get_depth() == 1 assert node_wchildren.get_last_sibling().desc == "5" def test_add_sibling_already_saved_instance(self, model): node_wchildren = model.objects.get(desc="2") existing_node = model.objects.get(desc="4") with pytest.raises(NodeAlreadySaved): node_wchildren.add_sibling("last-sibling", instance=existing_node) def test_add_child_with_pk_set(self, model): """ If the model is using a natural primary key then it will be already set when the instance is inserted. """ child = model(pk=999999, desc="natural key") result = model.objects.get(desc="2").add_child(instance=child) assert result == child @pytest.mark.django_db class TestDelete(TestTreeBase): @staticmethod @pytest.fixture( scope="function", params=zip(models.BASE_MODELS, models.DEP_MODELS), ids=lambda fv: f"base={fv[0].__name__} dep={fv[1].__name__}", ) def delete_dep_model_pair(request): base_model, dep_model = request.param base_model.load_bulk(BASE_DATA) for node in base_model.objects.all(): dep_model(node=node).save() return base_model, dep_model def test_delete_leaf(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.get(desc="231").delete() expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(delete_model) == expected assert result == (2, {delete_model._meta.label: 1, dep_model._meta.label: 1}) def test_delete_node(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.get(desc="23").delete() expected = [ ("1", 1, 0), ("2", 1, 3), ("21", 2, 0), ("22", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(delete_model) == expected assert result == (4, {delete_model._meta.label: 2, dep_model._meta.label: 2}) def test_delete_root(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.get(desc="2").delete() expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)] assert self.got(delete_model) == expected assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6}) def test_delete_filter_root_nodes(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.filter(desc__in=("2", "3")).delete() expected = [("1", 1, 0), ("4", 1, 1), ("41", 2, 0)] assert self.got(delete_model) == expected assert result == (14, {delete_model._meta.label: 7, dep_model._meta.label: 7}) def test_delete_filter_children(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.filter(desc__in=("2", "23", "231")).delete() expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)] assert self.got(delete_model) == expected assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6}) def test_delete_nonexistant_nodes(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.filter(desc__in=("ZZZ", "XXX")).delete() assert self.got(delete_model) == UNCHANGED assert result == (0, {}) def test_delete_same_node_twice(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.filter(desc__in=("2", "2")).delete() expected = [("1", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0)] assert self.got(delete_model) == expected assert result == (12, {delete_model._meta.label: 6, dep_model._meta.label: 6}) def test_delete_all_root_nodes(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.get_root_nodes().delete() assert result == (20, {delete_model._meta.label: 10, dep_model._meta.label: 10}) assert delete_model.objects.count() == 0 def test_delete_all_nodes(self, delete_dep_model_pair): delete_model, dep_model = delete_dep_model_pair result = delete_model.objects.all().delete() assert result == (20, {delete_model._meta.label: 10, dep_model._meta.label: 10}) assert delete_model.objects.count() == 0 @pytest.mark.django_db class TestMoveErrors(TestNonEmptyTree): def test_move_invalid_pos(self, model): node = model.objects.get(desc="231") with pytest.raises(InvalidPosition): node.move(node, "invalid_pos") def test_move_to_descendant(self, model): node = model.objects.get(desc="2") target = model.objects.get(desc="231") with pytest.raises(InvalidMoveToDescendant): node.move(target, "first-sibling") def test_move_missing_nodeorderby(self, model): node = model.objects.get(desc="231") with pytest.raises(MissingNodeOrderBy): node.move(node, "sorted-child") with pytest.raises(MissingNodeOrderBy): node.move(node, "sorted-sibling") @pytest.mark.django_db class TestMoveSortedErrors(TestTreeBase): def test_nonsorted_move_in_sorted(self, sorted_model): node = sorted_model.add_root(val1=3, val2=3, desc="zxy") with pytest.raises(InvalidPosition): node.move(node, "left") @pytest.mark.django_db class TestMoveLeafRoot(TestNonEmptyTree): def test_move_leaf_last_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "last-sibling") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ("231", 1, 0), ] assert self.got(model) == expected def test_move_leaf_first_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "first-sibling") expected = [ ("231", 1, 0), ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_left_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "left") expected = [ ("1", 1, 0), ("231", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_right_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("231", 1, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_last_child_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "last-child") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("231", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_first_child_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="231").move(target, "first-child") expected = [ ("1", 1, 0), ("2", 1, 5), ("231", 2, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected @pytest.mark.django_db class TestMoveLeaf(TestNonEmptyTree): def test_move_leaf_last_sibling(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "last-sibling") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("231", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_first_sibling(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "first-sibling") expected = [ ("1", 1, 0), ("2", 1, 5), ("231", 2, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_left_sibling(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "left") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("231", 2, 0), ("22", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_right_sibling(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("231", 2, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_left_sibling_itself(self, model): target = model.objects.get(desc="231") model.objects.get(desc="231").move(target, "left") assert self.got(model) == UNCHANGED def test_move_leaf_last_child(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "last-child") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 1), ("231", 3, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_leaf_first_child(self, model): target = model.objects.get(desc="22") model.objects.get(desc="231").move(target, "first-child") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 1), ("231", 3, 0), ("23", 2, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected @pytest.mark.django_db class TestMoveBranchRoot(TestNonEmptyTree): def test_move_branch_first_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "first-sibling") expected = [ ("4", 1, 1), ("41", 2, 0), ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_last_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "last-sibling") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_branch_left_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "left") expected = [ ("1", 1, 0), ("4", 1, 1), ("41", 2, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_right_sibling_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("4", 1, 1), ("41", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_left_noleft_sibling_root(self, model): target = model.objects.get(desc="2").get_first_sibling() model.objects.get(desc="4").move(target, "left") expected = [ ("4", 1, 1), ("41", 2, 0), ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_right_noright_sibling_root(self, model): target = model.objects.get(desc="2").get_last_sibling() model.objects.get(desc="4").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ("4", 1, 1), ("41", 2, 0), ] assert self.got(model) == expected def test_move_branch_first_child_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "first-child") expected = [ ("1", 1, 0), ("2", 1, 5), ("4", 2, 1), ("41", 3, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_last_child_root(self, model): target = model.objects.get(desc="2") model.objects.get(desc="4").move(target, "last-child") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("4", 2, 1), ("41", 3, 0), ("3", 1, 0), ] assert self.got(model) == expected @pytest.mark.django_db class TestMoveBranch(TestNonEmptyTree): def test_move_branch_first_sibling(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "first-sibling") expected = [ ("1", 1, 0), ("2", 1, 5), ("4", 2, 1), ("41", 3, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_last_sibling(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "last-sibling") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("4", 2, 1), ("41", 3, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_left_sibling(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "left") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("4", 2, 1), ("41", 3, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_right_sibling(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("4", 2, 1), ("41", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_left_noleft_sibling(self, model): target = model.objects.get(desc="23").get_first_sibling() model.objects.get(desc="4").move(target, "left") expected = [ ("1", 1, 0), ("2", 1, 5), ("4", 2, 1), ("41", 3, 0), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_right_noright_sibling(self, model): target = model.objects.get(desc="23").get_last_sibling() model.objects.get(desc="4").move(target, "right") expected = [ ("1", 1, 0), ("2", 1, 5), ("21", 2, 0), ("22", 2, 0), ("23", 2, 1), ("231", 3, 0), ("24", 2, 0), ("4", 2, 1), ("41", 3, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_left_itself_sibling(self, model): target = model.objects.get(desc="4") model.objects.get(desc="4").move(target, "left") assert self.got(model) == UNCHANGED def test_move_branch_first_child(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "first-child") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 2), ("4", 3, 1), ("41", 4, 0), ("231", 3, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected def test_move_branch_last_child(self, model): target = model.objects.get(desc="23") model.objects.get(desc="4").move(target, "last-child") expected = [ ("1", 1, 0), ("2", 1, 4), ("21", 2, 0), ("22", 2, 0), ("23", 2, 2), ("231", 3, 0), ("4", 3, 1), ("41", 4, 0), ("24", 2, 0), ("3", 1, 0), ] assert self.got(model) == expected @pytest.mark.django_db class TestTreeSorted(TestTreeBase): def got(self, sorted_model): return [ (o.val1, o.val2, o.desc, o.get_depth(), o.get_children_count()) for o in sorted_model.get_tree() ] def test_add_root_sorted(self, sorted_model): sorted_model.add_root(val1=3, val2=3, desc="zxy") sorted_model.add_root(val1=1, val2=4, desc="bcd") sorted_model.add_root(val1=2, val2=5, desc="zxy") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=4, val2=1, desc="fgh") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=2, val2=2, desc="qwe") sorted_model.add_root(val1=3, val2=2, desc="vcx") expected = [ (1, 4, "bcd", 1, 0), (2, 2, "qwe", 1, 0), (2, 5, "zxy", 1, 0), (3, 2, "vcx", 1, 0), (3, 3, "abc", 1, 0), (3, 3, "abc", 1, 0), (3, 3, "zxy", 1, 0), (4, 1, "fgh", 1, 0), ] assert self.got(sorted_model) == expected def test_add_child_root_sorted(self, sorted_model): root = sorted_model.add_root(val1=0, val2=0, desc="aaa") root.add_child(val1=3, val2=3, desc="zxy") root.add_child(val1=1, val2=4, desc="bcd") root.add_child(val1=2, val2=5, desc="zxy") root.add_child(val1=3, val2=3, desc="abc") root.add_child(val1=4, val2=1, desc="fgh") root.add_child(val1=3, val2=3, desc="abc") root.add_child(val1=2, val2=2, desc="qwe") root.add_child(val1=3, val2=2, desc="vcx") expected = [ (0, 0, "aaa", 1, 8), (1, 4, "bcd", 2, 0), (2, 2, "qwe", 2, 0), (2, 5, "zxy", 2, 0), (3, 2, "vcx", 2, 0), (3, 3, "abc", 2, 0), (3, 3, "abc", 2, 0), (3, 3, "zxy", 2, 0), (4, 1, "fgh", 2, 0), ] assert self.got(sorted_model) == expected def test_add_child_nonroot_sorted(self, sorted_model): get_node = lambda node_id: sorted_model.objects.get(pk=node_id) root_id = sorted_model.add_root(val1=0, val2=0, desc="a").pk node_id = get_node(root_id).add_child(val1=0, val2=0, desc="ac").pk get_node(root_id).add_child(val1=0, val2=0, desc="aa") get_node(root_id).add_child(val1=0, val2=0, desc="av") get_node(node_id).add_child(val1=0, val2=0, desc="aca") get_node(node_id).add_child(val1=0, val2=0, desc="acc") get_node(node_id).add_child(val1=0, val2=0, desc="acb") expected = [ (0, 0, "a", 1, 3), (0, 0, "aa", 2, 0), (0, 0, "ac", 2, 3), (0, 0, "aca", 3, 0), (0, 0, "acb", 3, 0), (0, 0, "acc", 3, 0), (0, 0, "av", 2, 0), ] assert self.got(sorted_model) == expected def test_move_sorted(self, sorted_model): sorted_model.add_root(val1=3, val2=3, desc="zxy") sorted_model.add_root(val1=1, val2=4, desc="bcd") sorted_model.add_root(val1=2, val2=5, desc="zxy") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=4, val2=1, desc="fgh") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=2, val2=2, desc="qwe") sorted_model.add_root(val1=3, val2=2, desc="vcx") root_nodes = sorted_model.get_root_nodes() target = root_nodes[0] for node in root_nodes[1:]: # because raw queries don't update django objects node = sorted_model.objects.get(pk=node.pk) target = sorted_model.objects.get(pk=target.pk) node.move(target, "sorted-child") expected = [ (1, 4, "bcd", 1, 7), (2, 2, "qwe", 2, 0), (2, 5, "zxy", 2, 0), (3, 2, "vcx", 2, 0), (3, 3, "abc", 2, 0), (3, 3, "abc", 2, 0), (3, 3, "zxy", 2, 0), (4, 1, "fgh", 2, 0), ] assert self.got(sorted_model) == expected def test_move_sortedsibling(self, sorted_model): # https://bitbucket.org/tabo/django-treebeard/issue/27 sorted_model.add_root(val1=3, val2=3, desc="zxy") sorted_model.add_root(val1=1, val2=4, desc="bcd") sorted_model.add_root(val1=2, val2=5, desc="zxy") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=4, val2=1, desc="fgh") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=2, val2=2, desc="qwe") sorted_model.add_root(val1=3, val2=2, desc="vcx") root_nodes = sorted_model.get_root_nodes() target = root_nodes[0] for node in root_nodes[1:]: # because raw queries don't update django objects node = sorted_model.objects.get(pk=node.pk) target = sorted_model.objects.get(pk=target.pk) node.val1 -= 2 node.save() node.move(target, "sorted-sibling") expected = [ (0, 2, "qwe", 1, 0), (0, 5, "zxy", 1, 0), (1, 2, "vcx", 1, 0), (1, 3, "abc", 1, 0), (1, 3, "abc", 1, 0), (1, 3, "zxy", 1, 0), (1, 4, "bcd", 1, 0), (2, 1, "fgh", 1, 0), ] assert self.got(sorted_model) == expected @pytest.mark.django_db class TestInheritedModels(TestTreeBase): @staticmethod @pytest.fixture( scope="function", params=zip(models.BASE_MODELS, models.INHERITED_MODELS), ids=lambda fv: f"base={fv[0].__name__} inherited={fv[1].__name__}", ) def inherited_model(request): base_model, inherited_model = request.param base_model.add_root(desc="1") base_model.add_root(desc="2") node21 = inherited_model(desc="21") base_model.objects.get(desc="2").add_child(instance=node21) base_model.objects.get(desc="21").add_child(desc="211") base_model.objects.get(desc="21").add_child(desc="212") base_model.objects.get(desc="2").add_child(desc="22") node3 = inherited_model(desc="3") base_model.add_root(instance=node3) return inherited_model def test_get_tree_all(self, inherited_model): got = [ (o.desc, o.get_depth(), o.get_children_count()) for o in inherited_model.get_tree() ] expected = [ ("1", 1, 0), ("2", 1, 2), ("21", 2, 2), ("211", 3, 0), ("212", 3, 0), ("22", 2, 0), ("3", 1, 0), ] assert got == expected def test_get_tree_node(self, inherited_model): node = inherited_model.objects.get(desc="21") got = [ (o.desc, o.get_depth(), o.get_children_count()) for o in inherited_model.get_tree(node) ] expected = [ ("21", 2, 2), ("211", 3, 0), ("212", 3, 0), ] assert got == expected def test_get_root_nodes(self, inherited_model): got = inherited_model.get_root_nodes() expected = ["1", "2", "3"] assert [node.desc for node in got] == expected def test_get_first_root_node(self, inherited_model): got = inherited_model.get_first_root_node() assert got.desc == "1" def test_get_last_root_node(self, inherited_model): got = inherited_model.get_last_root_node() assert got.desc == "3" def test_is_root(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.is_root() is False assert node3.is_root() is True def test_is_leaf(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.is_leaf() is False assert node3.is_leaf() is True def test_get_root(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_root().desc == "2" assert node3.get_root().desc == "3" def test_get_parent(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_parent().desc == "2" assert node3.get_parent() is None def test_get_children(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert [node.desc for node in node21.get_children()] == ["211", "212"] assert [node.desc for node in node3.get_children()] == [] def test_get_children_count(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_children_count() == 2 assert node3.get_children_count() == 0 def test_get_siblings(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert [node.desc for node in node21.get_siblings()] == ["21", "22"] assert [node.desc for node in node3.get_siblings()] == ["1", "2", "3"] def test_get_first_sibling(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_first_sibling().desc == "21" assert node3.get_first_sibling().desc == "1" def test_get_prev_sibling(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_prev_sibling() is None assert node3.get_prev_sibling().desc == "2" def test_get_next_sibling(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_next_sibling().desc == "22" assert node3.get_next_sibling() is None def test_get_last_sibling(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_last_sibling().desc == "22" assert node3.get_last_sibling().desc == "3" def test_get_first_child(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_first_child().desc == "211" assert node3.get_first_child() is None def test_get_last_child(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_last_child().desc == "212" assert node3.get_last_child() is None def test_get_ancestors(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert [node.desc for node in node21.get_ancestors()] == ["2"] assert [node.desc for node in node3.get_ancestors()] == [] def test_get_descendants(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert [node.desc for node in node21.get_descendants()] == ["211", "212"] assert [node.desc for node in node3.get_descendants()] == [] def test_get_descendant_count(self, inherited_model): node21 = inherited_model.objects.get(desc="21") node3 = inherited_model.objects.get(desc="3") assert node21.get_descendant_count() == 2 assert node3.get_descendant_count() == 0 def test_cascading_deletion(self, inherited_model): # Deleting a node by calling delete() on the inherited_model class # should delete descendants, even if those descendants are not # instances of inherited_model base_model = inherited_model.__bases__[0] node21 = inherited_model.objects.get(desc="21") node21.delete() node2 = base_model.objects.get(desc="2") for desc in ["21", "211", "212"]: assert not base_model.objects.filter(desc=desc).exists() assert [node.desc for node in node2.get_descendants()] == ["22"] @pytest.mark.django_db class TestMP_TreeAlphabet(TestTreeBase): @pytest.mark.skipif( not os.getenv("TREEBEARD_TEST_ALPHABET", False), reason="TREEBEARD_TEST_ALPHABET env variable not set.", ) def test_alphabet(self, mpalphabet_model): """This isn't actually a test, it's an informational routine.""" basealpha = numconv.BASE85 got_err = False last_good = None for alphabetlen in range(3, len(basealpha) + 1): alphabet = basealpha[0:alphabetlen] assert len(alphabet) >= 3 expected = [alphabet[0] + char for char in alphabet[1:]] expected.extend([alphabet[1] + char for char in alphabet]) expected.append(alphabet[2] + alphabet[0]) # remove all nodes mpalphabet_model.objects.all().delete() # change the model's alphabet mpalphabet_model.alphabet = alphabet mpalphabet_model.numconv_obj_ = None # insert root nodes for pos in range(len(alphabet) * 2): try: mpalphabet_model.add_root(numval=pos) except: got_err = True break if got_err: break got = [obj.path for obj in mpalphabet_model.objects.all()] if got != expected: break last_good = alphabet assert False, "Best BASE85 based alphabet for your setup: {} (base {})".format( last_good, len(last_good) ) @pytest.mark.django_db class TestHelpers(TestTreeBase): @staticmethod @pytest.fixture(scope="function", params=models.BASE_MODELS + models.PROXY_MODELS) def helpers_model(request): model = request.param model.load_bulk(BASE_DATA) for node in model.get_root_nodes(): model.load_bulk(BASE_DATA, node) model.add_root(desc="5") return model def test_descendants_group_count_root(self, helpers_model): expected = [ (o.desc, o.get_descendant_count()) for o in helpers_model.get_root_nodes() ] got = [ (o.desc, o.descendants_count) for o in helpers_model.get_descendants_group_count() ] assert got == expected def test_descendants_group_count_node(self, helpers_model): parent = helpers_model.get_root_nodes().get(desc="2") expected = [(o.desc, o.get_descendant_count()) for o in parent.get_children()] got = [ (o.desc, o.descendants_count) for o in helpers_model.get_descendants_group_count(parent) ] assert got == expected @pytest.mark.django_db class TestMP_TreeSortedAutoNow(TestTreeBase): """ The sorting mechanism used by treebeard when adding a node can fail if the ordering is using an "auto_now" field """ def test_sorted_by_autonow_workaround(self, mpsortedautonow_model): # workaround for i in range(1, 5): mpsortedautonow_model.add_root( desc="node%d" % (i,), created=datetime.datetime.now() ) def test_sorted_by_autonow_FAIL(self, mpsortedautonow_model): """ This test asserts that we have a problem. fix this, somehow """ mpsortedautonow_model.add_root(desc="node1") with pytest.raises(ValueError): mpsortedautonow_model.add_root(desc="node2") @pytest.mark.django_db class TestMP_TreeStepOverflow(TestTreeBase): def test_add_root(self, mpsmallstep_model): method = mpsmallstep_model.add_root for i in range(1, 10): method() with pytest.raises(PathOverflow): method() def test_add_child(self, mpsmallstep_model): root = mpsmallstep_model.add_root() method = root.add_child for i in range(1, 10): method() with pytest.raises(PathOverflow): method() def test_add_sibling(self, mpsmallstep_model): root = mpsmallstep_model.add_root() for i in range(1, 10): root.add_child() positions = ("first-sibling", "left", "right", "last-sibling") for pos in positions: with pytest.raises(PathOverflow): root.get_last_child().add_sibling(pos) def test_move(self, mpsmallstep_model): root = mpsmallstep_model.add_root() for i in range(1, 10): root.add_child() newroot = mpsmallstep_model.add_root() targets = [ (root, ["first-child", "last-child"]), ( root.get_first_child(), ["first-sibling", "left", "right", "last-sibling"], ), ] for target, positions in targets: for pos in positions: with pytest.raises(PathOverflow): newroot.move(target, pos) @pytest.mark.django_db class TestMP_TreeShortPath(TestTreeBase): """Test a tree with a very small path field (max_length=4) and a steplen of 1 """ def test_short_path(self, mpshortnotsorted_model): obj = mpshortnotsorted_model.add_root() obj = obj.add_child().add_child().add_child() with pytest.raises(PathOverflow): obj.add_child() @pytest.mark.django_db class TestMP_TreeFindProblems(TestTreeBase): def test_find_problems(self, mpalphabet_model): mpalphabet_model.alphabet = "01234" mpalphabet_model(path="01", depth=1, numchild=0, numval=0).save() mpalphabet_model(path="1", depth=1, numchild=0, numval=0).save() mpalphabet_model(path="111", depth=1, numchild=0, numval=0).save() mpalphabet_model(path="abcd", depth=1, numchild=0, numval=0).save() mpalphabet_model(path="qa#$%!", depth=1, numchild=0, numval=0).save() mpalphabet_model(path="0201", depth=2, numchild=0, numval=0).save() mpalphabet_model(path="020201", depth=3, numchild=0, numval=0).save() mpalphabet_model(path="03", depth=1, numchild=2, numval=0).save() mpalphabet_model(path="0301", depth=2, numchild=0, numval=0).save() mpalphabet_model(path="030102", depth=3, numchild=10, numval=0).save() mpalphabet_model(path="04", depth=10, numchild=1, numval=0).save() mpalphabet_model(path="0401", depth=20, numchild=0, numval=0).save() def got(ids): return [o.path for o in mpalphabet_model.objects.filter(pk__in=ids)] ( evil_chars, bad_steplen, orphans, wrong_depth, wrong_numchild, ) = mpalphabet_model.find_problems() assert ["abcd", "qa#$%!"] == got(evil_chars) assert ["1", "111"] == got(bad_steplen) assert ["0201", "020201"] == got(orphans) assert ["03", "0301", "030102"] == got(wrong_numchild) assert ["04", "0401"] == got(wrong_depth) @pytest.mark.django_db class TestMP_TreeFix(TestTreeBase): expected_no_holes = { models.MP_TestNodeShortPath: [ ("1", "b", 1, 2), ("11", "u", 2, 1), ("111", "i", 3, 1), ("1111", "e", 4, 0), ("12", "o", 2, 0), ("2", "d", 1, 0), ("3", "g", 1, 0), ("4", "a", 1, 4), ("41", "a", 2, 0), ("42", "a", 2, 0), ("43", "u", 2, 1), ("431", "i", 3, 1), ("4311", "e", 4, 0), ("44", "o", 2, 0), ], models.MP_TestSortedNodeShortPath: [ ("1", "a", 1, 4), ("11", "a", 2, 0), ("12", "a", 2, 0), ("13", "o", 2, 0), ("14", "u", 2, 1), ("141", "i", 3, 1), ("1411", "e", 4, 0), ("2", "b", 1, 2), ("21", "o", 2, 0), ("22", "u", 2, 1), ("221", "i", 3, 1), ("2211", "e", 4, 0), ("3", "d", 1, 0), ("4", "g", 1, 0), ], } expected_with_holes = { models.MP_TestNodeShortPath: [ ("1", "b", 1, 2), ("13", "u", 2, 1), ("134", "i", 3, 1), ("1343", "e", 4, 0), ("14", "o", 2, 0), ("2", "d", 1, 0), ("3", "g", 1, 0), ("4", "a", 1, 4), ("41", "a", 2, 0), ("42", "a", 2, 0), ("43", "u", 2, 1), ("434", "i", 3, 1), ("4343", "e", 4, 0), ("44", "o", 2, 0), ], models.MP_TestSortedNodeShortPath: [ ("1", "b", 1, 2), ("13", "u", 2, 1), ("134", "i", 3, 1), ("1343", "e", 4, 0), ("14", "o", 2, 0), ("2", "d", 1, 0), ("3", "g", 1, 0), ("4", "a", 1, 4), ("41", "a", 2, 0), ("42", "a", 2, 0), ("43", "u", 2, 1), ("434", "i", 3, 1), ("4343", "e", 4, 0), ("44", "o", 2, 0), ], } def got(self, model): return [ (o.path, o.desc, o.get_depth(), o.get_children_count()) for o in model.get_tree() ] def add_broken_test_data(self, model): model(path="4", depth=2, numchild=2, desc="a").save() model(path="13", depth=1000, numchild=0, desc="u").save() model(path="14", depth=4, numchild=500, desc="o").save() model(path="134", depth=321, numchild=543, desc="i").save() model(path="1343", depth=321, numchild=543, desc="e").save() model(path="42", depth=1, numchild=1, desc="a").save() model(path="43", depth=1000, numchild=0, desc="u").save() model(path="44", depth=4, numchild=500, desc="o").save() model(path="434", depth=321, numchild=543, desc="i").save() model(path="4343", depth=321, numchild=543, desc="e").save() model(path="41", depth=1, numchild=1, desc="a").save() model(path="3", depth=221, numchild=322, desc="g").save() model(path="1", depth=10, numchild=3, desc="b").save() model(path="2", depth=10, numchild=3, desc="d").save() def test_fix_tree_non_destructive(self, mpshort_model): self.add_broken_test_data(mpshort_model) mpshort_model.fix_tree(destructive=False) got = self.got(mpshort_model) expected = self.expected_with_holes[mpshort_model] assert got == expected mpshort_model.find_problems() def test_fix_tree_destructive(self, mpshort_model): self.add_broken_test_data(mpshort_model) mpshort_model.fix_tree(destructive=True) got = self.got(mpshort_model) expected = self.expected_no_holes[mpshort_model] assert got == expected mpshort_model.find_problems() def test_fix_tree_with_fix_paths(self, mpshort_model): self.add_broken_test_data(mpshort_model) mpshort_model.fix_tree(fix_paths=True) got = self.got(mpshort_model) expected = self.expected_no_holes[mpshort_model] assert got == expected mpshort_model.find_problems() @pytest.mark.django_db class TestIssues(TestTreeBase): # test for http://code.google.com/p/django-treebeard/issues/detail?id=14 def test_many_to_many_django_user_anonymous(self, mpm2muser_model): # Using AnonymousUser() in the querysets will expose non-treebeard # related problems in Django 1.0 # # Postgres: # ProgrammingError: can't adapt # SQLite: # InterfaceError: Error binding parameter 4 - probably unsupported # type. # MySQL compared a string to an integer field: # `treebeard_mp_testissue14_users`.`user_id` = 'AnonymousUser' # # Using a None field instead works (will be translated to IS NULL). # # anonuserobj = AnonymousUser() anonuserobj = None def qs_check(qs, expected): assert [o.name for o in qs] == expected def qs_check_first_or_user(expected, root, user): qs_check( root.get_children().filter(Q(name="first") | Q(users=user)), expected ) user = User.objects.create_user("test_user", "test@example.com", "testpasswd") user.save() root = mpm2muser_model.add_root(name="the root node") root.add_child(name="first") second = root.add_child(name="second") qs_check(root.get_children(), ["first", "second"]) qs_check(root.get_children().filter(Q(name="first")), ["first"]) qs_check(root.get_children().filter(Q(users=user)), []) qs_check_first_or_user(["first"], root, user) qs_check_first_or_user(["first", "second"], root, anonuserobj) user = User.objects.get(username="test_user") second.users.add(user) qs_check_first_or_user(["first", "second"], root, user) qs_check_first_or_user(["first"], root, anonuserobj) @pytest.mark.django_db class TestMoveNodeForm(TestNonEmptyTree): def _get_nodes_list(self, nodes): return [ (pk, "%s%s" % (" " * 4 * (depth - 1), str)) for pk, str, depth in nodes ] def _assert_nodes_in_choices(self, form, nodes): choices = form.fields["_ref_node_id"].choices assert choices.pop(0)[0] is None assert nodes == [(choice[0], choice[1]) for choice in choices] def _move_node_helper(self, node, safe_parent_nodes): form_class = movenodeform_factory(type(node)) form = form_class(instance=node) assert ["desc", "_position", "_ref_node_id"] == list(form.base_fields.keys()) got = [choice[0] for choice in form.fields["_position"].choices] assert ["first-child", "left", "right"] == got nodes = self._get_nodes_list(safe_parent_nodes) self._assert_nodes_in_choices(form, nodes) def _get_node_ids_strs_and_depths(self, nodes): return [(node.pk, str(node), node.get_depth()) for node in nodes] def test_form_root_node(self, model): nodes = list(model.get_tree()) node = nodes.pop(0) safe_parent_nodes = self._get_node_ids_strs_and_depths(nodes) self._move_node_helper(node, safe_parent_nodes) def test_form_leaf_node(self, model): nodes = list(model.get_tree()) safe_parent_nodes = self._get_node_ids_strs_and_depths(nodes) node = nodes.pop() self._move_node_helper(node, safe_parent_nodes) def test_form_admin(self, model): request = None nodes = list(model.get_tree()) safe_parent_nodes = self._get_node_ids_strs_and_depths(nodes) for node in model.objects.all(): site = AdminSite() form_class = movenodeform_factory(model) admin_class = admin_factory(form_class) ma = admin_class(model, site) got = list(ma.get_form(request).base_fields.keys()) desc_pos_refnodeid = ["desc", "_position", "_ref_node_id"] assert desc_pos_refnodeid == got got = ma.get_fieldsets(request) expected = [(None, {"fields": desc_pos_refnodeid})] assert got == expected got = ma.get_fieldsets(request, node) assert got == expected form = ma.get_form(request)() nodes = self._get_nodes_list(safe_parent_nodes) self._assert_nodes_in_choices(form, nodes) @pytest.mark.django_db class TestModelAdmin(TestNonEmptyTree): def test_default_fields(self, model): site = AdminSite() form_class = movenodeform_factory(model) admin_class = admin_factory(form_class) ma = admin_class(model, site) assert list(ma.get_form(None).base_fields.keys()) == [ "desc", "_position", "_ref_node_id", ] @pytest.mark.django_db class TestSortedForm(TestTreeSorted): def test_sorted_form(self, sorted_model): sorted_model.add_root(val1=3, val2=3, desc="zxy") sorted_model.add_root(val1=1, val2=4, desc="bcd") sorted_model.add_root(val1=2, val2=5, desc="zxy") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=4, val2=1, desc="fgh") sorted_model.add_root(val1=3, val2=3, desc="abc") sorted_model.add_root(val1=2, val2=2, desc="qwe") sorted_model.add_root(val1=3, val2=2, desc="vcx") form_class = movenodeform_factory(sorted_model) form = form_class() assert list(form.fields.keys()) == [ "val1", "val2", "desc", "_position", "_ref_node_id", ] form = form_class(instance=sorted_model.objects.get(desc="bcd")) assert list(form.fields.keys()) == [ "val1", "val2", "desc", "_position", "_ref_node_id", ] assert "id__position" in str(form) assert "id__ref_node_id" in str(form) @pytest.mark.django_db class TestForm(TestNonEmptyTree): def test_form(self, model): form_class = movenodeform_factory(model) form = form_class() assert list(form.fields.keys()) == ["desc", "_position", "_ref_node_id"] form = form_class(instance=model.objects.get(desc="1")) assert list(form.fields.keys()) == ["desc", "_position", "_ref_node_id"] assert "id__position" in str(form) assert "id__ref_node_id" in str(form) def test_move_node_form(self, model): form_class = movenodeform_factory(model) bad_node = model.objects.get(desc="1").add_child( desc='Benign' ) form = form_class(instance=bad_node) rendered_html = form.as_p() assert "Benign" in rendered_html assert "