Toggle formats when finding lots; add device.lots when GETting devices

This commit is contained in:
Xavier Bustamante Talavera 2018-10-08 17:32:45 +02:00
parent a3f6d7877a
commit df31074775
4 changed files with 91 additions and 17 deletions

View File

@ -9,7 +9,7 @@ from boltons import urlutils
from citext import CIText from citext import CIText
from ereuse_utils.naming import Naming from ereuse_utils.naming import Naming
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect Sequence, SmallInteger, Unicode, inspect, text
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
@ -401,7 +401,10 @@ class Manufacturer(db.Model):
__table_args__ = {'schema': 'common'} __table_args__ = {'schema': 'common'}
CSV_DELIMITER = csv.get_dialect('excel').delimiter CSV_DELIMITER = csv.get_dialect('excel').delimiter
name = db.Column(CIText(), primary_key=True) name = db.Column(CIText(),
primary_key=True,
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin'))
url = db.Column(URL(), unique=True) url = db.Column(URL(), unique=True)
logo = db.Column(URL()) logo = db.Column(URL())

View File

@ -30,6 +30,7 @@ class Device(Thing):
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__) events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
url = URL(dump_only=True, description=m.Device.url.__doc__) url = URL(dump_only=True, description=m.Device.url.__doc__)
lots = NestedOn('Lot', many=True, dump_only=True)
@pre_load @pre_load
def from_events_to_events_one(self, data: dict): def from_events_to_events_one(self, data: dict):

View File

@ -1,9 +1,13 @@
import uuid import uuid
from collections import deque from collections import deque
from enum import Enum
from typing import List, Set from typing import List, Set
import marshmallow as ma import marshmallow as ma
from flask import jsonify, request from flask import jsonify, request
from marshmallow import Schema as MarshmallowSchema, fields as f
from teal import query
from teal.marshmallow import EnumField
from teal.resource import View from teal.resource import View
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -11,7 +15,23 @@ from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.lot.models import Lot, Path from ereuse_devicehub.resources.lot.models import Lot, Path
class Filters(query.Query):
name = query.ILike(Lot.name)
class LotFormat(Enum):
UiTree = 'UiTree'
class LotView(View): class LotView(View):
class FindArgs(MarshmallowSchema):
"""
Allowed arguments for the ``find``
method (GET collection) endpoint
"""
format = EnumField(LotFormat, missing=None)
filter = f.Nested(Filters, missing=[])
def post(self): def post(self):
l = request.get_json() l = request.get_json()
lot = Lot(**l) lot = Lot(**l)
@ -27,21 +47,46 @@ class LotView(View):
return self.schema.jsonify(lot) return self.schema.jsonify(lot)
def find(self, args: dict): def find(self, args: dict):
"""Returns all lots as required for DevicehubClient:: """
Gets lots.
[ By passing the value `UiTree` in the parameter `format`
of the query you get a recursive nested suited for ui-tree::
[
{title: 'lot1', {title: 'lot1',
nodes: [{title: 'child1', nodes:[]}] nodes: [{title: 'child1', nodes:[]}]
] ]
Note that in this format filters are ignored.
Otherwise it just returns the standard flat view of lots that
you can filter.
""" """
nodes = [] if args['format'] == LotFormat.UiTree:
for model in Path.query: # type: Path nodes = []
path = deque(model.path.path.split('.')) for model in Path.query: # type: Path
self._p(nodes, path) path = deque(model.path.path.split('.'))
return jsonify({ self._p(nodes, path)
'items': nodes, return jsonify({
'url': request.path 'items': nodes,
}) 'url': request.path
})
else:
query = Lot.query.filter(*args['filter'])
lots = query.paginate(per_page=6)
ret = {
'items': self.schema.dump(lots.items, many=True, nested=0),
'pagination': {
'page': lots.page,
'perPage': lots.per_page,
'total': lots.total,
'previous': lots.prev_num,
'next': lots.next_num
},
'url': request.path
}
return jsonify(ret)
def _p(self, nodes: List[dict], path: deque): def _p(self, nodes: List[dict], path: deque):
"""Recursively creates the nested lot structure. """Recursively creates the nested lot structure.

View File

@ -4,7 +4,7 @@ from flask import g
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 from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard
from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.enums import ComputerChassis
from ereuse_devicehub.resources.lot.models import Lot, LotDevice from ereuse_devicehub.resources.lot.models import Lot, LotDevice
from tests import conftest from tests import conftest
@ -23,6 +23,7 @@ In case of error, debug with:
""" """
@pytest.mark.xfail(reason='Components are not added to lots!')
@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',
@ -40,6 +41,12 @@ 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 == {lot} assert device.lots == {lot}
assert device in lot
graphic = GraphicCard(serial_number='foo', model='bar')
device.components.add(graphic)
db.session.flush()
assert graphic in lot
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)
@ -209,8 +216,9 @@ def test_post_get_lot(user: UserClient):
assert not l['children'] assert not l['children']
def test_post_add_children_view(user: UserClient): def test_post_add_children_view_ui_tree_normal(user: UserClient):
"""Tests adding children lots to a lot through the view.""" """Tests adding children lots to a lot through the view and
GETting the results."""
parent, _ = user.post(({'name': 'Parent'}), res=Lot) parent, _ = user.post(({'name': 'Parent'}), res=Lot)
child, _ = user.post(({'name': 'Child'}), res=Lot) child, _ = user.post(({'name': 'Child'}), res=Lot)
parent, _ = user.post({}, parent, _ = user.post({},
@ -221,16 +229,29 @@ def test_post_add_children_view(user: UserClient):
child, _ = user.get(res=Lot, item=child['id']) child, _ = user.get(res=Lot, item=child['id'])
assert child['parents'][0]['id'] == parent['id'] assert child['parents'][0]['id'] == parent['id']
lots = user.get(res=Lot)[0]['items'] # Format UiTree
lots = user.get(res=Lot, query=[('format', 'UiTree')])[0]['items']
assert len(lots) == 1 assert len(lots) == 1
assert lots[0]['name'] == 'Parent' assert lots[0]['name'] == 'Parent'
assert len(lots[0]['nodes']) == 1 assert len(lots[0]['nodes']) == 1
assert lots[0]['nodes'][0]['name'] == 'Child' assert lots[0]['nodes'][0]['name'] == 'Child'
# Normal list format
lots = user.get(res=Lot)[0]['items']
assert len(lots) == 2
assert lots[0]['name'] == 'Parent'
assert lots[1]['name'] == 'Child'
# List format with a filter
lots = user.get(res=Lot, query=[('filter', {'name': 'pa'})])[0]['items']
assert len(lots) == 1
assert lots[0]['name'] == 'Parent'
def test_lot_post_add_remove_device_view(app: Devicehub, user: UserClient): def test_lot_post_add_remove_device_view(app: Devicehub, user: UserClient):
"""Tests adding a device to a lot using POST and """Tests adding a device to a lot using POST and
removing it with DELETE.""" removing it with DELETE."""
# todo check with components
with app.app_context(): with app.app_context():
device = Desktop(serial_number='foo', device = Desktop(serial_number='foo',
model='bar', model='bar',
@ -244,7 +265,11 @@ def test_lot_post_add_remove_device_view(app: Devicehub, user: UserClient):
res=Lot, res=Lot,
item='{}/devices'.format(parent['id']), item='{}/devices'.format(parent['id']),
query=[('id', device_id)]) query=[('id', device_id)])
assert lot['devices'][0]['id'] == device_id assert lot['devices'][0]['id'] == device_id, 'Lot contains device'
device, _ = user.get(res=Device, item=device_id)
assert len(device['lots']) == 1
assert device['lots'][0]['id'] == lot['id'], 'Device is inside lot'
# Remove the device # Remove the device
lot, _ = user.delete(res=Lot, lot, _ = user.delete(res=Lot,
item='{}/devices'.format(parent['id']), item='{}/devices'.format(parent['id']),