Update inventory; pass many tests
This commit is contained in:
parent
b56fbeeca7
commit
e3a887d9fa
|
@ -2,7 +2,7 @@ Events
|
||||||
======
|
======
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
event-diagram
|
event-diagram
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ Rate
|
||||||
Devicehub generates an rating for a device taking into consideration the
|
Devicehub generates an rating for a device taking into consideration the
|
||||||
visual, functional, and performance.
|
visual, functional, and performance.
|
||||||
|
|
||||||
.. todo:: add performance as a result of component fusion + general tests in https://
|
.. todo:: add performance as a result of component fusion + general tests in `here <https://
|
||||||
github.com/eReuse/Rdevicescore/blob/master/img/input_process_output.png
|
github.com/eReuse/Rdevicescore/blob/master/img/input_process_output.png>`_.
|
||||||
|
|
||||||
A Workflow is as follows:
|
A Workflow is as follows:
|
||||||
|
|
||||||
|
@ -172,12 +172,10 @@ There are four events for getting rid of devices:
|
||||||
been recovered under a new product.
|
been recovered under a new product.
|
||||||
|
|
||||||
.. note:: For usability purposes, users might not directly perform
|
.. note:: For usability purposes, users might not directly perform
|
||||||
``Dispose``, but this could automatically be done when
|
``Dispose``, but this could automatically be done when
|
||||||
performing ``ToDispose`` + ``Receive`` to a
|
performing ``ToDispose`` + ``Receive`` to a ``RecyclingCenter``.
|
||||||
``RecyclingCenter``.
|
|
||||||
|
|
||||||
.. todo:: Ensure that ``Dispose`` is a ``Trade`` event. An Org could
|
.. todo:: Ensure that ``Dispose`` is a ``Trade`` event. An Org could
|
||||||
``Sell`` or ``Donate`` a device with the objective of
|
``Sell`` or ``Donate`` a device with the objective of disposing them.
|
||||||
disposing them. Is ``Dispose`` ok, or do we want to keep
|
Is ``Dispose`` ok, or do we want to keep that extra ``Sell`` or
|
||||||
that extra ``Sell`` or ``Donate`` event? Could dispose
|
``Donate`` event? Could dispose be a synonym of any of those?
|
||||||
be a synonym of any of those?
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
Getting
|
|
||||||
=======
|
|
||||||
|
|
||||||
Devicehub uses the same path to get devices and lots.
|
|
||||||
|
|
||||||
To get the lot information ::
|
|
||||||
|
|
||||||
GET /inventory/24
|
|
||||||
|
|
||||||
You can specifically filter devices::
|
|
||||||
|
|
||||||
GET /inventory?devices?
|
|
||||||
GET /inventory/24?type=24&type=44&status={"name": "Reserved", "updated": "2018-01-01"}
|
|
||||||
GET /inventory/25?price=24&price=21
|
|
||||||
|
|
||||||
GET /devices/4?
|
|
||||||
|
|
||||||
Returns devices that matches the filters and the lots that contain them.
|
|
||||||
If the filters are applied to the lots, it returns the matched lots
|
|
||||||
and the devices that contain them.
|
|
||||||
You can join filters.
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ This is the documentation and API of the `eReuse.org DeviceHub
|
||||||
|
|
||||||
events
|
events
|
||||||
tags
|
tags
|
||||||
|
inventory
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
Inventory
|
||||||
|
=======
|
||||||
|
|
||||||
|
Devicehub uses the same path to get devices and lots.
|
||||||
|
|
||||||
|
To get all devices and groups: ``GET /inventory`` or the devices of a
|
||||||
|
specific groups: ``GET /inventory/24``.
|
||||||
|
|
||||||
|
You can **filter** devices ``GET /inventory/24?filter={"type": "Computer"}``,
|
||||||
|
and **sort** them ``GET /inventory?sort={"created": 1}``, and of course
|
||||||
|
you can combine both in the same query. You only get the groups that
|
||||||
|
contain the devices that pass the filters. So, if a group contains
|
||||||
|
only one device that is filtered, you don't get that group neither.
|
||||||
|
|
||||||
|
Results are **paginated**; you get up to 30 devices and up to 30
|
||||||
|
groups in a page. Select the actual page by ``GET /inventory?page=3``.
|
||||||
|
By default you get the page number ``1``.
|
||||||
|
|
||||||
|
Query
|
||||||
|
-----
|
||||||
|
The query consists of 4 optional params:
|
||||||
|
|
||||||
|
- **search**: Filters devices by performing a full-text search over their
|
||||||
|
physical properties, events, tags, and groups they are in:
|
||||||
|
|
||||||
|
- Device.type
|
||||||
|
- Device.serial_number
|
||||||
|
- Device.model
|
||||||
|
- Device.manufacturer
|
||||||
|
- Device.color
|
||||||
|
- Tag.id
|
||||||
|
- Tag.org
|
||||||
|
- Group.name
|
||||||
|
|
||||||
|
Search is a string.
|
||||||
|
- **filter**: Filters devices field-by-field. Each field can be
|
||||||
|
filtered in different ways, see them in
|
||||||
|
:class:`ereuse_devicehub.resources.inventory.Filters`. Filter is
|
||||||
|
a JSON-encoded object whose keys are the filters. By default
|
||||||
|
is empty (no filter applied).
|
||||||
|
- **sort**: Sorts the devices. You can specify multiple sort clauses
|
||||||
|
as it is a JSON-encoded object whose keys are fields and values
|
||||||
|
are truthy for *ascending* order, or falsy for *descending* order.
|
||||||
|
By default it is sorted by ``Device.created`` descending (newest
|
||||||
|
devices first).
|
||||||
|
- **page**: A natural number that specifies the page to retrieve.
|
||||||
|
By default is ``1``; the first page.
|
||||||
|
|
||||||
|
Result
|
||||||
|
------
|
||||||
|
The result is a JSON object with the following fields:
|
||||||
|
|
||||||
|
- **devices**: A list of devices.
|
||||||
|
- **groups**: A list of groups.
|
||||||
|
- **widgets**: A dictionary of widgets.
|
||||||
|
- **pagination**: Pagination information:
|
||||||
|
|
||||||
|
- **page**: The page you requested in the ``page`` param of the query,
|
||||||
|
or ``1``.
|
||||||
|
- **perPage**: How many devices are in every page, fixed to ``30``.
|
||||||
|
- **total**: How many total devices passed the filters.
|
|
@ -1,10 +1,10 @@
|
||||||
from typing import Any, Dict, Iterable, Tuple, Type, Union, Generator
|
from inspect import isclass
|
||||||
|
from typing import Any, Dict, Iterable, Tuple, Type, Union
|
||||||
|
|
||||||
from boltons.typeutils import issubclass
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources import models, schemas
|
||||||
from ereuse_utils.test import JSON
|
from ereuse_utils.test import JSON
|
||||||
from teal.client import Client as TealClient
|
from teal.client import Client as TealClient
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
@ -21,7 +21,7 @@ class Client(TealClient):
|
||||||
|
|
||||||
def open(self,
|
def open(self,
|
||||||
uri: str,
|
uri: str,
|
||||||
res: Union[str, Type[Thing]] = None,
|
res: Union[str, Type[Union[models.Thing, schemas.Thing]]] = None,
|
||||||
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200,
|
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200,
|
||||||
query: Iterable[Tuple[str, Any]] = tuple(),
|
query: Iterable[Tuple[str, Any]] = tuple(),
|
||||||
accept=JSON,
|
accept=JSON,
|
||||||
|
@ -30,14 +30,14 @@ class Client(TealClient):
|
||||||
headers: dict = None,
|
headers: dict = None,
|
||||||
token: str = None,
|
token: str = None,
|
||||||
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
|
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
|
||||||
if issubclass(res, Thing):
|
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
|
||||||
res = res.__name__
|
res = res.t
|
||||||
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
|
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
|
||||||
**kw)
|
**kw)
|
||||||
|
|
||||||
def get(self,
|
def get(self,
|
||||||
uri: str = '',
|
uri: str = '',
|
||||||
res: Union[Type[Thing], str] = None,
|
res: Union[Type[Union[models.Thing, schemas.Thing]], str] = None,
|
||||||
query: Iterable[Tuple[str, Any]] = tuple(),
|
query: Iterable[Tuple[str, Any]] = tuple(),
|
||||||
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200,
|
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200,
|
||||||
item: Union[int, str] = None,
|
item: Union[int, str] = None,
|
||||||
|
@ -50,7 +50,7 @@ class Client(TealClient):
|
||||||
def post(self,
|
def post(self,
|
||||||
data: str or dict,
|
data: str or dict,
|
||||||
uri: str = '',
|
uri: str = '',
|
||||||
res: Union[Type[Thing], str] = None,
|
res: Union[Type[Union[models.Thing, schemas.Thing]], str] = None,
|
||||||
query: Iterable[Tuple[str, Any]] = tuple(),
|
query: Iterable[Tuple[str, Any]] = tuple(),
|
||||||
status: Union[int, Type[HTTPException], Type[ValidationError]] = 201,
|
status: Union[int, Type[HTTPException], Type[ValidationError]] = 201,
|
||||||
content_type: str = JSON,
|
content_type: str = JSON,
|
||||||
|
@ -67,7 +67,7 @@ class Client(TealClient):
|
||||||
return self.post({'email': email, 'password': password}, '/users/login', status=200)
|
return self.post({'email': email, 'password': password}, '/users/login', status=200)
|
||||||
|
|
||||||
def get_many(self,
|
def get_many(self,
|
||||||
res: Union[Type[Thing], str],
|
res: Union[Type[Union[models.Thing, schemas.Thing]], str],
|
||||||
resources: Iterable[dict],
|
resources: Iterable[dict],
|
||||||
key: str = None,
|
key: str = None,
|
||||||
headers: dict = None,
|
headers: dict = None,
|
||||||
|
@ -101,7 +101,7 @@ class UserClient(Client):
|
||||||
|
|
||||||
def open(self,
|
def open(self,
|
||||||
uri: str,
|
uri: str,
|
||||||
res: Union[str, Type[Thing]] = None,
|
res: Union[str, Type[Union[models.Thing, schemas.Thing]]] = None,
|
||||||
status: int or HTTPException = 200,
|
status: int or HTTPException = 200,
|
||||||
query: Iterable[Tuple[str, Any]] = tuple(),
|
query: Iterable[Tuple[str, Any]] = tuple(),
|
||||||
accept=JSON,
|
accept=JSON,
|
||||||
|
|
|
@ -3,14 +3,14 @@ from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Dict, Set
|
from typing import Dict, Set
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \
|
from sqlalchemy import BigInteger, Column, Enum as DBEnum, Float, ForeignKey, Integer, Sequence, \
|
||||||
Unicode, inspect, Enum as DBEnum
|
SmallInteger, Unicode, inspect
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.orm import ColumnProperty, backref, relationship
|
from sqlalchemy.orm import ColumnProperty, backref, relationship
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from sqlalchemy_utils import ColorType
|
from sqlalchemy_utils import ColorType
|
||||||
|
|
||||||
from ereuse_devicehub.resources.enums import DataStorageInterface, RamInterface, RamFormat
|
from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
||||||
from ereuse_utils.naming import Naming
|
from ereuse_utils.naming import Naming
|
||||||
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
|
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
|
||||||
|
|
|
@ -3,6 +3,7 @@ from typing import Dict, List, Set
|
||||||
from colour import Color
|
from colour import Color
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.enums import RamInterface, RamFormat, DataStorageInterface
|
||||||
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
|
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
|
||||||
EventWithOneDevice
|
EventWithOneDevice
|
||||||
from ereuse_devicehub.resources.image.models import ImageList
|
from ereuse_devicehub.resources.image.models import ImageList
|
||||||
|
@ -91,10 +92,12 @@ class GraphicCard(Component):
|
||||||
|
|
||||||
class DataStorage(Component):
|
class DataStorage(Component):
|
||||||
size = ... # type: Column
|
size = ... # type: Column
|
||||||
|
interface = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.size = ... # type: int
|
self.size = ... # type: int
|
||||||
|
self.interface = ... # type: DataStorageInterface
|
||||||
|
|
||||||
|
|
||||||
class HardDrive(DataStorage):
|
class HardDrive(DataStorage):
|
||||||
|
@ -144,8 +147,12 @@ class Processor(Component):
|
||||||
class RamModule(Component):
|
class RamModule(Component):
|
||||||
size = ... # type: Column
|
size = ... # type: Column
|
||||||
speed = ... # type: Column
|
speed = ... # type: Column
|
||||||
|
interface = ... # type: Column
|
||||||
|
format = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.size = ... # type: int
|
self.size = ... # type: int
|
||||||
self.speed = ... # type: float
|
self.speed = ... # type: float
|
||||||
|
self.interface = ... # type: RamInterface
|
||||||
|
self.format = ... # type: RamFormat
|
||||||
|
|
|
@ -366,16 +366,18 @@ class StressTest(Test):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Benchmark(EventWithOneDevice):
|
class Benchmark(JoinedTableMixin, EventWithOneDevice):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkDataStorage(Benchmark):
|
class BenchmarkDataStorage(Benchmark):
|
||||||
readSpeed = Column(Float(decimal_return_scale=2), nullable=False)
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
||||||
writeSpeed = Column(Float(decimal_return_scale=2), nullable=False)
|
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||||
|
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkWithRate(Benchmark):
|
class BenchmarkWithRate(Benchmark):
|
||||||
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
||||||
rate = Column(SmallInteger, nullable=False)
|
rate = Column(SmallInteger, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -395,7 +397,7 @@ class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||||
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
|
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
|
||||||
@event.listens_for(Install.device, 'set', retval=True, propagate=True)
|
@event.listens_for(Install.device, 'set', retval=True, propagate=True)
|
||||||
@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True)
|
@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True)
|
||||||
def validate_device_is_data_storage(target, value, old_value, initiator):
|
def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator):
|
||||||
if not isinstance(value, DataStorage):
|
if not isinstance(value, DataStorage):
|
||||||
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -8,7 +8,7 @@ from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
RatingSoftware, SnapshotSoftware, TestHardDriveLength, SnapshotExpectedEvents
|
RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
||||||
from ereuse_devicehub.resources.image.models import Image
|
from ereuse_devicehub.resources.image.models import Image
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user import User
|
from ereuse_devicehub.resources.user import User
|
||||||
|
@ -112,6 +112,7 @@ class Rate(EventWithOneDevice):
|
||||||
rating = ... # type: Column
|
rating = ... # type: Column
|
||||||
appearance = ... # type: Column
|
appearance = ... # type: Column
|
||||||
functionality = ... # type: Column
|
functionality = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.rating = ... # type: float
|
self.rating = ... # type: float
|
||||||
|
@ -216,3 +217,37 @@ class EraseBasic(EventWithOneDevice):
|
||||||
class EraseSectors(EraseBasic):
|
class EraseSectors(EraseBasic):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Benchmark(EventWithOneDevice):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkDataStorage(Benchmark):
|
||||||
|
read_speed = ... # type: Column
|
||||||
|
write_speed = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.read_speed = ... # type: float
|
||||||
|
self.write_speed = ... # type: float
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkWithRate(Benchmark):
|
||||||
|
rate = ... # type: Column
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.rate = ... # type: int
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkProcessor(BenchmarkWithRate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkProcessorSysbench(BenchmarkProcessor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||||
|
pass
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
from flask import current_app as app, jsonify
|
from flask import current_app, current_app as app, jsonify
|
||||||
|
from flask_sqlalchemy import Pagination
|
||||||
from marshmallow import Schema as MarshmallowSchema
|
from marshmallow import Schema as MarshmallowSchema
|
||||||
from marshmallow.fields import Float, Nested, Str
|
from marshmallow.fields import Float, Integer, Nested, Str
|
||||||
|
from marshmallow.validate import Range
|
||||||
|
from sqlalchemy import Column
|
||||||
|
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.event.models import Rate
|
from ereuse_devicehub.resources.event.models import Rate
|
||||||
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
from teal.marshmallow import IsType
|
from teal.query import Between, FullTextSearch, ILike, Join, Or, Query, Sort, SortField
|
||||||
from teal.query import Between, Equal, ILike, Or, Query
|
from teal.resource import Resource, View
|
||||||
from teal.resource import Resource, Schema, View
|
|
||||||
|
|
||||||
|
|
||||||
class Inventory(Schema):
|
class Inventory(Thing):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,23 +28,56 @@ class TagQ(Query):
|
||||||
org = ILike(Tag.org)
|
org = ILike(Tag.org)
|
||||||
|
|
||||||
|
|
||||||
|
class OfType(Str):
|
||||||
|
def __init__(self, column: Column, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def _deserialize(self, value, attr, data):
|
||||||
|
v = super()._deserialize(value, attr, data)
|
||||||
|
return self.column.in_(current_app.resources[v].subresources_types)
|
||||||
|
|
||||||
|
|
||||||
class Filters(Query):
|
class Filters(Query):
|
||||||
type = Or(Equal(Device.type, Str(validate=IsType(Device.t))))
|
type = Or(OfType(Device.type))
|
||||||
model = ILike(Device.model)
|
model = ILike(Device.model)
|
||||||
manufacturer = ILike(Device.manufacturer)
|
manufacturer = ILike(Device.manufacturer)
|
||||||
serialNumber = ILike(Device.serial_number)
|
serialNumber = ILike(Device.serial_number)
|
||||||
rating = Nested(RateQ) # todo db join
|
rating = Join(Device.id == Rate.device_id, RateQ)
|
||||||
tag = Nested(TagQ) # todo db join
|
tag = Join(Device.id == Tag.id, TagQ)
|
||||||
|
|
||||||
|
|
||||||
|
class Sorting(Sort):
|
||||||
|
created = SortField(Device.created)
|
||||||
|
|
||||||
|
|
||||||
class InventoryView(View):
|
class InventoryView(View):
|
||||||
class FindArgs(MarshmallowSchema):
|
class FindArgs(MarshmallowSchema):
|
||||||
where = Nested(Filters, default={})
|
search = FullTextSearch() # todo Develop this. See more at docs/inventory.
|
||||||
|
filter = Nested(Filters, missing=[])
|
||||||
|
sort = Nested(Sorting, missing=[Device.created.desc()])
|
||||||
|
page = Integer(validate=Range(min=1), missing=1)
|
||||||
|
|
||||||
def find(self, args):
|
def find(self, args: dict):
|
||||||
devices = Device.query.filter_by()
|
"""
|
||||||
|
Supports the inventory view of ``devicehub-client``; returns
|
||||||
|
all the devices, groups and widgets of this Devicehub instance.
|
||||||
|
|
||||||
|
The result can be filtered, sorted, and paginated.
|
||||||
|
"""
|
||||||
|
devices = Device.query \
|
||||||
|
.filter(*args['filter']) \
|
||||||
|
.order_by(*args['sort']) \
|
||||||
|
.paginate(page=args['page'], per_page=30) # type: Pagination
|
||||||
inventory = {
|
inventory = {
|
||||||
'devices': app.resources[Device.t].schema.dump()
|
'devices': app.resources[Device.t].schema.dump(devices.items, many=True),
|
||||||
|
'groups': [],
|
||||||
|
'widgets': {},
|
||||||
|
'pagination': {
|
||||||
|
'page': devices.page,
|
||||||
|
'perPage': devices.per_page,
|
||||||
|
'total': devices.total,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return jsonify(inventory)
|
return jsonify(inventory)
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ from uuid import UUID
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from colour import Color
|
from colour import Color
|
||||||
|
|
||||||
from ereuse_utils.naming import Naming
|
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
|
@ -20,6 +18,7 @@ from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, Mismatch
|
||||||
from ereuse_devicehub.resources.event.models import Remove, Test
|
from ereuse_devicehub.resources.event.models import Remove, Test
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
from ereuse_devicehub.resources.user import User
|
from ereuse_devicehub.resources.user import User
|
||||||
|
from ereuse_utils.naming import Naming
|
||||||
from teal.db import ResourceNotFound
|
from teal.db import ResourceNotFound
|
||||||
from tests.conftest import file
|
from tests.conftest import file
|
||||||
|
|
||||||
|
@ -133,7 +132,7 @@ def test_add_remove():
|
||||||
# c4 is not with any pc
|
# c4 is not with any pc
|
||||||
values = file('pc-components.db')
|
values = file('pc-components.db')
|
||||||
pc = values['device']
|
pc = values['device']
|
||||||
c1, c2 = [Component(**c) for c in values['components']]
|
c1, c2 = (Component(**c) for c in values['components'])
|
||||||
pc = Computer(**pc, components=OrderedSet([c1, c2]))
|
pc = Computer(**pc, components=OrderedSet([c1, c2]))
|
||||||
db.session.add(pc)
|
db.session.add(pc)
|
||||||
c3 = Component(serial_number='nc1')
|
c3 = Component(serial_number='nc1')
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.sql.elements import BinaryExpression
|
|
||||||
|
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.client import UserClient
|
||||||
from ereuse_devicehub.resources.inventory import Filters, InventoryView
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.resources.device.models import Desktop, Device, Laptop, Microtower, \
|
||||||
|
SolidStateDrive
|
||||||
|
from ereuse_devicehub.resources.inventory import Filters, Inventory, Sorting
|
||||||
from teal.utils import compiled
|
from teal.utils import compiled
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +13,7 @@ from teal.utils import compiled
|
||||||
def test_inventory_filters():
|
def test_inventory_filters():
|
||||||
schema = Filters()
|
schema = Filters()
|
||||||
q = schema.load({
|
q = schema.load({
|
||||||
'type': ['Microtower', 'Laptop'],
|
'type': ['Computer', 'Laptop'],
|
||||||
'manufacturer': 'Dell',
|
'manufacturer': 'Dell',
|
||||||
'rating': {
|
'rating': {
|
||||||
'rating': [3, 6],
|
'rating': [3, 6],
|
||||||
|
@ -22,28 +25,86 @@ def test_inventory_filters():
|
||||||
})
|
})
|
||||||
s, params = compiled(Device, q)
|
s, params = compiled(Device, q)
|
||||||
# Order between query clauses can change
|
# Order between query clauses can change
|
||||||
assert '(device.type = %(type_1)s OR device.type = %(type_2)s)' in s
|
assert '(device.type IN (%(type_1)s, %(type_2)s, %(type_3)s, %(type_4)s, ' \
|
||||||
|
'%(type_5)s, %(type_6)s) OR device.type IN (%(type_7)s))' in s
|
||||||
assert 'device.manufacturer ILIKE %(manufacturer_1)s' in s
|
assert 'device.manufacturer ILIKE %(manufacturer_1)s' in s
|
||||||
assert 'rate.rating BETWEEN %(rating_1)s AND %(rating_2)s' in s
|
assert 'rate.rating BETWEEN %(rating_1)s AND %(rating_2)s' in s
|
||||||
assert 'rate.appearance BETWEEN %(appearance_1)s AND %(appearance_2)s' in s
|
assert 'rate.appearance BETWEEN %(appearance_1)s AND %(appearance_2)s' in s
|
||||||
assert '(tag.id ILIKE %(id_1)s OR tag.id ILIKE %(id_2)s)' in s
|
assert '(tag.id ILIKE %(id_1)s OR tag.id ILIKE %(id_2)s)' in s
|
||||||
assert params == {
|
|
||||||
'type_1': 'Microtower',
|
# type_x can be assigned at different values
|
||||||
'rating_2': 6.0,
|
# ex: type_1 can be 'Desktop' in one execution but the next one 'Laptop'
|
||||||
'manufacturer_1': 'Dell%',
|
assert set(params.keys()) == {
|
||||||
'appearance_1': 2.0,
|
'id_1',
|
||||||
'appearance_2': 4.0,
|
'manufacturer_1',
|
||||||
'id_1': 'bcn-%',
|
'type_4',
|
||||||
'rating_1': 3.0,
|
'type_3',
|
||||||
'id_2': 'activa-02%',
|
'id_2',
|
||||||
'type_2': 'Laptop'
|
'type_1',
|
||||||
|
'rating_1',
|
||||||
|
'type_5',
|
||||||
|
'appearance_2',
|
||||||
|
'type_6',
|
||||||
|
'type_7',
|
||||||
|
'appearance_1',
|
||||||
|
'rating_2',
|
||||||
|
'type_2'
|
||||||
|
}
|
||||||
|
assert set(params.values()) == {
|
||||||
|
'bcn-%',
|
||||||
|
'Dell%',
|
||||||
|
'Laptop',
|
||||||
|
'Server',
|
||||||
|
'activa-02%',
|
||||||
|
'Computer',
|
||||||
|
3.0,
|
||||||
|
'Microtower',
|
||||||
|
4.0,
|
||||||
|
'Netbook',
|
||||||
|
'Laptop',
|
||||||
|
2.0,
|
||||||
|
6.0,
|
||||||
|
'Desktop'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('app_context')
|
@pytest.mark.usefixtures('app_context')
|
||||||
def test_inventory_query():
|
def test_inventory_sort():
|
||||||
schema = InventoryView.FindArgs()
|
schema = Sorting()
|
||||||
args = schema.load({
|
r = next(schema.load({'created': True}))
|
||||||
'where': {'type': ['Computer']}
|
assert str(r) == 'device.created ASC'
|
||||||
})
|
|
||||||
assert isinstance(args['where'], BinaryExpression), '``where`` must be a SQLAlchemy query'
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def inventory_query_dummy(app: Devicehub):
|
||||||
|
with app.app_context():
|
||||||
|
db.session.add_all(( # The order matters ;-)
|
||||||
|
Desktop(serial_number='s1', model='ml1', manufacturer='mr1'),
|
||||||
|
Laptop(serial_number='s3', model='ml3', manufacturer='mr3'),
|
||||||
|
Microtower(serial_number='s2', model='ml2', manufacturer='mr2'),
|
||||||
|
SolidStateDrive(serial_number='s4', model='ml4', manufacturer='mr4')
|
||||||
|
))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('inventory_query_dummy')
|
||||||
|
def test_inventory_query_no_filters(user: UserClient):
|
||||||
|
i, _ = user.get(res=Inventory)
|
||||||
|
assert tuple(d['type'] for d in i['devices']) == (
|
||||||
|
'SolidStateDrive', 'Microtower', 'Laptop', 'Desktop'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('inventory_query_dummy')
|
||||||
|
def test_inventory_query_filter_type(user: UserClient):
|
||||||
|
i, _ = user.get(res=Inventory, query=[('filter', {'type': ['Computer', 'Microtower']})])
|
||||||
|
assert tuple(d['type'] for d in i['devices']) == ('Microtower', 'Laptop', 'Desktop')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('inventory_query_dummy')
|
||||||
|
def test_inventory_query_filter_sort(user: UserClient):
|
||||||
|
i, _ = user.get(res=Inventory, query=[
|
||||||
|
('sort', {'created': Sorting.ASCENDING}),
|
||||||
|
('filter', {'type': ['Computer']})
|
||||||
|
])
|
||||||
|
assert tuple(d['type'] for d in i['devices']) == ('Desktop', 'Laptop', 'Microtower')
|
||||||
|
|
Reference in New Issue