Fix inconsistencies in filtering devices inside lots

This commit is contained in:
Xavier Bustamante Talavera 2018-11-04 22:40:14 +01:00
parent afb2815883
commit 5bc72fbe8b
4 changed files with 83 additions and 62 deletions

View file

@ -5,7 +5,6 @@ from flask import current_app as app, render_template, request
from flask.json import jsonify from flask.json import jsonify
from flask_sqlalchemy import Pagination from flask_sqlalchemy import Pagination
from marshmallow import fields, 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 import query
from teal.cache import cache from teal.cache import cache
from teal.resource import View from teal.resource import View
@ -46,12 +45,10 @@ class LotQ(query.Query):
class Filters(query.Query): class Filters(query.Query):
_parent = aliased(Computer) _parent = Computer.__table__.alias()
_device_inside_lot = (Device.id == LotDevice.device_id) & (Lot.id == LotDevice.lot_id) _device_inside_lot = (Device.id == LotDevice.device_id) & (Lot.id == LotDevice.lot_id)
_component_inside_lot_through_parent = (Device.id == Component.id) \ _parent_device_in_lot = (Device.id == Component.id) & (Component.parent_id == _parent.c.id) \
& (Component.parent_id == _parent.id) \ & (_parent.c.id == LotDevice.device_id) & (Lot.id == LotDevice.lot_id)
& (_parent.id == LotDevice.device_id) \
& (Lot.id == LotDevice.lot_id)
type = query.Or(OfType(Device.type)) type = query.Or(OfType(Device.type))
model = query.ILike(Device.model) model = query.ILike(Device.model)
@ -59,7 +56,10 @@ class Filters(query.Query):
serialNumber = query.ILike(Device.serial_number) serialNumber = query.ILike(Device.serial_number)
rating = query.Join(Device.id == Rate.device_id, RateQ) rating = query.Join(Device.id == Rate.device_id, RateQ)
tag = query.Join(Device.id == Tag.device_id, TagQ) tag = query.Join(Device.id == Tag.device_id, TagQ)
lot = query.Join(_device_inside_lot | _component_inside_lot_through_parent, LotQ) # todo This part of the query is really slow
# And forces usage of distinct, as it returns many rows
# due to having multiple paths to the same
lot = query.Join(_device_inside_lot | _parent_device_in_lot, LotQ)
class Sorting(query.Sort): class Sorting(query.Sort):
@ -71,7 +71,7 @@ class DeviceView(View):
class FindArgs(marshmallow.Schema): class FindArgs(marshmallow.Schema):
search = f.Raw() search = f.Raw()
filter = f.Nested(Filters, missing=[]) filter = f.Nested(Filters, missing=[])
sort = f.Nested(Sorting, missing=[]) sort = f.Nested(Sorting, missing=[Device.id.asc()])
page = f.Integer(validate=v.Range(min=1), missing=1) page = f.Integer(validate=v.Range(min=1), missing=1)
def get(self, id): def get(self, id):
@ -123,7 +123,7 @@ class DeviceView(View):
def find(self, args: dict): def find(self, args: dict):
"""Gets many devices.""" """Gets many devices."""
search_p = args.get('search', None) search_p = args.get('search', None)
query = Device.query query = Device.query.distinct() # todo we should not force to do this if the query is ok
if search_p: if search_p:
properties = DeviceSearch.properties properties = DeviceSearch.properties
tags = DeviceSearch.tags tags = DeviceSearch.tags

View file

@ -101,14 +101,14 @@ class Lot(Thing):
Path.path.lquery(exp.cast('*{{1}}.{}.*'.format(id), LQUERY)) Path.path.lquery(exp.cast('*{{1}}.{}.*'.format(id), LQUERY))
) )
def __contains__(self, child: 'Lot'):
return Path.has_lot(self.id, child.id)
@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 __contains__(self, child: 'Lot'):
return Path.has_lot(self.id, child.id)
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Lot {0.name} devices={0.devices!r}>'.format(self) return '<Lot {0.name} devices={0.devices!r}>'.format(self)

View file

@ -4,7 +4,7 @@ from teal.utils import compiled
from ereuse_devicehub.client import UserClient from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.device.models import Desktop, Device, Laptop, Processor, \ from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, Laptop, Server, \
SolidStateDrive SolidStateDrive
from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.device.views import Filters, Sorting from ereuse_devicehub.resources.device.views import Filters, Sorting
@ -56,51 +56,70 @@ def test_device_sort():
@pytest.fixture() @pytest.fixture()
def device_query_dummy(app: Devicehub): def device_query_dummy(app: Devicehub):
"""
3 computers, where:
1. s1 Desktop with a Processor
2. s2 Desktop with an SSD
3. s3 Laptop
4. s4 Server with another SSD
:param app:
:return:
"""
with app.app_context(): with app.app_context():
devices = ( # The order matters ;-) devices = ( # The order matters ;-)
Desktop(serial_number='s1', Desktop(serial_number='1',
model='ml1', model='ml1',
manufacturer='mr1', manufacturer='mr1',
chassis=ComputerChassis.Tower), chassis=ComputerChassis.Tower),
Laptop(serial_number='s3', Desktop(serial_number='2',
model='ml3',
manufacturer='mr3',
chassis=ComputerChassis.Detachable),
Desktop(serial_number='s2',
model='ml2', model='ml2',
manufacturer='mr2', manufacturer='mr2',
chassis=ComputerChassis.Microtower), chassis=ComputerChassis.Microtower),
SolidStateDrive(serial_number='s4', model='ml4', manufacturer='mr4') Laptop(serial_number='3',
model='ml3',
manufacturer='mr3',
chassis=ComputerChassis.Detachable),
Server(serial_number='4',
model='ml4',
manufacturer='mr4',
chassis=ComputerChassis.Tower),
)
devices[0].components.add(
GraphicCard(serial_number='1-gc', model='s1ml', manufacturer='s1mr')
)
devices[1].components.add(
SolidStateDrive(serial_number='2-ssd', model='s2ml', manufacturer='s2mr')
)
devices[-1].components.add(
SolidStateDrive(serial_number='4-ssd', model='s4ml', manufacturer='s4mr')
) )
devices[-1].parent = devices[0] # s4 in s1
db.session.add_all(devices) db.session.add_all(devices)
devices[0].components.add(Processor(model='ml5', manufacturer='mr5'))
db.session.commit() db.session.commit()
@pytest.mark.usefixtures(device_query_dummy.__name__) @pytest.mark.usefixtures(device_query_dummy.__name__)
def test_device_query_no_filters(user: UserClient): def test_device_query_no_filters(user: UserClient):
i, _ = user.get(res=Device) i, _ = user.get(res=Device)
assert tuple(d['type'] for d in i['items']) == ( assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
'Desktop', 'Laptop', 'Desktop', 'SolidStateDrive', 'Processor' d['serialNumber'] for d in i['items']
) )
@pytest.mark.usefixtures(device_query_dummy.__name__) @pytest.mark.usefixtures(device_query_dummy.__name__)
def test_device_query_filter_type(user: UserClient): def test_device_query_filter_type(user: UserClient):
i, _ = user.get(res=Device, query=[('filter', {'type': ['Desktop', 'Laptop']})]) i, _ = user.get(res=Device, query=[('filter', {'type': ['Desktop', 'Laptop']})])
assert tuple(d['type'] for d in i['items']) == ('Desktop', 'Laptop', 'Desktop') assert ('1', '2', '3') == tuple(d['serialNumber'] for d in i['items'])
@pytest.mark.usefixtures(device_query_dummy.__name__) @pytest.mark.usefixtures(device_query_dummy.__name__)
def test_device_query_filter_sort(user: UserClient): def test_device_query_filter_sort(user: UserClient):
i, _ = user.get(res=Device, query=[ i, _ = user.get(res=Device, query=[
('sort', {'created': Sorting.ASCENDING}), ('sort', {'created': Sorting.DESCENDING}),
('filter', {'type': ['Computer']}) ('filter', {'type': ['Computer']})
]) ])
assert tuple(d['type'] for d in i['items']) == ('Desktop', 'Laptop', 'Desktop') assert ('4', '3', '2', '1') == tuple(d['serialNumber'] for d in i['items'])
@pytest.mark.usefixtures(device_query_dummy.__name__) @pytest.mark.usefixtures(device_query_dummy.__name__)
@ -111,7 +130,7 @@ def test_device_query_filter_lots(user: UserClient):
i, _ = user.get(res=Device, query=[ i, _ = user.get(res=Device, query=[
('filter', {'lot': {'id': [parent['id']]}}) ('filter', {'lot': {'id': [parent['id']]}})
]) ])
assert len(i['items']) == 0, 'No devices in lot' assert not i['items'], 'No devices in lot'
parent, _ = user.post({}, parent, _ = user.post({},
res=Lot, res=Lot,
@ -120,42 +139,37 @@ def test_device_query_filter_lots(user: UserClient):
i, _ = user.get(res=Device, query=[ i, _ = user.get(res=Device, query=[
('filter', {'type': ['Computer']}) ('filter', {'type': ['Computer']})
]) ])
lot, _ = user.post({}, assert ('1', '2', '3', '4') == tuple(d['serialNumber'] for d in i['items'])
res=Lot, parent, _ = user.post({},
item='{}/devices'.format(parent['id']), res=Lot,
query=[('id', d['id']) for d in i['items'][:-1]]) item='{}/devices'.format(parent['id']),
lot, _ = user.post({}, query=[('id', d['id']) for d in i['items'][:2]])
res=Lot, child, _ = user.post({},
item='{}/devices'.format(child['id']), res=Lot,
query=[('id', i['items'][-1]['id'])]) item='{}/devices'.format(child['id']),
query=[('id', d['id']) for d in i['items'][2:]])
i, _ = user.get(res=Device, query=[ i, _ = user.get(res=Device, query=[
('filter', {'lot': {'id': [parent['id']]}}), ('filter', {'lot': {'id': [parent['id']]}})
('sort', {'id': Sorting.ASCENDING})
]) ])
assert tuple(x['id'] for x in i['items']) == (1, 2, 3, 4, 5), \ assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
'The parent lot contains 2 items plus indirectly the third one, and 1st device the HDD.' x['serialNumber'] for x in i['items']
), 'The parent lot contains 2 items plus indirectly the other ' \
'2 from the child lot, with all their 2 components'
i, _ = user.get(res=Device, query=[ i, _ = user.get(res=Device, query=[
('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}), ('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}),
('sort', {'id': Sorting.ASCENDING})
]) ])
assert tuple(x['id'] for x in i['items']) == (1, 2, 3) assert ('1', '2', '3', '4') == tuple(x['serialNumber'] for x in i['items'])
s, _ = user.get(res=Device, query=[ s, _ = user.get(res=Device, query=[
('filter', {'lot': {'id': [child['id']]}}) ('filter', {'lot': {'id': [child['id']]}})
]) ])
assert len(s['items']) == 1 assert ('3', '4', '4-ssd') == tuple(x['serialNumber'] for x in s['items'])
assert s['items'][0]['chassis'] == 'Microtower', 'The child lot only contains the last device.'
s, _ = user.get(res=Device, query=[ s, _ = user.get(res=Device, query=[
('filter', {'lot': {'id': [child['id'], parent['id']]}}) ('filter', {'lot': {'id': [child['id'], parent['id']]}})
]) ])
assert all(x['id'] == id for x, id in zip(i['items'], (1, 2, 3, 4))), \ assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple(
'Adding both lots is redundant in this case and we have the 4 elements.' x['serialNumber'] for x in s['items']
i, _ = user.get(res=Device, query=[ ), 'Adding both lots is redundant in this case and we have the 4 elements.'
('filter', {'lot': {'id': [parent['id']]}, 'type': ['Computer']}),
('sort', {'id': Sorting.ASCENDING})
])
assert tuple(x['id'] for x in i['items']) == (1, 2, 3), 'Only computers now'
def test_device_query(user: UserClient): def test_device_query(user: UserClient):

View file

@ -32,30 +32,37 @@ def test_lot_modify_patch_endpoint(user: UserClient):
assert l_after['name'] == 'bar' assert l_after['name'] == 'bar'
@pytest.mark.xfail(reason='Components are not added to lots!') @pytest.mark.xfail(reason='the IN comparison does not work for device')
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_lot_device_relationship(): def test_lot_device_relationship():
device = Desktop(serial_number='foo', device = Desktop(serial_number='foo',
model='bar', model='bar',
manufacturer='foobar', manufacturer='foobar',
chassis=ComputerChassis.Lunchbox) chassis=ComputerChassis.Lunchbox)
lot = Lot('lot1') child = Lot('child')
lot.devices.add(device) child.devices.add(device)
db.session.add(lot) db.session.add(child)
db.session.flush() db.session.flush()
lot_device = LotDevice.query.one() # type: LotDevice lot_device = LotDevice.query.one() # type: LotDevice
assert lot_device.device_id == device.id assert lot_device.device_id == device.id
assert lot_device.lot_id == lot.id assert lot_device.lot_id == child.id
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 == {lot} assert device.lots == {child}
assert device in lot # todo Device IN LOT does not work
assert device in child
graphic = GraphicCard(serial_number='foo', model='bar') graphic = GraphicCard(serial_number='foo', model='bar')
device.components.add(graphic) device.components.add(graphic)
db.session.flush() db.session.flush()
assert graphic in lot assert graphic in child
parent = Lot('parent')
db.session.add(parent)
db.session.flush()
parent.add_child(child)
assert child in parent
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)