View for adding / removing lots / devices from lots
This commit is contained in:
parent
4953f7fb28
commit
39c79aef04
|
@ -1,10 +1,12 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from typing import Callable, Iterable, Tuple
|
||||||
|
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.lot import schemas
|
from ereuse_devicehub.resources.lot import schemas
|
||||||
from ereuse_devicehub.resources.lot.views import LotView
|
from ereuse_devicehub.resources.lot.views import LotBaseChildrenView, LotChildrenView, \
|
||||||
|
LotDeviceView, LotView
|
||||||
|
|
||||||
|
|
||||||
class LotDef(Resource):
|
class LotDef(Resource):
|
||||||
|
@ -13,6 +15,20 @@ class LotDef(Resource):
|
||||||
AUTH = True
|
AUTH = True
|
||||||
ID_CONVERTER = Converters.uuid
|
ID_CONVERTER = Converters.uuid
|
||||||
|
|
||||||
|
def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None,
|
||||||
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
|
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
||||||
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
|
children = LotChildrenView.as_view('lot-children', definition=self, auth=app.auth)
|
||||||
|
self.add_url_rule('/<{}:{}>/children'.format(self.ID_CONVERTER.value, self.ID_NAME),
|
||||||
|
view_func=children,
|
||||||
|
methods={'POST', 'DELETE'})
|
||||||
|
children = LotDeviceView.as_view('lot-device', definition=self, auth=app.auth)
|
||||||
|
self.add_url_rule('/<{}:{}>/devices'.format(self.ID_CONVERTER.value, self.ID_NAME),
|
||||||
|
view_func=children,
|
||||||
|
methods={'POST', 'DELETE'})
|
||||||
|
|
||||||
def init_db(self, db: 'db.SQLAlchemy'):
|
def init_db(self, db: 'db.SQLAlchemy'):
|
||||||
# Create functions
|
# Create functions
|
||||||
with pathlib.Path(__file__).parent.joinpath('dag.sql').open() as f:
|
with pathlib.Path(__file__).parent.joinpath('dag.sql').open() as f:
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import aliased
|
||||||
from sqlalchemy.sql import expression
|
from sqlalchemy.sql import expression
|
||||||
from sqlalchemy_utils import LtreeType
|
from sqlalchemy_utils import LtreeType
|
||||||
from sqlalchemy_utils.types.ltree import LQUERY
|
from sqlalchemy_utils.types.ltree import LQUERY
|
||||||
|
@ -26,6 +26,12 @@ class Lot(Thing):
|
||||||
backref=db.backref('parents', lazy=True, collection_class=set),
|
backref=db.backref('parents', lazy=True, collection_class=set),
|
||||||
secondary=lambda: LotDevice.__table__,
|
secondary=lambda: LotDevice.__table__,
|
||||||
collection_class=set)
|
collection_class=set)
|
||||||
|
"""
|
||||||
|
The **children** devices that the lot has.
|
||||||
|
|
||||||
|
Note that the lot can have more devices, if they are inside
|
||||||
|
descendant lots.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
|
def __init__(self, name: str, closed: bool = closed.default.arg) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -36,15 +42,34 @@ class Lot(Thing):
|
||||||
super().__init__(id=uuid.uuid4(), name=name, closed=closed)
|
super().__init__(id=uuid.uuid4(), name=name, closed=closed)
|
||||||
Path(self) # Lots have always one edge per default.
|
Path(self) # Lots have always one edge per default.
|
||||||
|
|
||||||
def add_child(self, child: 'Lot'):
|
def add_child(self, child):
|
||||||
"""Adds a child to this lot."""
|
"""Adds a child to this lot."""
|
||||||
|
if isinstance(child, Lot):
|
||||||
Path.add(self.id, child.id)
|
Path.add(self.id, child.id)
|
||||||
db.session.refresh(self) # todo is this useful?
|
db.session.refresh(self) # todo is this useful?
|
||||||
db.session.refresh(child)
|
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: 'Lot'):
|
def remove_child(self, child: 'Lot'):
|
||||||
Path.delete(self.id, child.id)
|
Path.delete(self.id, child.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
"""The children lots."""
|
||||||
|
# From https://stackoverflow.com/a/41158890
|
||||||
|
# todo test
|
||||||
|
cls = self.__class__
|
||||||
|
exp = '*.{}.*{{1}}'.format(UUIDLtree.convert(self.id))
|
||||||
|
child_lots = aliased(Lot)
|
||||||
|
|
||||||
|
return self.query \
|
||||||
|
.join(cls.paths) \
|
||||||
|
.filter(Path.path.lquery(expression.cast(exp, LQUERY))) \
|
||||||
|
.join(child_lots, Path.lot)
|
||||||
|
|
||||||
def __contains__(self, child: 'Lot'):
|
def __contains__(self, child: 'Lot'):
|
||||||
return Path.has_lot(self.id, child.id)
|
return Path.has_lot(self.id, child.id)
|
||||||
|
|
||||||
|
@ -96,16 +121,6 @@ class Path(db.Model):
|
||||||
super().__init__(lot=lot)
|
super().__init__(lot=lot)
|
||||||
self.path = UUIDLtree(lot.id)
|
self.path = UUIDLtree(lot.id)
|
||||||
|
|
||||||
def children(self) -> Set['Path']:
|
|
||||||
"""Get the children edges."""
|
|
||||||
# todo is it useful? test it when first usage
|
|
||||||
# From https://stackoverflow.com/a/41158890
|
|
||||||
exp = '*.{}.*{{1}}'.format(self.lot_id)
|
|
||||||
return set(self.query
|
|
||||||
.filter(self.path.lquery(expression.cast(exp, LQUERY)))
|
|
||||||
.distinct(self.__class__.lot_id)
|
|
||||||
.all())
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, parent_id: uuid.UUID, child_id: uuid.UUID):
|
def add(cls, parent_id: uuid.UUID, child_id: uuid.UUID):
|
||||||
"""Creates an edge between parent and child."""
|
"""Creates an edge between parent and child."""
|
||||||
|
@ -118,7 +133,10 @@ class Path(db.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool:
|
def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool:
|
||||||
return bool(db.session.execute(
|
parent_id = UUIDLtree.convert(parent_id)
|
||||||
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(
|
child_id = UUIDLtree.convert(child_id)
|
||||||
str(parent_id).replace('-', '_'), str(child_id).replace('-', '_'))
|
return bool(
|
||||||
).first())
|
db.session.execute(
|
||||||
|
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(parent_id, child_id)
|
||||||
|
).first()
|
||||||
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Set
|
from typing import Set, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
|
@ -25,7 +26,7 @@ class Lot(Thing):
|
||||||
self.devices = ... # type: Set[Device]
|
self.devices = ... # type: Set[Device]
|
||||||
self.paths = ... # type: Set[Path]
|
self.paths = ... # type: Set[Path]
|
||||||
|
|
||||||
def add_child(self, child: 'Lot'):
|
def add_child(self, child: Union['Lot', uuid.UUID]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_child(self, child: 'Lot'):
|
def remove_child(self, child: 'Lot'):
|
||||||
|
@ -35,6 +36,10 @@ class Lot(Thing):
|
||||||
def roots(cls):
|
def roots(cls):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self) -> Set['Lot']:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Path:
|
class Path:
|
||||||
id = ... # type: Column
|
id = ... # type: Column
|
||||||
|
|
|
@ -9,6 +9,9 @@ from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
|
||||||
class Lot(Thing):
|
class Lot(Thing):
|
||||||
id = f.UUID(dump_only=True)
|
id = f.UUID(dump_only=True)
|
||||||
name = f.String(validate=f.validate.Length(max=STR_SIZE))
|
name = f.String(validate=f.validate.Length(max=STR_SIZE), required=True)
|
||||||
closed = f.String(required=True, missing=False, description=m.Lot.closed.comment)
|
closed = f.Boolean(missing=False, description=m.Lot.closed.comment)
|
||||||
devices = f.String(NestedOn(Device, many=True, collection_class=set, only_query='id'))
|
devices = NestedOn(Device, many=True, dump_only=True)
|
||||||
|
children = NestedOn('Lot',
|
||||||
|
many=True,
|
||||||
|
dump_only=True)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
from flask import current_app as app, request
|
import marshmallow as ma
|
||||||
|
from flask import request
|
||||||
from teal.resource import View
|
from teal.resource import View
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
@ -9,10 +11,8 @@ from ereuse_devicehub.resources.lot.models import Lot
|
||||||
|
|
||||||
class LotView(View):
|
class LotView(View):
|
||||||
def post(self):
|
def post(self):
|
||||||
json = request.get_json(validate=False)
|
l = request.get_json()
|
||||||
e = app.resources[json['type']].schema.load(json)
|
lot = Lot(**l)
|
||||||
Model = db.Model._decl_class_registry.data[json['type']]()
|
|
||||||
lot = Model(**e)
|
|
||||||
db.session.add(lot)
|
db.session.add(lot)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
ret = self.schema.jsonify(lot)
|
ret = self.schema.jsonify(lot)
|
||||||
|
@ -21,5 +21,74 @@ class LotView(View):
|
||||||
|
|
||||||
def one(self, id: uuid.UUID):
|
def one(self, id: uuid.UUID):
|
||||||
"""Gets one event."""
|
"""Gets one event."""
|
||||||
event = Lot.query.filter_by(id=id).one()
|
lot = Lot.query.filter_by(id=id).one() # type: Lot
|
||||||
return self.schema.jsonify(event)
|
return self.schema.jsonify(lot)
|
||||||
|
|
||||||
|
|
||||||
|
class LotBaseChildrenView(View):
|
||||||
|
"""Base class for adding / removing children devices and
|
||||||
|
lots from a lot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ListArgs(ma.Schema):
|
||||||
|
id = ma.fields.List(ma.fields.UUID())
|
||||||
|
|
||||||
|
def __init__(self, definition: 'Resource', **kw) -> None:
|
||||||
|
super().__init__(definition, **kw)
|
||||||
|
self.list_args = self.ListArgs()
|
||||||
|
|
||||||
|
def get_ids(self) -> Set[uuid.UUID]:
|
||||||
|
args = self.QUERY_PARSER.parse(self.list_args, request, locations=('querystring',))
|
||||||
|
return set(args['id'])
|
||||||
|
|
||||||
|
def get_lot(self, id: uuid.UUID) -> Lot:
|
||||||
|
return Lot.query.filter_by(id=id).one()
|
||||||
|
|
||||||
|
# noinspection PyMethodOverriding
|
||||||
|
def post(self, id: uuid.UUID):
|
||||||
|
lot = self.get_lot(id)
|
||||||
|
self._post(lot, self.get_ids())
|
||||||
|
db.session.commit()
|
||||||
|
ret = self.schema.jsonify(lot)
|
||||||
|
ret.status_code = 201
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def delete(self, id: uuid.UUID):
|
||||||
|
lot = self.get_lot(id)
|
||||||
|
self._delete(lot, self.get_ids())
|
||||||
|
db.session.commit()
|
||||||
|
return self.schema.jsonify(lot)
|
||||||
|
|
||||||
|
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class LotChildrenView(LotBaseChildrenView):
|
||||||
|
"""View for adding and removing child lots from a lot.
|
||||||
|
|
||||||
|
Ex. ``lot/<id>/children/id=X&id=Y``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
for id in ids:
|
||||||
|
lot.add_child(id) # todo what to do if child exists already?
|
||||||
|
|
||||||
|
def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
for id in ids:
|
||||||
|
lot.remove_child(id)
|
||||||
|
|
||||||
|
|
||||||
|
class LotDeviceView(LotBaseChildrenView):
|
||||||
|
"""View for adding and removing child devices from a lot.
|
||||||
|
|
||||||
|
Ex. ``lot/<id>/devices/id=X&id=Y``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
lot.devices |= self.get_ids()
|
||||||
|
|
||||||
|
def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||||
|
lot.devices -= self.get_ids()
|
||||||
|
|
|
@ -25,7 +25,9 @@ def test_api_docs(client: Client):
|
||||||
'/snapshots/',
|
'/snapshots/',
|
||||||
'/users/login',
|
'/users/login',
|
||||||
'/events/',
|
'/events/',
|
||||||
'/lots/'
|
'/lots/',
|
||||||
|
'/lots/{id}/children',
|
||||||
|
'/lots/{id}/devices'
|
||||||
}
|
}
|
||||||
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
||||||
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
|
from ereuse_devicehub.client import UserClient
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Desktop
|
from ereuse_devicehub.resources.device.models import Desktop
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis
|
from ereuse_devicehub.resources.enums import ComputerChassis
|
||||||
|
@ -181,3 +182,40 @@ def test_lot_roots():
|
||||||
assert Lot.roots() == {l1, l2, l3}
|
assert Lot.roots() == {l1, l2, l3}
|
||||||
l1.add_child(l2)
|
l1.add_child(l2)
|
||||||
assert Lot.roots() == {l1, l3}
|
assert Lot.roots() == {l1, l3}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
def test_lot_model_children():
|
||||||
|
"""Tests the property Lot.children"""
|
||||||
|
lots = Lot('1'), Lot('2'), Lot('3')
|
||||||
|
l1, l2, l3 = lots
|
||||||
|
db.session.add_all(lots)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
l1.add_child(l2)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
children = l1.children
|
||||||
|
assert list(children) == [l2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_get_lot(user: UserClient):
|
||||||
|
"""Tests submitting and retreiving a basic lot."""
|
||||||
|
l, _ = user.post({'name': 'Foo'}, res=Lot)
|
||||||
|
assert l['name'] == 'Foo'
|
||||||
|
l, _ = user.get(res=Lot, item=l['id'])
|
||||||
|
assert l['name'] == 'Foo'
|
||||||
|
assert not l['children']
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_add_children_view(user: UserClient):
|
||||||
|
"""Tests adding children lots to a lot through the view."""
|
||||||
|
l, _ = user.post(({'name': 'Parent'}), res=Lot)
|
||||||
|
child, _ = user.post(({'name': 'Child'}), res=Lot)
|
||||||
|
l, _ = user.post({}, res=Lot, item='{}/children'.format(l['id']), query=[('id', child['id'])])
|
||||||
|
assert l['children'][0]['id'] == child['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='Just develop the test')
|
||||||
|
def test_post_add_device_view(user: UserClient):
|
||||||
|
pass
|
||||||
|
|
Reference in a new issue