From 863578559c885e949c347f8bc2665ff7344c23d3 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sat, 6 Oct 2018 12:45:56 +0200 Subject: [PATCH] Filter devices by being inside lots --- ereuse_devicehub/resources/device/views.py | 20 ++++++++-- ereuse_devicehub/resources/lot/models.py | 17 +++++++- ereuse_devicehub/resources/lot/models.pyi | 13 +++++++ tests/test_device_find.py | 45 ++++++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index 9aabcc76..61993ab0 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -4,7 +4,8 @@ import marshmallow from flask import current_app as app, render_template, request from flask.json import jsonify from flask_sqlalchemy import Pagination -from marshmallow import fields as f, validate as v +from marshmallow import fields, fields as f, validate as v +from sqlalchemy.orm import aliased from teal import query from teal.cache import cache from teal.resource import View @@ -12,9 +13,10 @@ from teal.resource import View from ereuse_devicehub import auth from ereuse_devicehub.db import db from ereuse_devicehub.resources import search -from ereuse_devicehub.resources.device.models import Device, Manufacturer +from ereuse_devicehub.resources.device.models import Component, Computer, Device, Manufacturer from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.event.models import Rate +from ereuse_devicehub.resources.lot.models import Lot, LotDevice from ereuse_devicehub.resources.tag.model import Tag @@ -39,16 +41,28 @@ class TagQ(query.Query): org = query.ILike(Tag.org) +class LotQ(query.Query): + id = query.Or(query.QueryField(Lot.descendantsq, fields.UUID())) + + class Filters(query.Query): + _parent = aliased(Computer) + _device_inside_lot = (Device.id == LotDevice.device_id) & (Lot.id == LotDevice.lot_id) + _component_inside_lot_through_parent = (Device.id == Component.id) \ + & (Component.parent_id == _parent.id) \ + & (_parent.id == LotDevice.device_id) + type = query.Or(OfType(Device.type)) model = query.ILike(Device.model) manufacturer = query.ILike(Device.manufacturer) serialNumber = query.ILike(Device.serial_number) rating = query.Join(Device.id == Rate.device_id, RateQ) - tag = query.Join(Device.id == Tag.id, TagQ) + tag = query.Join(Device.id == Tag.device_id, TagQ) + lot = query.Join(_device_inside_lot | _component_inside_lot_through_parent, LotQ) class Sorting(query.Sort): + id = query.SortField(Device.id) created = query.SortField(Device.created) diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index 24c2afe4..faf59587 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -77,14 +77,27 @@ class Lot(Thing): .join(self.__class__.paths) \ .filter(Path.path.lquery(exp.cast('*.{}.*{{1}}'.format(id), LQUERY))) + @property + def descendants(self): + return self.descendantsq(self.id) + + @classmethod + def descendantsq(cls, id): + _id = UUIDLtree.convert(id) + 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(self.id) + 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 self.query.join(Path, join_clause).filter( + return cls.query.join(Path, join_clause).filter( Path.path.lquery(exp.cast('*{{1}}.{}.*'.format(id), LQUERY)) ) diff --git a/ereuse_devicehub/resources/lot/models.pyi b/ereuse_devicehub/resources/lot/models.pyi index 39eac1e4..ff060e7d 100644 --- a/ereuse_devicehub/resources/lot/models.pyi +++ b/ereuse_devicehub/resources/lot/models.pyi @@ -43,14 +43,27 @@ class Lot(Thing): def children(self) -> LotQuery: pass + @property + def descendants(self) -> LotQuery: + pass + + @classmethod + def descendantsq(cls, id) -> LotQuery: + pass + @property def parents(self) -> LotQuery: pass + @classmethod + def parentsq(cls, id) -> LotQuery: + pass + @property def url(self) -> urlutils.URL: pass + class Path: id = ... # type: Column lot_id = ... # type: Column diff --git a/tests/test_device_find.py b/tests/test_device_find.py index af73176e..71a49c2b 100644 --- a/tests/test_device_find.py +++ b/tests/test_device_find.py @@ -8,6 +8,7 @@ from ereuse_devicehub.resources.device.models import Desktop, Device, Laptop, So from ereuse_devicehub.resources.device.views import Filters, Sorting from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.event.models import Snapshot +from ereuse_devicehub.resources.lot.models import Lot from tests import conftest from tests.conftest import file @@ -98,6 +99,50 @@ def test_device_query_filter_sort(user: UserClient): assert tuple(d['type'] for d in i['items']) == ('Desktop', 'Laptop', 'Desktop') +@pytest.mark.usefixtures(device_query_dummy.__name__) +def test_device_query_filter_lots(user: UserClient): + parent, _ = user.post({'name': 'Parent'}, res=Lot) + child, _ = user.post({'name': 'Child'}, res=Lot) + parent, _ = user.post({}, + res=Lot, + item='{}/children'.format(parent['id']), + query=[('id', child['id'])]) + i, _ = user.get(res=Device, query=[ + ('filter', {'type': ['Computer']}) + ]) + lot, _ = user.post({}, + res=Lot, + item='{}/devices'.format(parent['id']), + query=[('id', d['id']) for d in i['items'][:-1]]) + lot, _ = user.post({}, + res=Lot, + item='{}/devices'.format(child['id']), + query=[('id', i['items'][-1]['id'])]) + i, _ = user.get(res=Device, query=[ + ('filter', {'lot': {'id': [parent['id']]}}), + ('sort', {'id': Sorting.ASCENDING}) + ]) + assert len(i['items']) == 4 + assert tuple(x['id'] for x in i['items']) == (1, 2, 3, 4), \ + 'The parent lot contains 2 items plus indirectly the third one, and 1st device the HDD.' + + s, _ = user.get(res=Device, query=[ + ('filter', {'lot': {'id': [child['id']]}}) + ]) + assert s['items'][0]['chassis'] == 'Microtower', 'The child lot only contains the last device.' + s, _ = user.get(res=Device, query=[ + ('filter', {'lot': {'id': [child['id'], parent['id']]}}) + ]) + assert all(x['id'] == id for x, id in zip(i['items'], (1, 2, 3, 4))), \ + 'Adding both lots is redundant in this case and we have the 4 elements.' + i, _ = user.get(res=Device, query=[ + ('filter', {'lot': {'id': [parent['id']]}, 'type': ['Computer']}), + ('sort', {'id': Sorting.ASCENDING}) + ]) + assert len(i['items']) == 3 + assert tuple(x['id'] for x in i['items']) == (1, 2, 3), 'Only computers now' + + def test_device_query(user: UserClient): """Checks result of inventory.""" user.post(conftest.file('basic.snapshot'), res=Snapshot)