Skip to content

Commit de982db

Browse files
committed
Merge pull request #50 from Klupamos/master
QOL changes
2 parents 1ffd8ea + 8468fbd commit de982db

2 files changed

Lines changed: 61 additions & 7 deletions

File tree

sqlalchemy_mptt/mixins.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sqlalchemy.orm import backref, relationship, object_session
1414
from sqlalchemy.orm.session import Session
1515
from sqlalchemy.ext.declarative import declared_attr
16+
from sqlalchemy.ext.hybrid import hybrid_method
1617

1718
from .events import _get_tree_table
1819

@@ -72,7 +73,7 @@ def parent_id(cls):
7273
if not pk.name:
7374
pk.name = cls.get_pk_name()
7475

75-
return Column("parent_id", Integer,
76+
return Column("parent_id", pk.type,
7677
ForeignKey('%s.%s' % (cls.__tablename__, pk.name),
7778
ondelete='CASCADE'))
7879

@@ -99,6 +100,30 @@ def right(cls):
99100
def level(cls):
100101
return Column("level", Integer, nullable=False, default=0)
101102

103+
@hybrid_method
104+
def is_ancestor_of(self, other, inclusive=False):
105+
""" class or instance level method which returns True if self is ancestor (closer to root) of other else False.
106+
Optional flag `inclusive` on whether or not to treat self as ancestor of self.
107+
108+
For example see:
109+
110+
* :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure`
111+
"""
112+
if inclusive:
113+
return (self.tree_id == other.tree_id) & (self.left <= other.left) & (other.right <= self.right)
114+
return (self.tree_id == other.tree_id) & (self.left < other.left) & (other.right < self.right)
115+
116+
@hybrid_method
117+
def is_descendant_of(self, other, inclusive=False):
118+
""" class or instance level method which returns True if self is descendant (farther from root) of other else False.
119+
Optional flag `inclusive` on whether or not to treat self as descendant of self.
120+
121+
For example see:
122+
123+
* :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure`
124+
"""
125+
return other.is_ancestor_of(self, inclusive)
126+
102127
def move_inside(self, parent_id):
103128
""" Moving one node of tree inside another
104129
@@ -247,9 +272,7 @@ def _drilldown_query(self, nodes=None):
247272
table = self.__class__
248273
if not nodes:
249274
nodes = self._base_query_obj()
250-
return nodes.filter(table.tree_id == self.tree_id)\
251-
.filter(table.left >= self.left)\
252-
.filter(table.right <= self.right)
275+
return nodes.filter(self.is_ancestor_of(table, inclusive=True))
253276

254277
def drilldown_tree(self, session=None, json=False, json_fields=None):
255278
""" This method generate a branch from a tree, begining with current
@@ -309,9 +332,7 @@ def path_to_root(self, session=None):
309332
"""
310333
table = self.__class__
311334
query = self._base_query_obj(session=session)
312-
query = query.filter(table.tree_id == self.tree_id)\
313-
.filter(table.left <= self.left)\
314-
.filter(table.right >= self.right)
335+
query = query.filter(table.is_ancestor_of(self, inclusive=True))
315336
return self._base_order(query, order=desc)
316337

317338
@classmethod

sqlalchemy_mptt/tests/cases/integrity.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,36 @@ def test_left_and_right_always_unique_number(self):
9191
right = self.session.query(table.right)
9292
keys = [x[0] for x in left.union(right)]
9393
self.assertEqual(len(keys), len(set(keys)))
94+
95+
def test_hierarchy_structure(self):
96+
""" Nodes with left < self and right > self are considered ancestors,
97+
while nodes with left > self and right < self are considered descendants
98+
"""
99+
table = self.model
100+
pivot = self.session.query(table).filter(table.right - table.left != 1).filter(table.parent_id != None).first()
101+
102+
# Exclusive Tests
103+
ancestors = self.session.query(table).filter(table.is_ancestor_of(pivot)).all()
104+
for ancestor in ancestors:
105+
self.assertTrue(ancestor.is_ancestor_of(pivot))
106+
self.assertNotIn(pivot, ancestors)
107+
108+
descendants = self.session.query(table).filter(table.is_descendant_of(pivot)).all()
109+
for descendant in descendants:
110+
self.assertTrue(descendant.is_descendant_of(pivot))
111+
self.assertNotIn(pivot, descendants)
112+
113+
self.assertEqual(set(), set(ancestors).intersection(set(descendants)))
114+
115+
# Inclusive Tests - because sometimes inclusivity is nice, like with self joins
116+
ancestors = self.session.query(table).filter(table.is_ancestor_of(pivot, inclusive=True)).all()
117+
for ancestor in ancestors:
118+
self.assertTrue(ancestor.is_ancestor_of(pivot, inclusive=True))
119+
self.assertIn(pivot, ancestors)
120+
121+
descendants = self.session.query(table).filter(table.is_descendant_of(pivot, inclusive=True)).all()
122+
for descendant in descendants:
123+
self.assertTrue(descendant.is_descendant_of(pivot, inclusive=True))
124+
self.assertIn(pivot, descendants)
125+
126+
self.assertEqual(set([pivot]), set(ancestors).intersection(set(descendants)))

0 commit comments

Comments
 (0)