Delete lots; add LotParent view; use parents, all_devices relationships; return uiTree with list of lots and parents

This commit is contained in:
Xavier Bustamante Talavera 2018-11-13 15:52:27 +01:00
parent 560e0ed8dc
commit 0a9fbb0226
5 changed files with 188 additions and 145 deletions

View File

@ -1,5 +1,6 @@
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from sqlalchemy.sql import expression
from sqlalchemy_utils import view from sqlalchemy_utils import view
from teal.db import SchemaSQLAlchemy from teal.db import SchemaSQLAlchemy
@ -18,9 +19,6 @@ class SQLAlchemy(SchemaSQLAlchemy):
self.drop_schema(schema='common') self.drop_schema(schema='common')
db = SQLAlchemy(session_options={"autoflush": False})
def create_view(name, selectable): def create_view(name, selectable):
"""Creates a view. """Creates a view.
@ -29,7 +27,7 @@ def create_view(name, selectable):
sqlalchemy-utils/blob/master/tests/test_views.py>`_ for an sqlalchemy-utils/blob/master/tests/test_views.py>`_ for an
example on how to use. example on how to use.
""" """
table = view.create_table_from_selectable(name=name, selectable=selectable, metadata=None) table = view.create_table_from_selectable(name, selectable)
# We need to ensure views are created / destroyed before / after # We need to ensure views are created / destroyed before / after
# SchemaSQLAlchemy's listeners execute # SchemaSQLAlchemy's listeners execute
@ -37,3 +35,8 @@ def create_view(name, selectable):
event.listen(db.metadata, 'after_create', view.CreateView(name, selectable), insert=True) event.listen(db.metadata, 'after_create', view.CreateView(name, selectable), insert=True)
event.listen(db.metadata, 'before_drop', view.DropView(name)) event.listen(db.metadata, 'before_drop', view.DropView(name))
return table return table
db = SQLAlchemy(session_options={"autoflush": False})
f = db.func
exp = expression

View File

@ -7,13 +7,12 @@ from citext import CIText
from flask import g from flask import g
from sqlalchemy import TEXT from sqlalchemy import TEXT
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import expression as exp
from sqlalchemy_utils import LtreeType from sqlalchemy_utils import LtreeType
from sqlalchemy_utils.types.ltree import LQUERY from sqlalchemy_utils.types.ltree import LQUERY
from teal.db import CASCADE_OWN, UUIDLtree from teal.db import CASCADE_OWN, UUIDLtree
from teal.resource import url_for_resource from teal.resource import url_for_resource
from ereuse_devicehub.db import create_view, db from ereuse_devicehub.db import create_view, db, exp, f
from ereuse_devicehub.resources.device.models import Component, Device from ereuse_devicehub.resources.device.models import Component, Device
from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -31,6 +30,7 @@ class Lot(Thing):
devices = db.relationship(Device, devices = db.relationship(Device,
backref=db.backref('lots', lazy=True, collection_class=set), backref=db.backref('lots', lazy=True, collection_class=set),
secondary=lambda: LotDevice.__table__, secondary=lambda: LotDevice.__table__,
lazy=True,
collection_class=set) collection_class=set)
""" """
The **children** devices that the lot has. The **children** devices that the lot has.
@ -38,6 +38,32 @@ class Lot(Thing):
Note that the lot can have more devices, if they are inside Note that the lot can have more devices, if they are inside
descendant lots. descendant lots.
""" """
parents = db.relationship(lambda: Lot,
viewonly=True,
lazy=True,
collection_class=set,
secondary=lambda: LotParent.__table__,
primaryjoin=lambda: Lot.id == LotParent.child_id,
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
cascade='refresh-expire', # propagate changes outside ORM
backref=db.backref('children',
viewonly=True,
lazy=True,
cascade='refresh-expire',
collection_class=set)
)
"""The parent lots."""
all_devices = db.relationship(Device,
viewonly=True,
lazy=True,
collection_class=set,
secondary=lambda: LotDeviceDescendants.__table__,
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id)
"""All devices, including components, inside this lot and its
descendants.
"""
def __init__(self, name: str, closed: bool = closed.default.arg, def __init__(self, name: str, closed: bool = closed.default.arg,
description: str = None) -> None: description: str = None) -> None:
@ -49,38 +75,11 @@ class Lot(Thing):
super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description) super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description)
Path(self) # Lots have always one edge per default. Path(self) # Lots have always one edge per default.
def add_child(self, child):
"""Adds a child to this lot."""
if isinstance(child, Lot):
Path.add(self.id, child.id)
db.session.refresh(self) # todo is this useful?
db.session.refresh(child)
else:
assert isinstance(child, uuid.UUID)
Path.add(self.id, child)
db.session.refresh(self) # todo is this useful?
def remove_child(self, child):
if isinstance(child, Lot):
Path.delete(self.id, child.id)
else:
assert isinstance(child, uuid.UUID)
Path.delete(self.id, child)
@property @property
def url(self) -> urlutils.URL: def url(self) -> urlutils.URL:
"""The URL where to GET this event.""" """The URL where to GET this event."""
return urlutils.URL(url_for_resource(Lot, item_id=self.id)) return urlutils.URL(url_for_resource(Lot, item_id=self.id))
@property
def children(self):
"""The children lots."""
# From https://stackoverflow.com/a/41158890
id = UUIDLtree.convert(self.id)
return self.query \
.join(self.__class__.paths) \
.filter(Path.path.lquery(exp.cast('*.{}.*{{1}}'.format(id), LQUERY)))
@property @property
def descendants(self): def descendants(self):
return self.descendantsq(self.id) return self.descendantsq(self.id)
@ -90,26 +89,45 @@ class Lot(Thing):
_id = UUIDLtree.convert(id) _id = UUIDLtree.convert(id)
return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY)) return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY))
@property
def parents(self):
return self.parentsq(self.id)
@classmethod
def parentsq(cls, id: UUID):
"""The parent lots."""
id = UUIDLtree.convert(id)
i = db.func.index(Path.path, id)
parent_id = db.func.replace(exp.cast(db.func.subpath(Path.path, i - 1, i), TEXT), '_', '-')
join_clause = parent_id == exp.cast(Lot.id, TEXT)
return cls.query.join(Path, join_clause).filter(
Path.path.lquery(exp.cast('*{{1}}.{}.*'.format(id), LQUERY))
)
@classmethod @classmethod
def roots(cls): def roots(cls):
"""Gets the lots that are not under any other lot.""" """Gets the lots that are not under any other lot."""
return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1) return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1)
def add_children(self, *children):
"""Add children lots to this lot.
This operation is highly costly as it forces refreshing
many models in session.
"""
for child in children:
if isinstance(child, Lot):
Path.add(self.id, child.id)
db.session.refresh(child)
else:
assert isinstance(child, uuid.UUID)
Path.add(self.id, child)
# We need to refresh the models involved in this operation
# outside the session / ORM control so the models
# that have relationships to this model
# with the cascade 'refresh-expire' can welcome the changes
db.session.refresh(self)
def remove_children(self, *children):
"""Remove children lots from this lot.
This operation is highly costly as it forces refreshing
many models in session.
"""
for child in children:
if isinstance(child, Lot):
Path.delete(self.id, child.id)
db.session.refresh(child)
else:
assert isinstance(child, uuid.UUID)
Path.delete(self.id, child)
db.session.refresh(self)
def delete(self): def delete(self):
"""Deletes the lot. """Deletes the lot.
@ -117,10 +135,15 @@ class Lot(Thing):
devices orphan from this lot and then marks this lot devices orphan from this lot and then marks this lot
for deletion. for deletion.
""" """
for child in self.children: self.remove_children(*self.children)
self.remove_child(child)
db.session.delete(self) db.session.delete(self)
def _refresh_models_with_relationships_to_lots(self):
session = db.Session.object_session(self)
for model in session:
if isinstance(model, (Device, Lot, Path)):
session.expire(model)
def __contains__(self, child: Union['Lot', Device]): def __contains__(self, child: Union['Lot', Device]):
if isinstance(child, Lot): if isinstance(child, Lot):
return Path.has_lot(self.id, child.id) return Path.has_lot(self.id, child.id)
@ -225,8 +248,8 @@ class LotDeviceDescendants(db.Model):
"""Query that gets the descendants of the ancestor lot.""" """Query that gets the descendants of the ancestor lot."""
devices = db.select([ devices = db.select([
LotDevice.device_id, LotDevice.device_id,
_ancestor.c.id.label('ancestor_lot_id'),
_desc.c.id.label('parent_lot_id'), _desc.c.id.label('parent_lot_id'),
_ancestor.c.id.label('ancestor_lot_id'),
None None
]).select_from(_ancestor).select_from(lot_device).where(descendants) ]).select_from(_ancestor).select_from(lot_device).where(descendants)
@ -240,12 +263,22 @@ class LotDeviceDescendants(db.Model):
components = db.select([ components = db.select([
Component.id.label('device_id'), Component.id.label('device_id'),
_ancestor.c.id.label('ancestor_lot_id'),
_desc.c.id.label('parent_lot_id'), _desc.c.id.label('parent_lot_id'),
_ancestor.c.id.label('ancestor_lot_id'),
LotDevice.device_id.label('device_parent_id'), LotDevice.device_id.label('device_parent_id'),
]).select_from(_ancestor).select_from(lot_device_component).where(descendants) ]).select_from(_ancestor).select_from(lot_device_component).where(descendants)
__table__ = create_view('lot_device_descendants', devices.union(components))
class LotParent(db.Model):
i = f.index(Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_')))
__table__ = create_view( __table__ = create_view(
name='lot_device_descendants', 'lot_parent',
selectable=devices.union(components) db.select([
Path.lot_id.label('child_id'),
exp.cast(f.replace(exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'),
UUID).label('parent_id')
]).select_from(Path).where(i > 0),
) )

View File

@ -22,6 +22,8 @@ class Lot(Thing):
devices = ... # type: relationship devices = ... # type: relationship
paths = ... # type: relationship paths = ... # type: relationship
description = ... # type: Column description = ... # type: Column
all_devices = ... # type: relationship
parents = ... # type: relationship
def __init__(self, name: str, closed: bool = closed.default.arg) -> None: def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
super().__init__() super().__init__()
@ -30,22 +32,21 @@ class Lot(Thing):
self.closed = ... # type: bool self.closed = ... # type: bool
self.devices = ... # type: Set[Device] self.devices = ... # type: Set[Device]
self.paths = ... # type: Set[Path] self.paths = ... # type: Set[Path]
description = ... # type: str self.description = ... # type: str
self.all_devices = ... # type: Set[Device]
self.parents = ... # type: Set[Lot]
self.children = ... # type: Set[Lot]
def add_child(self, child: Union[Lot, uuid.UUID]): def add_children(self, *children: Union[Lot, uuid.UUID]):
pass pass
def remove_child(self, child: Union[Lot, uuid.UUID]): def remove_children(self, *children: Union[Lot, uuid.UUID]):
pass pass
@classmethod @classmethod
def roots(cls) -> LotQuery: def roots(cls) -> LotQuery:
pass pass
@property
def children(self) -> LotQuery:
pass
@property @property
def descendants(self) -> LotQuery: def descendants(self) -> LotQuery:
pass pass
@ -54,14 +55,6 @@ class Lot(Thing):
def descendantsq(cls, id) -> LotQuery: def descendantsq(cls, id) -> LotQuery:
pass pass
@property
def parents(self) -> LotQuery:
pass
@classmethod
def parentsq(cls, id) -> LotQuery:
pass
@property @property
def url(self) -> urlutils.URL: def url(self) -> urlutils.URL:
pass pass

View File

@ -1,7 +1,7 @@
import uuid import uuid
from collections import deque from collections import deque
from enum import Enum from enum import Enum
from typing import List, Set from typing import Dict, List, Set, Union
import marshmallow as ma import marshmallow as ma
from flask import Response, jsonify, request from flask import Response, jsonify, request
@ -67,10 +67,12 @@ class LotView(View):
you can filter. you can filter.
""" """
if args['format'] == LotFormat.UiTree: if args['format'] == LotFormat.UiTree:
return jsonify({ lots = self.schema.dump(Lot.query, many=True, nested=1)
'items': self.ui_tree(), ret = {
'items': {l['id']: l for l in lots},
'tree': self.ui_tree(),
'url': request.path 'url': request.path
}) }
else: else:
query = Lot.query query = Lot.query
if args['search']: if args['search']:
@ -89,14 +91,6 @@ class LotView(View):
} }
return jsonify(ret) return jsonify(ret)
@classmethod
def ui_tree(cls) -> List[dict]:
nodes = []
for model in Path.query: # type: Path
path = deque(model.path.path.split('.'))
cls._p(nodes, path)
return nodes
def delete(self, id): def delete(self, id):
lot = Lot.query.filter_by(id=id).one() lot = Lot.query.filter_by(id=id).one()
lot.delete() lot.delete()
@ -104,7 +98,15 @@ class LotView(View):
return Response(status=204) return Response(status=204)
@classmethod @classmethod
def _p(cls, nodes: List[dict], path: deque): def ui_tree(cls) -> List[Dict]:
tree = []
for model in Path.query: # type: Path
path = deque(model.path.path.split('.'))
cls._p(tree, path)
return tree
@classmethod
def _p(cls, nodes: List[Dict[str, Union[uuid.UUID, List]]], path: deque):
"""Recursively creates the nested lot structure. """Recursively creates the nested lot structure.
Every recursive step consumes path (a deque of lot_id), Every recursive step consumes path (a deque of lot_id),
@ -116,14 +118,8 @@ class LotView(View):
# does lot_id exist already in node? # does lot_id exist already in node?
node = next(part for part in nodes if lot_id == part['id']) node = next(part for part in nodes if lot_id == part['id'])
except StopIteration: except StopIteration:
lot = Lot.query.filter_by(id=lot_id).one()
node = { node = {
'id': lot_id, 'id': lot_id,
'name': lot.name,
'url': lot.url.to_text(),
'closed': lot.closed,
'updated': lot.updated,
'created': lot.created,
'nodes': [] 'nodes': []
} }
nodes.append(node) nodes.append(node)
@ -180,12 +176,10 @@ class LotChildrenView(LotBaseChildrenView):
id = ma.fields.List(ma.fields.UUID()) id = ma.fields.List(ma.fields.UUID())
def _post(self, lot: Lot, ids: Set[uuid.UUID]): def _post(self, lot: Lot, ids: Set[uuid.UUID]):
for id in ids: lot.add_children(*ids)
lot.add_child(id) # todo what to do if child exists already?
def _delete(self, lot: Lot, ids: Set[uuid.UUID]): def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
for id in ids: lot.remove_children(*ids)
lot.remove_child(id)
class LotDeviceView(LotBaseChildrenView): class LotDeviceView(LotBaseChildrenView):

View File

@ -37,23 +37,33 @@ def test_lot_model_children():
l1, l2, l3 = lots l1, l2, l3 = lots
db.session.add_all(lots) db.session.add_all(lots)
db.session.flush() db.session.flush()
assert not l1.children
assert not l1.parents
assert not l2.children
assert not l2.parents
assert not l3.parents
assert not l3.children
l1.add_child(l2) l1.add_children(l2)
db.session.flush() assert l1.children == {l2}
assert l2.parents == {l1}
assert list(l1.children) == [l2] l2.add_children(l3)
assert l1.children == {l2}
l2.add_child(l3) assert l2.parents == {l1}
assert list(l1.children) == [l2] assert l2.children == {l3}
assert l3.parents == {l2}
l2.delete() l2.delete()
db.session.flush() db.session.flush()
assert not list(l1.children) assert not l1.children
assert not l3.parents
l1.delete() l1.delete()
db.session.flush() db.session.flush()
l3b = Lot.query.one() l3b = Lot.query.one()
assert l3 == l3b assert l3 == l3b
assert not l3.parents
def test_lot_modify_patch_endpoint_and_delete(user: UserClient): def test_lot_modify_patch_endpoint_and_delete(user: UserClient):
@ -87,8 +97,8 @@ def test_lot_device_relationship():
assert lot_device.created assert lot_device.created
assert lot_device.author_id == g.user.id assert lot_device.author_id == g.user.id
assert device.lots == {child} assert device.lots == {child}
# todo Device IN LOT does not work
assert device in child assert device in child
assert device in child.all_devices
graphic = GraphicCard(serial_number='foo', model='bar') graphic = GraphicCard(serial_number='foo', model='bar')
device.components.add(graphic) device.components.add(graphic)
@ -98,7 +108,7 @@ def test_lot_device_relationship():
parent = Lot('parent') parent = Lot('parent')
db.session.add(parent) db.session.add(parent)
db.session.flush() db.session.flush()
parent.add_child(child) parent.add_children(child)
assert child in parent assert child in parent
@ -111,13 +121,13 @@ def test_add_edge():
db.session.add(parent) db.session.add(parent)
db.session.flush() db.session.flush()
parent.add_child(child) parent.add_children(child)
assert child in parent assert child in parent
assert len(child.paths) == 1 assert len(child.paths) == 1
assert len(parent.paths) == 1 assert len(parent.paths) == 1
parent.remove_child(child) parent.remove_children(child)
assert child not in parent assert child not in parent
assert len(child.paths) == 1 assert len(child.paths) == 1
assert len(parent.paths) == 1 assert len(parent.paths) == 1
@ -126,8 +136,8 @@ def test_add_edge():
db.session.add(grandparent) db.session.add(grandparent)
db.session.flush() db.session.flush()
grandparent.add_child(parent) grandparent.add_children(parent)
parent.add_child(child) parent.add_children(child)
assert parent in grandparent assert parent in grandparent
assert child in parent assert child in parent
@ -148,31 +158,36 @@ def test_lot_multiple_parents(auth_app_context):
db.session.add_all(lots) db.session.add_all(lots)
db.session.flush() db.session.flush()
grandparent1.add_child(parent) grandparent1.add_children(parent)
assert parent in grandparent1 assert parent in grandparent1
parent.add_child(child) parent.add_children(child)
assert child in parent assert child in parent
assert child in grandparent1 assert child in grandparent1
grandparent2.add_child(parent) grandparent2.add_children(parent)
assert parent in grandparent1 assert parent in grandparent1
assert parent in grandparent2 assert parent in grandparent2
assert child in parent assert child in parent
assert child in grandparent1 assert child in grandparent1
assert child in grandparent2 assert child in grandparent2
p = parent.id
c = child.id
gp1 = grandparent1.id
gp2 = grandparent2.id
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree() nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
assert nodes[0]['name'] == 'grandparent1' assert nodes[0]['id'] == gp1
assert nodes[0]['nodes'][0]['name'] == 'parent' assert nodes[0]['nodes'][0]['id'] == p
assert nodes[0]['nodes'][0]['nodes'][0]['name'] == 'child' assert nodes[0]['nodes'][0]['nodes'][0]['id'] == c
assert nodes[0]['nodes'][0]['nodes'][0]['nodes'] == [] assert nodes[0]['nodes'][0]['nodes'][0]['nodes'] == []
assert nodes[1]['name'] == 'grandparent2' assert nodes[1]['id'] == gp2
assert nodes[1]['nodes'][0]['name'] == 'parent' assert nodes[1]['nodes'][0]['id'] == p
assert nodes[1]['nodes'][0]['nodes'][0]['name'] == 'child' assert nodes[1]['nodes'][0]['nodes'][0]['id'] == c
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == [] assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
# Now remove all childs # Now remove all childs
grandparent1.remove_child(parent) grandparent1.remove_children(parent)
assert parent not in grandparent1 assert parent not in grandparent1
assert child in parent assert child in parent
assert parent in grandparent2 assert parent in grandparent2
@ -180,14 +195,14 @@ def test_lot_multiple_parents(auth_app_context):
assert child in grandparent2 assert child in grandparent2
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree() nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
assert nodes[0]['name'] == 'grandparent1' assert nodes[0]['id'] == gp1
assert nodes[0]['nodes'] == [] assert nodes[0]['nodes'] == []
assert nodes[1]['name'] == 'grandparent2' assert nodes[1]['id'] == gp2
assert nodes[1]['nodes'][0]['name'] == 'parent' assert nodes[1]['nodes'][0]['id'] == p
assert nodes[1]['nodes'][0]['nodes'][0]['name'] == 'child' assert nodes[1]['nodes'][0]['nodes'][0]['id'] == c
assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == [] assert nodes[1]['nodes'][0]['nodes'][0]['nodes'] == []
grandparent2.remove_child(parent) grandparent2.remove_children(parent)
assert parent not in grandparent2 assert parent not in grandparent2
assert parent not in grandparent1 assert parent not in grandparent1
assert child not in grandparent2 assert child not in grandparent2
@ -195,27 +210,27 @@ def test_lot_multiple_parents(auth_app_context):
assert child in parent assert child in parent
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree() nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
assert nodes[0]['name'] == 'grandparent1' assert nodes[0]['id'] == gp1
assert nodes[0]['nodes'] == [] assert nodes[0]['nodes'] == []
assert nodes[1]['name'] == 'grandparent2' assert nodes[1]['id'] == gp2
assert nodes[1]['nodes'] == [] assert nodes[1]['nodes'] == []
assert nodes[2]['name'] == 'parent' assert nodes[2]['id'] == p
assert nodes[2]['nodes'][0]['name'] == 'child' assert nodes[2]['nodes'][0]['id'] == c
assert nodes[2]['nodes'][0]['nodes'] == [] assert nodes[2]['nodes'][0]['nodes'] == []
parent.remove_child(child) parent.remove_children(child)
assert child not in parent assert child not in parent
assert len(child.paths) == 1 assert len(child.paths) == 1
assert len(parent.paths) == 1 assert len(parent.paths) == 1
nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree() nodes = auth_app_context.resources[Lot.t].VIEW.ui_tree()
assert nodes[0]['name'] == 'grandparent1' assert nodes[0]['id'] == gp1
assert nodes[0]['nodes'] == [] assert nodes[0]['nodes'] == []
assert nodes[1]['name'] == 'grandparent2' assert nodes[1]['id'] == gp2
assert nodes[1]['nodes'] == [] assert nodes[1]['nodes'] == []
assert nodes[2]['name'] == 'parent' assert nodes[2]['id'] == p
assert nodes[2]['nodes'] == [] assert nodes[2]['nodes'] == []
assert nodes[3]['name'] == 'child' assert nodes[3]['id'] == c
assert nodes[3]['nodes'] == [] assert nodes[3]['nodes'] == []
@ -243,29 +258,29 @@ def test_lot_unite_graphs_and_find():
db.session.add_all(lots) db.session.add_all(lots)
db.session.flush() db.session.flush()
l1.add_child(l2) l1.add_children(l2)
assert l2 in l1 assert l2 in l1
l3.add_child(l2) l3.add_children(l2)
assert l2 in l3 assert l2 in l3
l5.add_child(l7) l5.add_children(l7)
assert l7 in l5 assert l7 in l5
l4.add_child(l5) l4.add_children(l5)
assert l5 in l4 assert l5 in l4
assert l7 in l4 assert l7 in l4
l5.add_child(l8) l5.add_children(l8)
assert l8 in l5 assert l8 in l5
l4.add_child(l6) l4.add_children(l6)
assert l6 in l4 assert l6 in l4
l6.add_child(l5) l6.add_children(l5)
assert l5 in l6 and l5 in l4 assert l5 in l6 and l5 in l4
# We unite the two graphs # We unite the two graphs
l2.add_child(l4) l2.add_children(l4)
assert l4 in l2 and l5 in l2 and l6 in l2 and l7 in l2 and l8 in l2 assert l4 in l2 and l5 in l2 and l6 in l2 and l7 in l2 and l8 in l2
assert l4 in l3 and l5 in l3 and l6 in l3 and l7 in l3 and l8 in l3 assert l4 in l3 and l5 in l3 and l6 in l3 and l7 in l3 and l8 in l3
# We remove the union # We remove the union
l2.remove_child(l4) l2.remove_children(l4)
assert l4 not in l2 and l5 not in l2 and l6 not in l2 and l7 not in l2 and l8 not in l2 assert l4 not in l2 and l5 not in l2 and l6 not in l2 and l7 not in l2 and l8 not in l2
assert l4 not in l3 and l5 not in l3 and l6 not in l3 and l7 not in l3 and l8 not in l3 assert l4 not in l3 and l5 not in l3 and l6 not in l3 and l7 not in l3 and l8 not in l3
@ -279,7 +294,7 @@ def test_lot_roots():
db.session.flush() db.session.flush()
assert set(Lot.roots()) == {l1, l2, l3} assert set(Lot.roots()) == {l1, l2, l3}
l1.add_child(l2) l1.add_children(l2)
assert set(Lot.roots()) == {l1, l3} assert set(Lot.roots()) == {l1, l3}
@ -306,11 +321,16 @@ def test_lot_post_add_children_view_ui_tree_normal(user: UserClient):
assert child['parents'][0]['id'] == parent['id'] assert child['parents'][0]['id'] == parent['id']
# Format UiTree # Format UiTree
lots = user.get(res=Lot, query=[('format', 'UiTree')])[0]['items'] r = user.get(res=Lot, query=[('format', 'UiTree')])[0]
assert 1 == len(lots) lots, nodes = r['items'], r['tree']
assert lots[0]['name'] == 'Parent' assert 1 == len(nodes)
assert len(lots[0]['nodes']) == 1 assert nodes[0]['id'] == parent['id']
assert lots[0]['nodes'][0]['name'] == 'Child' assert len(nodes[0]['nodes']) == 1
assert nodes[0]['nodes'][0]['id'] == child['id']
assert 2 == len(lots)
assert 'Parent' == lots[parent['id']]['name']
assert 'Child' == lots[child['id']]['name']
assert lots[child['id']]['parents'][0]['name'] == 'Parent'
# Normal list format # Normal list format
lots = user.get(res=Lot)[0]['items'] lots = user.get(res=Lot)[0]['items']