Ejemplo n.º 1
0
 def _inc_path(self):
     """:returns: The path of the next sibling of a given node path."""
     newpos = self._str2int(self.path[-self.steplen:]) + 1
     key = self._int2str(newpos)
     if len(key) > self.steplen:
         raise PathOverflow(_("Path Overflow from: '%s'" % (self.path, )))
     return '%s%s%s' % (self.path[:-self.steplen], '0' *
                        (self.steplen - len(key)), key)
Ejemplo n.º 2
0
 def _inc_path(cls, path):
     """:returns: The path of the next sibling of a given node path."""
     newpos = cls._str2int(path[-cls.steplen:]) + 1
     key = cls._int2str(newpos)
     if len(key) > cls.steplen:
         raise PathOverflow(_("Path Overflow from: '%s'" % (path, )))
     return '%s%s%s' % (path[:-cls.steplen], '0' *
                        (cls.steplen - len(key)), key)
Ejemplo n.º 3
0
    def add_child(self, **kwargs):
        """
        Adds a child to the node.

        :raise PathOverflow: when no more child nodes can be added
        """

        if not self.is_leaf() and self.node_order_by:
            # there are child nodes and node_order_by has been set
            # delegate sorted insertion to add_sibling
            # we increase the numchild value of the object in memory, but can't
            self.numchild += 1
            return self.get_last_child().add_sibling('sorted-sibling',
                                                     **kwargs)

        # creating a new object
        newobj = self.__class__(**kwargs)
        newobj.depth = self.depth + 1
        if not self.is_leaf():
            # adding the new child as the last one
            newobj.path = self._inc_path(self.get_last_child().path)
        else:
            # the node had no children, adding the first child
            newobj.path = self._get_path(self.path, newobj.depth, 1)
            if len(newobj.path) > \
                    newobj.__class__._meta.get_field('path').max_length:
                raise PathOverflow(
                    _('The new node is too deep in the tree, try'
                      ' increasing the path.max_length property'
                      ' and UPDATE your database'))
        # saving the instance before returning it
        newobj.save()
        newobj._cached_parent_obj = self

        # we increase the numchild value of the object in memory, but can't
        # save because that makes this django 1.0 compatible code explode
        self.numchild += 1

        # we need to use a raw query
        sql = "UPDATE %(table)s " \
                 "SET numchild=numchild+1 " \
               "WHERE path=%%s" % {
                 'table': connection.ops.quote_name(
                              self.__class__._meta.db_table)}
        cursor = connection.cursor()
        cursor.execute(sql, [self.path])
        transaction.commit_unless_managed()

        return newobj
Ejemplo n.º 4
0
    def process(self):
        if self.node_cls.node_order_by and not self.node.is_leaf():
            # there are child nodes and node_order_by has been set
            # delegate sorted insertion to add_sibling
            self.node.numchild += 1
            return self.node.get_last_child().add_sibling(
                'sorted-sibling', **self.kwargs)

        if len(self.kwargs) == 1 and 'instance' in self.kwargs:
            # adding the passed (unsaved) instance to the tree
            newobj = self.kwargs['instance']
            if newobj.pk:
                raise NodeAlreadySaved("Attempted to add a tree node that is "\
                    "already in the database")
        else:
            # creating a new object
            newobj = self.node_cls(**self.kwargs)

        newobj.depth = self.node.depth + 1
        if self.node.is_leaf():
            # the node had no children, adding the first child
            newobj.path = self.node_cls._get_path(
                self.node.path, newobj.depth, 1)
            max_length = self.node_cls._meta.get_field('path').max_length
            if len(newobj.path) > max_length:
                raise PathOverflow(
                    _('The new node is too deep in the tree, try'
                      ' increasing the path.max_length property'
                      ' and UPDATE your database'))
        else:
            # adding the new child as the last one
            newobj.path = self.node.get_last_child()._inc_path()

        get_result_class(self.node_cls).objects.filter(
            path=self.node.path).update(numchild=F('numchild')+1)

        # we increase the numchild value of the object in memory
        self.node.numchild += 1

        # saving the instance before returning it
        newobj._cached_parent_obj = self.node
        newobj.save()

        return newobj
Ejemplo n.º 5
0
    def add_child_bulk(self, parent, node_data):
        # @@@ forked version of `Node._inc_path`
        # https://github.com/django-treebeard/django-treebeard/blob/master/treebeard/mp_tree.py#L1121
        child_node = Node(**node_data)
        child_node.depth = parent.depth + 1

        last_child = self.node_last_child_lookup.get(parent.urn)
        if not last_child:
            # The node had no children, adding the first child.
            child_node.path = Node._get_path(parent.path, child_node.depth, 1)
            if self.check_depth(child_node.path):
                raise PathOverflow(
                    ugettext_noop("The new node is too deep in the tree, try"
                                  " increasing the path.max_length property"
                                  " and UPDATE your database"))
        else:
            # Adding the new child as the last one.
            child_node.path = last_child._inc_path()
        self.node_last_child_lookup[parent.urn] = child_node
        self.nodes_to_create.append(child_node)
        return child_node
Ejemplo n.º 6
0
    def process(self):
        if self.node.object_id != self.kwargs.get('object_id', False):
            raise KeyError(
                "The object_id for parent and child must be the same")

        if self.node_cls.node_order_by and not self.node.is_leaf():
            # there are child nodes and node_order_by has been set
            # delegate sorted insertion to add_sibling
            self.node.numchild += 1
            return self.node.get_last_child().add_sibling(
                'sorted-sibling', **self.kwargs)

        # creating a new object
        newobj = self.node_cls(**self.kwargs)
        newobj.depth = self.node.depth + 1
        if self.node.is_leaf():
            # the node had no children, adding the first child
            newobj.path = self.node_cls._get_path(self.node.path, newobj.depth,
                                                  1)
            max_length = self.node_cls._meta.get_field('path').max_length
            if len(newobj.path) > max_length:
                raise PathOverflow(
                    _('The new node is too deep in the tree, try'
                      ' increasing the path.max_length property'
                      ' and UPDATE your database'))
        else:
            # adding the new child as the last one
            newobj.path = self.node.get_last_child()._inc_path()
        # saving the instance before returning it
        newobj.save()
        newobj._cached_parent_obj = self.node

        self.node_cls.objects.filter(
            path=self.node.path,
            object_id=self.node.object_id).update(numchild=F('numchild') + 1)

        # we increase the numchild value of the object in memory
        self.node.numchild += 1
        transaction.commit_unless_managed()
        return newobj
Ejemplo n.º 7
0
    def add_child(self, **kwargs):
        """
        Adds a child to the node.

        :raise PathOverflow: when no more child nodes can be added
        """

        if not self.is_leaf() and self.node_order_by:
            # there are child nodes and node_order_by has been set
            # delegate sorted insertion to add_sibling
            self.numchild += 1
            return self.get_last_child().add_sibling('sorted-sibling',
                                                     **kwargs)

        # creating a new object
        newobj = self.__class__(**kwargs)
        newobj.depth = self.depth + 1
        if not self.is_leaf():
            # adding the new child as the last one
            newobj.path = self._inc_path(self.get_last_child().path)
        else:
            # the node had no children, adding the first child
            newobj.path = self._get_path(self.path, newobj.depth, 1)
            max_length = newobj.__class__._meta.get_field('path').max_length
            if len(newobj.path) > max_length:
                raise PathOverflow(
                    _('The new node is too deep in the tree, try'
                      ' increasing the path.max_length property'
                      ' and UPDATE your database'))
            # saving the instance before returning it
        newobj.save()
        newobj._cached_parent_obj = self

        self.__class__.objects.filter(path=self.path).update(
            numchild=F('numchild') + 1)

        # we increase the numchild value of the object in memory
        self.numchild += 1
        transaction.commit_unless_managed()
        return newobj
Ejemplo n.º 8
0
    def fix_tree(cls, fix_paths=False, **kwargs):
        super().fix_tree(**kwargs)

        if fix_paths:
            with transaction.atomic():
                # To fix holes and mis-orderings in paths, we consider each non-leaf node in turn
                # and ensure that its children's path values are consecutive (and in the order
                # given by node_order_by, if applicable). children_to_fix is a queue of child sets
                # that we know about but have not yet fixed, expressed as a tuple of
                # (parent_path, depth). Since we're updating paths as we go, we must take care to
                # only add items to this list after the corresponding parent node has been fixed
                # (and is thus not going to change).

                # Initially children_to_fix is the set of root nodes, i.e. ones with a path
                # starting with '' and depth 1.
                children_to_fix = [('', 1)]

                while children_to_fix:
                    parent_path, depth = children_to_fix.pop(0)

                    children = cls.objects.filter(path__startswith=parent_path,
                                                  depth=depth).values(
                                                      'pk', 'path', 'depth',
                                                      'numchild')

                    desired_sequence = children.order_by(
                        *(cls.node_order_by or ['path']))

                    # mapping of current path position (converted to numeric) to item
                    actual_sequence = {}

                    # highest numeric path position currently in use
                    max_position = None

                    # loop over items to populate actual_sequence and max_position
                    for item in desired_sequence:
                        actual_position = cls._str2int(
                            item['path'][-cls.steplen:])
                        actual_sequence[actual_position] = item
                        if max_position is None or actual_position > max_position:
                            max_position = actual_position

                    # loop over items to perform path adjustments
                    for (i, item) in enumerate(desired_sequence):
                        desired_position = i + 1  # positions are 1-indexed
                        actual_position = cls._str2int(
                            item['path'][-cls.steplen:])
                        if actual_position == desired_position:
                            pass
                        else:
                            # if a node is already in the desired position, move that node
                            # to max_position + 1 to get it out of the way
                            occupant = actual_sequence.get(desired_position)
                            if occupant:
                                old_path = occupant['path']
                                max_position += 1
                                new_path = cls._get_path(
                                    parent_path, depth, max_position)
                                if len(new_path) > len(old_path):
                                    previous_max_path = cls._get_path(
                                        parent_path, depth, max_position - 1)
                                    raise PathOverflow(
                                        "Path Overflow from: '%s'" %
                                        (previous_max_path, ))

                                cls._rewrite_node_path(old_path, new_path)
                                # update actual_sequence to reflect the new position
                                actual_sequence[max_position] = occupant
                                del (actual_sequence[desired_position])
                                occupant['path'] = new_path

                            # move item into the (now vacated) desired position
                            old_path = item['path']
                            new_path = cls._get_path(parent_path, depth,
                                                     desired_position)
                            cls._rewrite_node_path(old_path, new_path)
                            # update actual_sequence to reflect the new position
                            actual_sequence[desired_position] = item
                            del (actual_sequence[actual_position])
                            item['path'] = new_path

                        if item['numchild']:
                            # this item has children to process, and we have now moved the parent
                            # node into its final position, so it's safe to add to children_to_fix
                            children_to_fix.append((item['path'], depth + 1))
Ejemplo n.º 9
0
    def fix_tree(cls, destructive=False, fix_paths=False):
        """
        Solves some problems that can appear when transactions are not used and
        a piece of code breaks, leaving the tree in an inconsistent state.

        The problems this method solves are:

           1. Nodes with an incorrect ``depth`` or ``numchild`` values due to
              incorrect code and lack of database transactions.
           2. "Holes" in the tree. This is normal if you move/delete nodes a
              lot. Holes in a tree don't affect performance,
           3. Incorrect ordering of nodes when ``node_order_by`` is enabled.
              Ordering is enforced on *node insertion*, so if an attribute in
              ``node_order_by`` is modified after the node is inserted, the
              tree ordering will be inconsistent.

        :param fix_paths:

            A boolean value. If True, a slower, more complex fix_tree method
            will be attempted. If False (the default), it will use a safe (and
            fast!) fix approach, but it will only solve the ``depth`` and
            ``numchild`` nodes, it won't fix the tree holes or broken path
            ordering.

        :param destructive:

            Deprecated; alias for ``fix_paths``.
        """
        cls = get_result_class(cls)
        vendor = cls.get_database_vendor('write')

        cursor = cls._get_database_cursor('write')

        # fix the depth field
        # we need the WHERE to speed up postgres
        sql = (
            "UPDATE %s "
            "SET depth=" + sql_length("path", vendor=vendor) + "/%%s "
            "WHERE depth!=" + sql_length("path", vendor=vendor) + "/%%s"
        ) % (connection.ops.quote_name(cls._meta.db_table), )
        vals = [cls.steplen, cls.steplen]
        cursor.execute(sql, vals)

        # fix the numchild field
        vals = ['_' * cls.steplen]
        # the cake and sql portability are a lie
        if cls.get_database_vendor('read') == 'mysql':
            sql = (
                "SELECT tbn1.path, tbn1.numchild, ("
                "SELECT COUNT(1) "
                "FROM %(table)s AS tbn2 "
                "WHERE tbn2.path LIKE " +
                sql_concat("tbn1.path", "%%s", vendor=vendor) + ") AS real_numchild "
                "FROM %(table)s AS tbn1 "
                "HAVING tbn1.numchild != real_numchild"
            ) % {'table': connection.ops.quote_name(cls._meta.db_table)}
        else:
            subquery = "(SELECT COUNT(1) FROM %(table)s AS tbn2"\
                        " WHERE tbn2.path LIKE " + sql_concat("tbn1.path", "%%s", vendor=vendor) + ")"
            sql = ("SELECT tbn1.path, tbn1.numchild, " + subquery +
                    " FROM %(table)s AS tbn1 WHERE tbn1.numchild != " +
                    subquery)
            sql = sql % {
                'table': connection.ops.quote_name(cls._meta.db_table)}
            # we include the subquery twice
            vals *= 2
        cursor.execute(sql, vals)
        sql = "UPDATE %(table)s "\
                "SET numchild=%%s "\
                "WHERE path=%%s" % {
                    'table': connection.ops.quote_name(cls._meta.db_table)}
        for node_data in cursor.fetchall():
            vals = [node_data[2], node_data[0]]
            cursor.execute(sql, vals)

        if fix_paths or destructive:
            with transaction.atomic():
                # To fix holes and mis-orderings in paths, we consider each non-leaf node in turn
                # and ensure that its children's path values are consecutive (and in the order
                # given by node_order_by, if applicable). children_to_fix is a queue of child sets
                # that we know about but have not yet fixed, expressed as a tuple of
                # (parent_path, depth). Since we're updating paths as we go, we must take care to
                # only add items to this list after the corresponding parent node has been fixed
                # (and is thus not going to change).

                # Initially children_to_fix is the set of root nodes, i.e. ones with a path
                # starting with '' and depth 1.
                children_to_fix = [('', 1)]

                while children_to_fix:
                    parent_path, depth = children_to_fix.pop(0)

                    children = cls.objects.filter(
                        path__startswith=parent_path, depth=depth
                    ).values('pk', 'path', 'depth', 'numchild')

                    desired_sequence = children.order_by(*(cls.node_order_by or ['path']))

                    # mapping of current path position (converted to numeric) to item
                    actual_sequence = {}

                    # highest numeric path position currently in use
                    max_position = None

                    # loop over items to populate actual_sequence and max_position
                    for item in desired_sequence:
                        actual_position = cls._str2int(item['path'][-cls.steplen:])
                        actual_sequence[actual_position] = item
                        if max_position is None or actual_position > max_position:
                            max_position = actual_position

                    # loop over items to perform path adjustments
                    for (i, item) in enumerate(desired_sequence):
                        desired_position = i + 1  # positions are 1-indexed
                        actual_position = cls._str2int(item['path'][-cls.steplen:])
                        if actual_position == desired_position:
                            pass
                        else:
                            # if a node is already in the desired position, move that node
                            # to max_position + 1 to get it out of the way
                            occupant = actual_sequence.get(desired_position)
                            if occupant:
                                old_path = occupant['path']
                                max_position += 1
                                new_path = cls._get_path(parent_path, depth, max_position)
                                if len(new_path) > len(old_path):
                                    previous_max_path = cls._get_path(parent_path, depth, max_position - 1)
                                    raise PathOverflow(_("Path Overflow from: '%s'" % (previous_max_path, )))

                                cls._rewrite_node_path(old_path, new_path)
                                # update actual_sequence to reflect the new position
                                actual_sequence[max_position] = occupant
                                del(actual_sequence[desired_position])
                                occupant['path'] = new_path

                            # move item into the (now vacated) desired position
                            old_path = item['path']
                            new_path = cls._get_path(parent_path, depth, desired_position)
                            cls._rewrite_node_path(old_path, new_path)
                            # update actual_sequence to reflect the new position
                            actual_sequence[desired_position] = item
                            del(actual_sequence[actual_position])
                            item['path'] = new_path

                        if item['numchild']:
                            # this item has children to process, and we have now moved the parent
                            # node into its final position, so it's safe to add to children_to_fix
                            children_to_fix.append((item['path'], depth + 1))