GithubHelp home page GithubHelp logo

uralbash / sqlalchemy_mptt Goto Github PK

View Code? Open in Web Editor NEW
194.0 10.0 30.0 405 KB

SQLAlchemy nested sets mixin (MPTT)

Home Page: http://sqlalchemy-mptt.readthedocs.io

License: MIT License

Python 99.42% Makefile 0.14% Shell 0.44%
python mptt sqlalchemy nested-set

sqlalchemy_mptt's Introduction

Build Status Coverage Status

Library for implementing Modified Preorder Tree Traversal with your SQLAlchemy Models and working with trees of Model instances, like django-mptt. Docs http://sqlalchemy-mptt.readthedocs.io/

Nested sets traversal

The nested set model is a particular technique for representing nested sets (also known as trees or hierarchies) in relational databases.

Installing

Install from github:

pip install git+http://github.com/uralbash/sqlalchemy_mptt.git

PyPi:

pip install sqlalchemy_mptt

Source:

pip install -e .

Usage

Add mixin to model

from sqlalchemy import Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy_mptt.mixins import BaseNestedSets

Base = declarative_base()


class Tree(Base, BaseNestedSets):
    __tablename__ = "tree"

    id = Column(Integer, primary_key=True)
    visible = Column(Boolean)

    def __repr__(self):
        return "<Node (%s)>" % self.id

Now you can add, move and delete obj!

Insert node

node = Tree(parent_id=6)
session.add(node)
level           Nested sets example
1                    1(1)22
        _______________|___________________
       |               |                   |
2    2(2)5           6(4)11             12(7)21
       |               ^                   ^
3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                      |          |
4                                  14(9)15   18(11)19

level     Insert node with parent_id == 6
1                    1(1)24
        _______________|_________________
       |               |                 |
2    2(2)5           6(4)13           14(7)23
       |           ____|____          ___|____
       |          |         |        |        |
3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                           |        |         |
4                      10(23)11  16(9)17  20(11)21

Delete node

node = session.query(Tree).filter(Tree.id == 4).one()
session.delete(node)
level           Nested sets example
1                    1(1)22
        _______________|___________________
       |               |                   |
2    2(2)5           6(4)11             12(7)21
       |               ^                   ^
3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                      |          |
4                                  14(9)15   18(11)19

level         Delete node == 4
1                    1(1)16
        _______________|_____
       |                     |
2    2(2)5                 6(7)15
       |                     ^
3    3(3)4            7(8)10   11(10)14
                        |          |
4                     8(9)9    12(11)13

Update node

node = session.query(Tree).filter(Tree.id == 8).one()
node.parent_id = 5
session.add(node)
level           Nested sets example
    1                    1(1)22
            _______________|___________________
           |               |                   |
    2    2(2)5           6(4)11             12(7)21
           |               ^                   ^
    3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                          |          |
    4                                  14(9)15   18(11)19

level               Move 8 - > 5
    1                     1(1)22
             _______________|__________________
            |               |                  |
    2     2(2)5           6(4)15            16(7)21
            |               ^                  |
    3     3(3)4      7(5)12   13(6)14      17(10)20
                       |                        |
    4                8(8)11                18(11)19
                       |
    5                9(9)10

Move node (support multitree)

Nested sets multitree

Nested sets multitree

Move inside

node = session.query(Tree).filter(Tree.id == 4).one()
node.move_inside("15")
4 -> 15

level Nested sets tree1 1 1(1)16 _______________ | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13

level Nested sets tree2 1 1(12)28 ________________ | | 2 2(13)5 6(15)17 18(18)27 | ^ ^ 3 3(14)4 7(4)12 13(16)14 15(17)16 19(19)22 23(21)26 ^ | | 4 8(5)9 10(6)11 20(20)21 24(22)25

Move after

node = session.query(Tree).filter(Tree.id == 8).one()
node.move_after("5")
level           Nested sets example
     1                    1(1)22
             _______________|___________________
            |               |                   |
     2    2(2)5           6(4)11             12(7)21
            |               ^                   ^
     3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                           |          |
     4                                  14(9)15   18(11)19

 level               Move 8 after 5
     1                     1(1)22
              _______________|__________________
             |               |                  |
     2     2(2)5           6(4)15            16(7)21
             |               ^                  |
     3     3(3)4    7(5)8  9(8)12  13(6)14   17(10)20
                             |                  |
     4                    10(9)11            18(11)19

Move to top level

node = session.query(Tree).filter(Tree.id == 15).one()
node.move_after("1")
level           tree_id = 1
1                    1(1)22
        _______________|___________________
       |               |                   |
2    2(2)5           6(4)11             12(7)21
       |               ^                   ^
3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                      |          |
4                                  14(9)15   18(11)19

level           tree_id = 2
1                     1(15)6
                         ^
2                 2(16)3   4(17)5

level           tree_id = 3
1                    1(12)16
         _______________|
        |               |
2    2(13)5          6(18)15
        |               ^
3    3(14)4     7(19)10   11(21)14
                   |          |
4               8(20)9    12(22)13

Support and Development

To report bugs, use the issue tracker.

We welcome any contribution: suggestions, ideas, commits with new futures, bug fixes, refactoring, docs, tests, translations, etc...

If you have question, contact me [email protected] or #sacrud IRC channel IRC Freenode

License

The project is licensed under the MIT license.

sqlalchemy_mptt's People

Contributors

antoine-gallix avatar bitdeli-chef avatar jirikuncar avatar mush42 avatar ticosax avatar timgates42 avatar trasp avatar uralbash avatar waffle-iron avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sqlalchemy_mptt's Issues

polymorphic identity not work

from sqlalchemy import (
    Column,
    String,
    Integer,
    ForeignKey,
    create_engine
)
from sqlalchemy.orm import create_session
from sqlalchemy.ext.declarative import declarative_base

e = create_engine('sqlite:////tmp/foo.db', echo=True)
Base = declarative_base(bind=e)

from sqlalchemy_mptt import BaseNestedSets


class Employee(Base, BaseNestedSets):
    __tablename__ = 'employees'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(30), nullable=False)

    __mapper_args__ = {'polymorphic_on': type}

    def __init__(self, name):
        self.name = name


class Manager(Employee):
    __tablename__ = 'managers'
    __mapper_args__ = {'polymorphic_identity': 'manager'}

    employee_id = Column(Integer, ForeignKey('employees.id'),
                         primary_key=True)
    manager_data = Column(String(50))

    def __init__(self, name, manager_data):
        super(Manager, self).__init__(name)
        self.manager_data = manager_data


class Owner(Manager):
    __tablename__ = 'owners'
    __mapper_args__ = {'polymorphic_identity': 'owner'}

    employee_id = Column(Integer, ForeignKey('managers.employee_id'),
                         primary_key=True)
    owner_secret = Column(String(50))

    def __init__(self, name, manager_data, owner_secret):
        super(Owner, self).__init__(name, manager_data)
        self.owner_secret = owner_secret

Base.metadata.drop_all()
Base.metadata.create_all()

s = create_session(bind=e, autoflush=True, autocommit=False)
o = Owner('nosklo', 'mgr001', 'ownerpwd')
s.add(o)
s.commit()

raises error

Traceback (most recent call last):
  File "polymorphic.py", line 30, in <module>
    class Manager(Employee):
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/ext/declarative/api.py", line 55, in __init__
    _as_declarative(cls, classname, cls.__dict__)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/ext/declarative/base.py", line 88, in _as_declarative
    _MapperConfig.setup_mapping(cls, classname, dict_)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/ext/declarative/base.py", line 103, in setup_mapping
    cfg_cls(cls_, classname, dict_)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/ext/declarative/base.py", line 131, in __init__
    self._setup_table()
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/ext/declarative/base.py", line 394, in _setup_table
    **table_kw)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 416, in __new__
    metadata._remove_table(name, schema)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/util/langhelpers.py", line 60, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/util/compat.py", line 182, in reraise
    raise value
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 411, in __new__
    table._init(name, metadata, *args, **kw)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 488, in _init
    self._init_items(*args)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 72, in _init_items
    item._set_parent_with_dispatch(self)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/base.py", line 433, in _set_parent_with_dispatch
    self._set_parent(parent)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 3122, in _set_parent
    ColumnCollectionMixin._set_parent(self, table)
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/sql/schema.py", line 2459, in _set_parent
    col = table.c[col]
  File "/home/uralbash/.virtualenvs/sacrud/lib/python3.4/site-packages/SQLAlchemy-1.0.6-py3.4-linux-x86_64.egg/sqlalchemy/util/_collections.py", line 193, in __getitem__
    return self._data[key]
KeyError: 'lft'

Flushing the session should expire the instance and it's children

This test currently fails.

    def test_session_expire(self):
        node = self.session.query(self.model).filter(self.model.ppk == 4).one()
        node.move_after('1')
        self.session.flush()
        # self.session.expire(node)
        self.assertEqual(node.tree_id, 2)
        self.assertEqual(node.parent_id, None)

Expiring the node solves the problem. So shouldn't the after_flush_post_exec listener be expiring those attributes too?
A simpler solution would be to update those values from mptt_before_update.

IntegrityError on attempt to initalize the tree

I'm using sqlalchemy-mptt 0.2.2 and I can't get the sample code to initially create a tree working.

The (almost unmodified from the doc) excerpt:

tree_manager.register_events(remove=True)  # Disable MPTT events
for node in nodes:
    session.add(node)
session.commit()

Node.rebuild(session)  # rebuild lft, rgt value automatically
tree_manager.register_events()  # enabled MPTT events back

is raising an IntegrityError complaining that 'lft' is NULL while it's not authorized on the first commit().
I attempted:

tree_manager.register_events(remove=True)  # Disable MPTT events
for node in nodes:
    node.lft = node.rgt = node.level = node.tree_id = 0
    session.add(node)
session.commit()

Node.rebuild(session  # rebuild lft, rgt value automatically
tree_manager.register_events()  # enabled MPTT events back

but that didn't made the trick either. Everything works fine if I keep events though:

for node in nodes:
    session.add(node)
session.commit()

Am I doing something wrong or is the doc outdated?

BTW, I'm not sure to understand the purpose of the 'tree_id' column. I supposed all nodes below the same root should have the same tree_id, but this doesn't seem to be the case. That may be another story though.

Incompatibility with Django MPTT: level value for root node

Currently Django MPTT assigns level=0 to root nodes, while sqlalchemy_mptt assigns level=1
http://django-mptt.readthedocs.io/en/latest/technical_details.html?highlight=level#level

In our case we have to work with datastructures created in Django from Airflow/SQLAlchemy pipeline. level incompatibility makes it impossible to rely on filtering by level value to find all nodes at given depth.

Are you willing to consider changing starting value of level from 1 to 0 to improve compatibility with Django MPTT?

Expire left/right attributes of parent somewhen after the `before_insert` event.

The following code fails because t0's left and right attributes are set to 1 and 2 before inserting t1, but during insertion of t1, they are changed to 1, 4 in the database only, without reflecting the changes in the parent.

t0 = Tree()
t1 = Tree(parent=t0)

session.add(t0)
session.flush()

assert t0.left == 1
assert t0.right == 4
assert t1.left == 2
assert t1.right == 3

This should be addressed somehow. Currently a session.expire(t0, ['left', 'right']) after session.flush() works, but it would be nice to handle this case automatically.

Compatibility with joined-table inheritance

Currently sqlalchemy-mptt tries to set table indexes in the table_args class method. This make sqlalchemy complains when it tries to construct child classes because it couldn't find columns declared in the base class.
I fixed this in my local setup by adding index=True to column definition, instead of defining indexs in a separate class method.

Should I create a PR to fix this?

Moving node to leftmost position doesn't work

When i'm trying to move node to leftmost position using node.move_before nothing happens with tree.
i think it's because there's no left_sibling here: https://github.com/uralbash/sqlalchemy_mptt/blob/master/sqlalchemy_mptt/events.py#L362

Here's test code:

class MoveLeft(self):
    def test_move_to_leftmost(self):
        self.session.query(self.model).delete()

        _level = self.model.get_default_level()
        pk_column = self.model.get_pk_column()

        self.session.add_all([
            self.model(**{pk_column.name: 1}),
            self.model(**{pk_column.name: 2, 'parent_id': 1}),
            self.model(**{pk_column.name: 3, 'parent_id': 1}),
            self.model(**{pk_column.name: 4, 'parent_id': 1}),
        ])

        # initial tree:
        #        1
        #   /    |   \
        #  2     3    4

        self.assertEqual(
            [
                (1, 1, 8, _level + 0, None, 1),
                (2, 2, 3, _level + 1, 1, 1),
                (3, 4, 5, _level + 1, 1, 1),
                (4, 6, 7, _level + 1, 1, 1)
            ],
            self.result.all()
        )

        # move 4 to left
        node4 = self.session.query(self.model).filter(pk_column == 4).one()
        node4.move_before("2")

        # expected result:
        #        1
        #   /    |   \
        #  4     2    3

        self.assertEqual(
            [
                (1, 1, 8, _level + 0, None, 1),
                (2, 4, 5, _level + 0, 1, 1),
                (3, 6, 7, _level + 0, 1, 1),
                (4, 2, 3, _level + 0, 1, 1)
            ],
            self.result.all()
        )

How to apply soft-delete for a node deletion?

Hi,

I need to apply soft-delete when doing a node deletion. I will use an extra field named: deleted with its value 1 char Y for the row is soft-deleted and 1 char N forr the row is not soft-deleted.

I don't want to hard delete using real SQL DELETE command. because hard / real DELETE will make worst performance in database.

Please Enligthenment

Thank you

when update node, it move to left side in subtree

MPTTPages(1, 1, 36, 3)
▼MPTTPages(12, 2, 15, 3)
    MPTTPages(13, 3, 4, 3)
    MPTTPages(15, 5, 6, 3)
    ▼MPTTPages(18, 7, 14, 3)
        MPTTPages(22, 8, 9, 3)
        ▼MPTTPages(21, 10, 13, 3)
            MPTTPages(19, 11, 12, 3)

if update 15 w/o move

MPTTPages(1, 1, 36, 3)
▼MPTTPages(12, 2, 15, 3)
    MPTTPages(15, 3, 4, 3)
    MPTTPages(13, 5, 6, 3)
    ▼MPTTPages(18, 7, 14, 3)
        MPTTPages(22, 8, 9, 3)
        ▼MPTTPages(21, 10, 13, 3)
            MPTTPages(19, 11, 12, 3)

Package is not installable if sqlalchemy is not (yet) installed

When installing using a pip requirements file, listing both sqlalchemy and sqlalchemy_mptt, the installation fails because when pip runs python setup.py egg_info for the sqlalchemy_mptt package, an exception is raised about sqlalchemy not being found.

This is due to the setup.py file importing __version__ from the package, and thus indirectly trying to import sqlalchemy as well.

In such cases I use the following approach in my setup.py files:

data = Setup.read(os.path.join('sqlalchemy_mptt', '__init__.py'))
__version__ = (re.search(u"__version__\s*=\s*u?'([^']+)'", data).group(1).strip())

(I know some people don't like this approach, but while not that elegant, it is simple enough to work in all cases I had to use it).

linear structure

Mptt is a great project,
It helps a lot of junior programmers to realize the connection between tree structure and database.
Maybe we can build another project about linear structure.
Is there such a plan?

Allow the primary key to not be named "id"

Currently, the code makes the assumption that there is a field called "id". I have a model that looks a little like:

class MyModel(Model):
    pk = Column('id', Integer, primary_key = True)
    ...

Reason being that I don't like to override python built-ins.

Doesn't work on SQLAlchemy version 1.0

Class definition seems to be failing on working code while upgrading to SQLAlchemy==1.0.

  File "/home/fayaz/Programming/weaver-backend/weaver/core/model/item.py", line 199, in <module>
    class ItemCategory(Base, BaseNestedSets):                                                                                         
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/ext/declarative/api.py", line 55, in __init__
    _as_declarative(cls, classname, cls.__dict__)                                                                                     
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/ext/declarative/base.py", line 87, in _as_declarative                                                                                                                               
    _MapperConfig.setup_mapping(cls, classname, dict_)                                                                                
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/ext/declarative/base.py", line 102, in setup_mapping                                                                                                                                
    cfg_cls(cls_, classname, dict_)                                                                                                   
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/ext/declarative/base.py", line 130, in __init__                                                                                                                                     
    self._setup_table()                                                                                                               
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/ext/declarative/base.py", line 392, in _setup_table                                                                                                                                 
    **table_kw)                                                                                                                       
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 416, in __new__
    metadata._remove_table(name, schema)                                                                                              
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/util/langhelpers.py", line 60, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)                                                                                       
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 411, in __new__
    table._init(name, metadata, *args, **kw)                                                                                          
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 488, in _init
    self._init_items(*args)                                                                                                           
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 72, in _init_items
    item._set_parent_with_dispatch(self)                                                                                              
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/base.py", line 434, in _set_parent_with_dispatch                                                                                                                                
    self.dispatch.after_parent_attach(self, parent)                                                                                   
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/event/attr.py", line 258, in __call__
    fn(*args, **kw)                                                                                                                   
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 2411, in _col_attached
    self._check_attach(evt=True)                                                                                                      
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 2421, in _check_attach
    self._set_parent_with_dispatch(tables.pop())                                                                                      
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/base.py", line 433, in _set_parent_with_dispatch                                                                                                                                
    self._set_parent(parent)                                                                                                          
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 2468, in _set_parent
    ColumnCollectionMixin._set_parent(self, table)                                                                                    
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 2435, in _set_parent
    col = table.c[col]                                                                                                                
  File "/home/fayaz/Programming/weaver-env/local/lib/python2.7/site-packages/sqlalchemy/util/_collections.py", line 191, in __getitem__                                                                                                                                     
    return self._data[key]                                                                                                            
KeyError: u'parent_id'

fix tree shorting

    print """ level           Nested sets example
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level           Nested sets example

                                __parent_id______________________
                               |                                 |
        1                    1(1)22                              |
                _______________|___________________              |
               |               |                   |             |
        2    2(2)5           6(4)11             12(7)21          |
               |               ^                   ^             |
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20    |
                                              |          |       |
        4                                  14(9)15   18(11)19    |
                                                         |       |
                                                         |_______|
                    id lft rgt lvl parent tree
    """

UnicodeDecodeError when running setup.py

I'm getting this UnicodeDecodeError when trying to run setup.py (with or without pip).

Traceback (most recent call last):
 File "setup.py", line 7, in <module>
    __version__ = (re.search(r'__version__\s*=\s*u?"([^"]+)"', fh.read())
 File "/usr/lib/python3.4/encodings/ascii.py", line 26, in decode
     return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 78: ordinal not in range(128)

Path To Root Feature Request

sqla_mptt has been working great for me. My use case, however, requires the ability to get the path from any node to the root. Since I am using mptt for read speed it seemed silly for me to iterate up the parents as it is essentially the same as using adjacency lists. Instead, I made a query method to get all of the nodes to the root using a single query.

It seemed like this was a good candidate to be part of the implementation of BaseNestedSets.

    def path_to_root(self):
        """Generate path from a leaf or intermediate node to the root.

        For example:

            node11.path_to_root()

            .. code::

                level           Nested sets example

                                 -----------------------------------------
                1               |    1(1)22                               |
                        ________|______|_____________________             |
                       |        |      |                     |            |
                       |          -----+---------            |            |
                2    2(2)5           6(4)11      | --     12(7)21         |
                       |               ^             |    /     \         |
                3    3(3)4       7(5)8   9(6)10      ---/----    \        |
                                                    13(8)16 |  17(10)20   |
                                                       |    |     |       |
                4                                   14(9)15 | 18(11)19    |
                                                            |             |
                                                             -------------
        """
        Node = self.__class__
        ParentNode = aliased(Node)
        session = object_session(self)

        query = (session.query(ParentNode).select_from(ParentNode)
                 .join(Node,
                       and_(Node.left.between(ParentNode.left, ParentNode.right),
                            Node.id==self.id))
                 .order_by(Node.left)
                 )

        return query

Feature Request: Implement Child and sibling methods

Coming from django_mptt, I miss the feature of being able to retrieve simply the children/siblings of the node. Both of these features are tightly linked as the siblings of a node are it's parent's children. I am working with a hierarchy of pages and want the user to only see a layer down from the current node. Although it is possible to write methods to filter out a drilldown tree I suspect that there is a more efficient way of preforming this operation.

I am really glad that this plugin exists for flask and would love to help in any way that I can.

better initialization of empty tree

Hi!

In order to initialize an empty tree. I used the following code based on the example in the doc:

    # if the table is empty
    if not current_session.query(TreeDelcaretiveClass).all():
        tree_manager.register_events(remove=True)
        # Hard code the tree_id since I know this is the first node and the only root node in the current tree
        root = TreeDelcaretiveClass(name="Root", tree_id=1)
        # Hard code the left since I know the initial value for the left must be 1
        root.left = 1
        # Hard code the right since I know the initial value for the right must be 2
        root.right = 2
        current_session.add(root)
        current_session.commit()

        tree_manager.register_events()
        TreeDeclaretiveClass.rebuild_tree(current_session, TreeDeclaretive.tree_id)

Maybe this can be a function in the mixin module that help user initialize the empty tree?
With at least one item in the tree (even if it is not used), tree will be very easy to use afterward.

Automatically register tree classes

Tree classes can automatically be registered by adding the following code to the mixin class:

@classmethod
def __declare_last__(cls):
    cls.register_tree()

This would avoid to have to call Tree.register_tree() after each declaration of Tree.

SQLAlchemy IntegrityError when trying to load data to database without events

Hey. Nice module.

I'm trying to load a huge amount of data into PostgreSQL via SQLAlchemy, and I want to use it with MPTT, so I searched and found your module. But I got stuck with a problem.

Here's my model:

from sqlalchemy_mptt.mixins import BaseNestedSets
from app import db

class MyModelTree(db.Model, BaseNestedSets):
    __tablename__ = "my_model_tree"
    id = db.Column(db.Integer, primary_key=True)
    code = db.Column(db.String(100), index=True, unique=True)
    name = db.Column(db.String(512), index=True)
    code_parent = db.Column(db.String(100), index=True)

And here's a part of my upload to database script:

from sqlalchemy_mptt import tree_manager
...
for event, element in etree.iterparse(filepath, tag="XML_ObjectFields"):
    item = models.MyModelTree()
    item.code = element.find('code').text
    item.name = element.find('name').text
    if element.find('code_parent') is not None:
        item.code_parent = element.find('code_parent').text
        parent = models.MyModelTree.query.filter_by(code=element.find('code_parent').text).first()
        if parent:
            item.parent_id = parent.id
    db.session.add(item)
    element.clear()
db.session.commit()
...
tree_manager.register_events()

And here's the error:

Traceback (most recent call last):
  File "upload_data.py", line 48, in <module>
    parent = models.MyModelTree.query.filter_by(code=element.find('code_parent').text).first()
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\query.py", line 2634, in first
    ret = list(self[0:1])
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\query.py", line 2457, in __getitem__
    return list(res)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\query.py", line 2735, in __iter__
    self.session._autoflush()
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 1303, in _autoflush
    util.raise_from_cause(e)
  File "C:\Python27\lib\site-packages\sqlalchemy\util\compat.py", line 200, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 1293, in _autoflush
    self.flush()
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2019, in flush
    self._flush(objects)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2137, in _flush
    transaction.rollback(_capture_exception=True)
  File "C:\Python27\lib\site-packages\sqlalchemy\util\langhelpers.py", line 60, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2101, in _flush
    flush_context.execute()
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 373, in execute
    rec.execute(self)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 532, in execute
    uow
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 139, in save_obj
    save_obj(base_mapper, [state], uowtransaction, single=True)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 174, in save_obj
    mapper, table, insert)
  File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 800, in _emit_insert_statements
    execute(statement, params)
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 914, in execute
    return meth(self, multiparams, params)
  File "C:\Python27\lib\site-packages\sqlalchemy\sql\elements.py", line 323, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1010, in _execute_clauseelement
    compiled_sql, distilled_params
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1146, in _execute_context
    context)
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1341, in _handle_dbapi_exception
    exc_info
  File "C:\Python27\lib\site-packages\sqlalchemy\util\compat.py", line 200, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1139, in _execute_context
    context)
  File "C:\Python27\lib\site-packages\sqlalchemy\engine\default.py", line 450, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.IntegrityError: (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely) (psycopg2.IntegrityError) null value in column "rgt" violates not-null constraint
DETAIL:  Failing row contains (1, 00000000000, ╨Ю╨▒╤К╨╡╨║╤В╤Л ╨░╨┤╨╝╨╕╨╜╨╕╤Б╤В╤А╨░╤В╨╕╨▓╨╜╨╛-╤В╨╡╤А╤А╨╕╤В╨╛╤А╨╕..., null, null, null, 0, null, null).
 [SQL: 'INSERT INTO my_model_tree (code, name, code_parent, rgt, lft, level, parent_id, tree_id) VALUES (%(code)s, %(name)s, %(code_parent)s, %(rgt)s, %(lft)s, %(level)s, %(parent_id)s, %(tree_id)s) RETURNING my_model_tree.id'] [parameters: {'code': '00000000000', 'rgt': None, 'name': u'\u041e\u0431\u044a\u0435\u043a\u0442\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u0438\u0432\u043d\u043e-\u0442\u0435\u0440\u0440\u0438\u0442\u043e\u0440\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u0435\u043b\u0435\u043d\u0438\u044f,^ \u043a\u0440\u043e\u043c\u0435 \u0441\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043d\u0430\u0441\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u043f\u0443\u043d\u043a\u0442\u043e\u0432', 'parent_id': None, 'level': 0, 'lft': None, 'code_parent': None, 'tree_id': None}]

Seems like it does not generate rgt/lft values when events are off. Should I set them to any random number instead? Cause I will rebuild the tree after I finish uploading all data anyways. Or is there any other way to first insert all the data, and only then count mptt-tree values?

Thanx

P.S: Do I rebuild values like this?
models.MyModelTree.rebuild_tree(db.session, models.MyModelTree.tree_id)

Updating nodes without an existing parent does not work

There appears to be an issue when adding a parent ID to an existing node that doesn’t already have a parent ID, in the same tree. The use case is like so:

node_1 # a node that has no parent, but valid left and right values. so it is a root and a leaf
parent_node # another node that we will be making into the parent node for node_1

node_1.parent_id = parent_node.id
session.add(node_1)
session.commit()

The tree_id is the same for both nodes. Once this is done, node_1 does not get updated with the new parent ID. The parent ID still remains null, and the left and right values are the same.

This issue happens only in nodes where they don’t have an existing parent_id. So, whenever we try to update such nodes by giving them a parent_id, the update fails and the nodes remain the same.

Support polymorphic tree models

Implementing trees where nodes can be inherited (using joined table inheritance, in my case) is currently not supported and raises different errors.

It would be great if node classes could be extended and loaded polymorphically.

Dynamic query

Drilldowntree return a list but not a query, returning a query can make the query easier

move_after moves node too far right

move_after moves element one position further than expected.
I think it's because after we called mptt_before_delete here, lft and rgt of left sibling has changed, but variable left_sibling still has old values which lead to incorrect values of delta_lft and delta_rgt in _insert_subtree

Here's test code:

class MoveRight(object):
    def test_move_right(self):
        self.session.query(self.model).delete()

        _level = self.model.get_default_level()
        pk_column = self.model.get_pk_column()

        self.session.add_all([
            self.model(**{pk_column.name: 1}),
            self.model(**{pk_column.name: 2, 'parent_id': 1}),
            self.model(**{pk_column.name: 3, 'parent_id': 1}),
            self.model(**{pk_column.name: 4, 'parent_id': 1}),
        ])

        # initial tree:
        #        1
        #   /    |   \
        #  2     3    4

        self.assertEqual(
            [
                (1, 1, 8, _level + 0, None, 1),
                (2, 2, 3, _level + 1, 1, 1),
                (3, 4, 5, _level + 1, 1, 1),
                (4, 6, 7, _level + 1, 1, 1)
            ],
            self.result.all()
        )

        # move 2 to right
        node2 = self.session.query(self.model).filter(pk_column == 2).one()
        node2.move_after("3")

        # expected result:
        #        1
        #   /    |   \
        #  3     2    4

        self.assertEqual(
            [
                (1, 1, 8, _level + 0, None, 1),
                (2, 4, 5, _level + 1, 1, 1),
                (3, 2, 3, _level + 1, 1, 1),
                (4, 6, 7, _level + 1, 1, 1)
            ],
            self.result.all()
        )

Reading the tree

Maybe I misunderstand, but the only method that I see for actually reading the tree, BaseNestedSets.get_tree, doesn't actually use the MPTT information, but simply executes a lot of queries.

I'd love to see some helpers for actually querying the tree.

null value in column \"lft\" violates not-null constraint

I am using this library for category table in my fastapi application. When I try to create a category, I get null value in column "lft" violates not-null constraint error. Here is the code setup

from sqlalchemy_mptt.mixins import BaseNestedSets

class Category(Base, BaseNestedSets, TimestampMixin):
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(255), nullable=False)
    slug = Column(String, index=True, unique=True)
    description = Column(Text())
    # parent_id = Column(Integer(), default=0)
    products = relationship("Product", back_populates="category")
    background_img = Column(String(255))

    def __init__(self, *args, **kwargs) -> None:
        generate_slug(self, sluggable_column="title", *args, **kwargs)
        super().__init__(*args, **kwargs)

    def __str__(self):
        return f"<Category {self.title}>"

class Product(Base, TimestampMixin):
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(255), nullable=False)
    slug = Column(String, index=True, unique=True)
    description = Column(Text())
    # to establish a bidirectional relationship(many to one) through relationship and back_ref
    category_id = Column(Integer, ForeignKey("category.id"))
    category = relationship("Category", back_populates="products")


async def resolve_create_category(db, data, parent_id):
    print("parent_id", data, parent_id)
    qs = Category.__table__.select().where(Category.__table__.c.title == data.title)
    category = await db.fetch_one(query=qs)
    print('category', category)
    if category:
        return CategoryCreatePayload(
            category=None,
            errors=[
                Error(
                    code="CATEGORY_ALREADY_EXIST",
                    message=f"Category with title {data.title} already exist",
                )
            ],
        )
    # check if parent id is sent
    if parent_id:
      query = Category.__table__.insert().values(
        title=data.title,
        description=data.description
    )
    # otherwise root value
    else:
      query = Category.__table__.insert().values(
          parent_id=parent_id,
          title=data.title,
          description=data.description
      )
    print("query", query)
    category_id = await db.execute(query)
    print("category_id", category_id)
    response_payload = {**data.__dict__, "id": category_id}
    print("response_payload", response_payload)
    return CategoryCreatePayload(category=Category(**response_payload))

print("parent_id", data, parent_id) gives

parent_id CategoryCreateInput(title="Men's", slug=None, description='Mens category') None

this is what printed by print('query', query) statement

query INSERT INTO category (created_at, updated_at, title, description, level, parent_id) VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, :title, :description, :level, :parent_id)

It could not reach up to the line print("category_id", category_id)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.