diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 5c11bdf8..e66c6888 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -9,7 +9,7 @@ from boltons import urlutils from citext import CIText from ereuse_utils.naming import Naming 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.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.util import OrderedSet @@ -401,7 +401,10 @@ class Manufacturer(db.Model): __table_args__ = {'schema': 'common'} 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) logo = db.Column(URL()) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 03269f1e..1e4c303e 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -30,6 +30,7 @@ class Device(Thing): 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) url = URL(dump_only=True, description=m.Device.url.__doc__) + lots = NestedOn('Lot', many=True, dump_only=True) @pre_load def from_events_to_events_one(self, data: dict): diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index 64bc0424..df630a04 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -1,9 +1,13 @@ import uuid from collections import deque +from enum import Enum from typing import List, Set import marshmallow as ma 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 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 +class Filters(query.Query): + name = query.ILike(Lot.name) + + +class LotFormat(Enum): + UiTree = 'UiTree' + + 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): l = request.get_json() lot = Lot(**l) @@ -27,21 +47,46 @@ class LotView(View): return self.schema.jsonify(lot) 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', 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 = [] - for model in Path.query: # type: Path - path = deque(model.path.path.split('.')) - self._p(nodes, path) - return jsonify({ - 'items': nodes, - 'url': request.path - }) + if args['format'] == LotFormat.UiTree: + nodes = [] + for model in Path.query: # type: Path + path = deque(model.path.path.split('.')) + self._p(nodes, path) + return jsonify({ + '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): """Recursively creates the nested lot structure. diff --git a/tests/test_lot.py b/tests/test_lot.py index a5d93b04..9b620362 100644 --- a/tests/test_lot.py +++ b/tests/test_lot.py @@ -4,7 +4,7 @@ from flask import g from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db 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.lot.models import Lot, LotDevice 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__) def test_lot_device_relationship(): device = Desktop(serial_number='foo', @@ -40,6 +41,12 @@ def test_lot_device_relationship(): assert lot_device.created assert lot_device.author_id == g.user.id 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__) @@ -209,8 +216,9 @@ def test_post_get_lot(user: UserClient): assert not l['children'] -def test_post_add_children_view(user: UserClient): - """Tests adding children lots to a lot through the view.""" +def test_post_add_children_view_ui_tree_normal(user: UserClient): + """Tests adding children lots to a lot through the view and + GETting the results.""" parent, _ = user.post(({'name': 'Parent'}), res=Lot) child, _ = user.post(({'name': 'Child'}), res=Lot) parent, _ = user.post({}, @@ -221,16 +229,29 @@ def test_post_add_children_view(user: UserClient): child, _ = user.get(res=Lot, item=child['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 lots[0]['name'] == 'Parent' assert len(lots[0]['nodes']) == 1 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): """Tests adding a device to a lot using POST and removing it with DELETE.""" + # todo check with components with app.app_context(): device = Desktop(serial_number='foo', model='bar', @@ -244,7 +265,11 @@ def test_lot_post_add_remove_device_view(app: Devicehub, user: UserClient): res=Lot, item='{}/devices'.format(parent['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 lot, _ = user.delete(res=Lot, item='{}/devices'.format(parent['id']),