def test_set_positions_with_gap_in_sequence(cur, nd1, nd2, nd3): set_position(cur, nd1, 0, auto_position=False) set_position(cur, nd2, 1, auto_position=False) set_position(cur, nd3, 3, auto_position=False) assert get_node(cur, nd1.id).position == 0 assert get_node(cur, nd2.id).position == 1 assert get_node(cur, nd3.id).position == 3
def test_set_position_autoposition(cur, root, nd1, nd2, nd3): set_position(cur, nd1, 0, auto_position=True) set_position(cur, nd2, 2, auto_position=True) set_position(cur, nd3.id, -1, auto_position=True) assert get_node(cur, nd1.id).position == 0 assert get_node(cur, nd2.id).position == 2 assert get_node(cur, nd3.id).position == nd3.position + 1
def get_inherited_properties(cur, node): """ Get the entire inherited property dictionary. To calculate this, the trees path from root node till ``node`` will be traversed. For each level, the property dictionary will be merged into the previous one. This is a simple merge, only the first level of keys will be combined. :param node: :type node: Node or uuid4 :rtype: dict """ ret = {} id = str(node) if isinstance(node, str): node = get_node(cur, id) ancestors = list(get_ancestors(cur, id)) for ancestor in ancestors[::-1]: # Go top down ret.update(ancestor.properties) ret.update(node.properties) return ret
def set_properties(cur, node, new_properties): """ Set the property dictionary to ``new_properties``. Return ``NodeData`` object with updated properties. :param node: :type node: Node or uuid4 :param new_properties: dict """ if not isinstance(new_properties, dict): raise TypeError('Only dictionaries are supported.') id = str(node) if isinstance(node, str): node = get_node(cur, id) sql = """ UPDATE nodes SET properties=%s WHERE id=%s; """ cur.execute(sql, (json.dumps(new_properties), str(node))) kwargs = node.to_dict() kwargs['properties'] = new_properties return NodeData(**kwargs)
def get_recursive_properties(cur, node): """ Get the entire inherited and recursively merged property dictionary. To calculate this, the trees path from root node till ``node`` will be traversed. For each level, the property dictionary will be merged into the previous one. This is a recursive merge, so all dictionary levels will be combined. :param node: :type node: Node or uuid4 :rtype: dict """ ret = {} id = str(node) if isinstance(node, str): node = get_node(cur, id) ancestors = list(get_ancestors(cur, id)) for ancestor in ancestors[::-1]: # Go top down recursive_dict_merge(ret, ancestor.properties, create_copy=False) recursive_dict_merge(ret, node.properties, create_copy=False) return ret
def get_or_create_nd(cur, parent, properties, *args, **kwargs): xtype = properties.get('type') node_id = node_ids.get(xtype, None) if node_id is None: node = insert_node(cur, parent, properties=properties, *args, **kwargs) node_ids[xtype] = node.id return node return get_node(cur, node_id)
def test_delete_node(cur, nd1, nd2_1, nd2_1_1, nd2_leaf): """ Tree layout before delete: / - nd1 - nd2-1 - nd2-1-1 - nd2-leaf - nd2 - nd3 Expected tree layout after move: / - nd1 - nd2-1 - nd2 - nd3 """ delete_node(cur, nd2_1_1, auto_position=False) # Deleted node doesn't exist anymore with pytest.raises(exceptions.NodeNotFound): get_node(cur, nd2_1_1.id) # nd2-1 has no children and no descendants assert set(get_child_ids(cur, nd2_1)) == set() assert set(get_child_ids(cur, nd2_1_1)) == set() assert set(get_descendant_ids(cur, nd2_1)) == set() # nd1 just contains nd2-1 assert set(get_child_ids(cur, nd1)) == {nd2_1.id} assert set(get_descendant_ids(cur, nd1)) == {nd2_1.id} # Ancestor and descendant sets of nd2-1-1 and nd2-leaf are empty # (or raise error in the future because they don't exist anymore) assert set(get_ancestor_ids(cur, nd2_1_1)) == set() assert set(get_ancestor_ids(cur, nd2_leaf)) == set() assert set(get_descendant_ids(cur, nd2_1_1)) == set() assert set(get_descendant_ids(cur, nd2_leaf)) == set()
def test_delete_node(cur, nd1, nd2_1, nd2_1_1, nd2_leaf): """ Tree layout before delete: / - nd1 - nd2-1 - nd2-1-1 - nd2-leaf - nd2 - nd3 Expected tree layout after move: / - nd1 - nd2-1 - nd2 - nd3 """ delete_node(cur, nd2_1_1, auto_position=False) # Deleted node doesn't exist anymore with pytest.raises(ValueError): get_node(cur, nd2_1_1.id) # nd2-1 has no children and no descendants assert set(get_child_ids(cur, nd2_1)) == set() assert set(get_child_ids(cur, nd2_1_1)) == set() assert set(get_descendant_ids(cur, nd2_1)) == set() # nd1 just contains nd2-1 assert set(get_child_ids(cur, nd1)) == {nd2_1.id} assert set(get_descendant_ids(cur, nd1)) == {nd2_1.id} # Ancestor and descendant sets of nd2-1-1 and nd2-leaf are empty # (or raise error in the future because they don't exist anymore) assert set(get_ancestor_ids(cur, nd2_1_1)) == set() assert set(get_ancestor_ids(cur, nd2_leaf)) == set() assert set(get_descendant_ids(cur, nd2_1_1)) == set() assert set(get_descendant_ids(cur, nd2_leaf)) == set()
def change_parent(cur, node, new_parent, position=None, auto_position=True): """ Move node and its subtree from its current to another parent node. Return updated ``Node`` object with new parent set. Raise ``ValueError`` if ``new_parent`` is inside ``node`` s subtree. :param node: :type node: Node or int :param new_parent: Reference to the new parent node :type new_parent: Node or int :param int position: Position in between siblings. If 0, the node will be inserted at the beginning of the parents children. If -1, the node will be inserted the the end of the parents children. If `auto_position` is disabled, this is just a value. :param bool auto_position: See :ref:`coreapi-positioning`. """ new_parent_id = int(new_parent) if new_parent_id in get_descendant_ids(cur, node): raise ValueError('Cannot move node into its own subtree.') # Can't run set_position() here because the node hasn't been moved yet, # must do it manually if auto_position: if type(position) == int and position >= 0: ensure_free_position(cur, new_parent_id, position) else: position = find_highest_position(cur, new_parent_id) + 1 sql = """ UPDATE nodes SET parent=%s, position=%s WHERE id=%s; """ cur.execute(sql, (new_parent_id, position, int(node))) if type(node) == int: node = get_node(cur, node) kwargs = node.to_dict() kwargs['parent'] = new_parent_id kwargs['position'] = position return NodeData(**kwargs)
def change_parent(cur, node, new_parent, position=None, auto_position=True): """ Move node and its subtree from its current to another parent node. Return updated ``Node`` object with new parent set. Raise ``ValueError`` if ``new_parent`` is inside ``node`` s subtree. :param node: :type node: Node or int :param new_parent: Reference to the new parent node :type new_parent: Node or int :param int position: Position in between siblings. If 0, the node will be inserted at the beginning of the parents children. If -1, the node will be inserted the the end of the parents children. If `auto_position` is disabled, this is just a value. :param bool auto_position: See :ref:`core-positioning`. """ new_parent_id = int(new_parent) if new_parent_id in get_descendant_ids(cur, node): raise ValueError('Cannot move node into its own subtree.') # Can't run set_position() here because the node hasn't been moved yet, # must do it manually if auto_position: if type(position) == int and position >= 0: ensure_free_position(cur, new_parent_id, position) else: position = find_highest_position(cur, new_parent_id) + 1 sql = """ UPDATE nodes SET parent=%s, position=%s WHERE id=%s; """ cur.execute(sql, (new_parent_id, position, int(node))) if type(node) == int: node = get_node(cur, node) kwargs = node.to_dict() kwargs['parent'] = new_parent_id kwargs['position'] = position return NodeData(**kwargs)
def update_properties(cur, node, new_properties): """ Update existing property dictionary with another dictionary. Return ``NodeData`` object with updated properties. :param node: :type node: Node or uuid4 :param new_properties: dict """ if not isinstance(new_properties, dict): raise TypeError('Only dictionaries are supported.') id = str(node) if isinstance(node, str): node = get_node(cur, id) properties = node.properties.copy() properties.update(new_properties) return set_properties(cur, node, properties)
def update_properties(cur, node, new_properties): """ Update existing property dictionary with another dictionary. Return ``NodeData`` object with updated properties. :param node: :type node: Node or int :param new_properties: dict """ if type(new_properties) != dict: raise TypeError('Only dictionaries are supported.') id = int(node) if type(node) == int: node = get_node(cur, id) properties = node.properties.copy() properties.update(new_properties) return set_properties(cur, node, properties)
def set_property_value(cur, node, key, value): """ Set the value for a single property key. Return ``NodeData`` object with updated properties. :param node: :type node: Node or uuid4 :param key: str :param value: object """ id = str(node) if isinstance(node, str): node = get_node(cur, id) properties = node.properties.copy() properties[key] = value set_properties(cur, node, properties) kwargs = node.to_dict() kwargs['properties'] = properties return NodeData(**kwargs)
def delete_node(cur, node, auto_position=True): """ Delete node and its subtree. :param node: :type node: Node or int :param bool auto_position: See :ref:`coreapi-positioning` """ id = int(node) # Get Node object if integer (ID) was passed if auto_position and type(node) != NodeData: node = get_node(cur, id) sql = """ DELETE FROM nodes WHERE id=%s; """ cur.execute(sql, (id, )) if auto_position: shift_positions(cur, node.parent, node.position, -1)
def set_position(cur, node, position, auto_position=True): """ Set ``position`` for ``node``. :param node: :type node: Node or int :param int position: Position in between siblings. If 0, the node will be inserted at the beginning of the parents children. If -1, the node will be inserted the the end of the parents children. If `auto_position` is disabled, this is just a value. :param bool auto_position: See :ref:`core-positioning` """ if auto_position: id = int(node) if type(node) == int: node = get_node(cur, id) if type(position) == int and position >= 0: ensure_free_position(cur, node.parent, position) else: position = find_highest_position(cur, node.parent) + 1 else: id = int(node) sql = """ UPDATE nodes SET position=%s WHERE id=%s; """ cur.execute(sql, (position, int(node))) return position
def set_position(cur, node, position, auto_position=True): """ Set ``position`` for ``node``. :param node: :type node: Node or int :param int position: Position in between siblings. If 0, the node will be inserted at the beginning of the parents children. If -1, the node will be inserted the the end of the parents children. If `auto_position` is disabled, this is just a value. :param bool auto_position: See :ref:`coreapi-positioning` """ if auto_position: id = int(node) if type(node) == int: node = get_node(cur, id) if type(position) == int and position >= 0: ensure_free_position(cur, node.parent, position) else: position = find_highest_position(cur, node.parent) + 1 else: id = int(node) sql = """ UPDATE nodes SET position=%s WHERE id=%s; """ cur.execute(sql, (position, int(node))) return position
def delete_node(cur, node, auto_position=True): """ Delete node and its subtree. :param node: :type node: Node or int :param bool auto_position: See :ref:`core-positioning` """ id = int(node) # Get Node object if integer (ID) was passed if auto_position and type(node) != NodeData: node = get_node(cur, id) sql = """ DELETE FROM nodes WHERE id=%s; """ cur.execute(sql, (id, )) if auto_position: shift_positions(cur, node.parent, node.position, -1)
def test_get_node_non_existing(cur): with pytest.raises(exceptions.NodeNotFound): get_node(cur, str(uuid.uuid4()))
def test_get_node(cur, nd1): node = get_node(cur, nd1.id) assert node.id == nd1.id assert node.parent == nd1.parent
def test_swap_node_positions(cur, nd1, nd2): swap_node_positions(cur, nd1, nd2) assert get_node(cur, nd1.id).position == nd2.position assert get_node(cur, nd2.id).position == nd1.position
def test_shift_positions_to_the_left(cur, root, nd1, nd2, nd3): shift_positions(cur, root, nd2.position, -1) assert get_node(cur, nd1.id).position == 0 assert get_node(cur, nd2.id).position == 1 assert get_node(cur, nd3.id).position == 3
def test_shift_positions_to_the_right(cur, root, nd1, nd2, nd3): shift_positions(cur, root, nd2.position, +1) assert get_node(cur, nd1.id).position == 0 assert get_node(cur, nd2.id).position == 2 assert get_node(cur, nd3.id).position == 4
def test_change_parent_starts_couting_at_zero(cur, root, nd2, nd2_1): change_parent(cur, nd2_1, nd2, position=None, auto_position=True) nd2_1 = get_node(cur, nd2_1.id) assert nd2_1.position == 0
def test_change_parent(cur, root, nd1, nd2, nd2_1, nd2_1_1, nd2_leaf): """ Tree layout before move: / - nd1 - nd2 - nd2-1 - nd2-1-1 - nd2-leaf - nd3 Expected tree layout after move: / - nd1 - nd2-1 - nd2-1-1 - nd2-leaf - nd2 - nd3 """ # We expect nd2-1 to be child of nd2 and nd2-1-1 to be child # of nd2-1. # Move nd2-1 from nd2 to nd1 _temp_node = change_parent(cur, nd2_1.id, nd1, auto_position=False) # Return value should have new parent set assert _temp_node.parent == nd1.id # nd2-1 should have nd1 as parent node = get_node(cur, nd2_1.id) assert node.parent == nd1.id # nd2-1-1 should still have the same parent (nd2-1) child_node = get_node(cur, nd2_1_1.id) assert child_node.parent == nd2_1.id # nd2-leaf should still have the same parent (nd2-1-1) child_node = get_node(cur, nd2_leaf.id) assert child_node.parent == nd2_1_1.id # The ancestor set of nd2-1 should now contain nd1 and root assert set(get_ancestor_ids(cur, nd2_1)) == {root.id, nd1.id} # The ancestor set of nd2-1-1 should now contain nd2-1, nd1 and root expected = {root.id, nd1.id, nd2_1.id} assert set(get_ancestor_ids(cur, nd2_1_1)) == expected # The ancestor set of nd2-leaf should now contain node-2-1-1, nd2-1, # nd1 and root expected = {root.id, nd1.id, nd2_1.id, nd2_1_1.id} assert set(get_ancestor_ids(cur, nd2_leaf)) == expected # The ancestor set of nd2 should now only contain root assert set(get_ancestor_ids(cur, nd2)) == {root.id} # Check if nd2-1, nd2-1-1 and nd2-leaf are part of nd1's descendant # set now expected = {nd2_1.id, nd2_1_1.id, nd2_leaf.id} assert set(get_descendant_ids(cur, nd1)) == expected # nd2's descendant set should be empty now assert set(get_descendant_ids(cur, nd2)) == set() # Last but not least, the children function proof what we checked above too assert len(set(get_children(cur, nd1))) == 1 assert len(set(get_children(cur, nd2))) == 0
def test_change_parent_to_highest_position(cur, root, nd2, nd2_1): highest_position = find_highest_position(cur, root) change_parent(cur, nd2_1, root, position=None, auto_position=True) nd2_1 = get_node(cur, nd2_1.id) assert nd2_1.position == highest_position + 1
def test_get_node_non_existing(cur): with pytest.raises(ValueError): get_node(cur, 1)
def test_get_node_needs_number(cur, root): with pytest.raises(TypeError): get_node(cur, root)
def test_set_position(cur, root): set_position(cur, root, 0, auto_position=False) assert get_node(cur, root.id).position == 0