Add inventory and missing fields

This commit is contained in:
Xavier Bustamante Talavera 2018-06-12 16:50:05 +02:00
parent 02a0332dd6
commit b56fbeeca7
14 changed files with 235 additions and 28 deletions

View File

@ -139,12 +139,12 @@ is as follows:
``Snapshot``.
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
and ``component`` IDs from 1, linking it to them. It repeats
this for all the erased data storage devices; **T2+Tn** being
this for all the erased data storage devices; **T3+Tn** being
*n* the erased data storage devices.
4. WorkbenchServer does like in 3. but for the event ``Install``,
finishing in **T2+Tn+Tx**, being *x* the number of data storage
finishing in **T3+Tn+Tx**, being *x* the number of data storage
devices with an OS installed into.
5. In **T2+Tn+Tx**, when all *expected events* have been performed,
5. In **T3+Tn+Tx**, when all *expected events* have been performed,
Devicehub **closes** the ``Snapshot`` from 1.
Optionally, Devicehub understands receiving a ``Snapshot`` with all

22
docs/getting.rst Normal file
View File

@ -0,0 +1,22 @@
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.

View File

@ -9,6 +9,7 @@ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, EventDef,
PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \
StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \
EraseSectorsDef
from ereuse_devicehub.resources.inventory import InventoryDef
from ereuse_devicehub.resources.tag import TagDef
from ereuse_devicehub.resources.user import OrganizationDef, UserDef
from teal.config import Config
@ -22,7 +23,7 @@ class DevicehubConfig(Config):
OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef,
StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef,
PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef,
TestDataStorageDef, WorkbenchRateDef
TestDataStorageDef, WorkbenchRateDef, InventoryDef
}
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str

View File

@ -4,12 +4,13 @@ from operator import attrgetter
from typing import Dict, Set
from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \
Unicode, inspect
Unicode, inspect, Enum as DBEnum
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import ColumnProperty, backref, relationship
from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType
from ereuse_devicehub.resources.enums import DataStorageInterface, RamInterface, RamFormat
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_utils.naming import Naming
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
@ -151,6 +152,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
class DataStorage(JoinedComponentTableMixin, Component):
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
interface = Column(DBEnum(DataStorageInterface))
class HardDrive(DataStorage):
@ -182,3 +184,5 @@ class Processor(JoinedComponentTableMixin, Component):
class RamModule(JoinedComponentTableMixin, Component):
size = Column(SmallInteger, check_range('size', min=128, max=17000))
speed = Column(Float, check_range('speed', min=100, max=10000))
interface = Column(DBEnum(RamInterface))
format = Column(DBEnum(RamFormat))

View File

@ -1,8 +1,10 @@
from marshmallow.fields import Float, Integer, Str
from marshmallow.validate import Length, OneOf, Range
from marshmallow_enum import EnumField
from sqlalchemy.util import OrderedSet
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.enums import RamInterface, RamFormat
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
@ -105,3 +107,5 @@ class Processor(Component):
class RamModule(Component):
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
interface = EnumField(RamInterface)
format = EnumField(RamFormat)

View File

@ -3,7 +3,6 @@ from enum import Enum, IntEnum, unique
from typing import Union
@unique
class SnapshotSoftware(Enum):
"""The algorithm_software used to perform the Snapshot."""
@ -125,3 +124,28 @@ class SnapshotExpectedEvents(Enum):
BOX_RATE_5 = 1, 5
BOX_RATE_3 = 1, 3
# After looking at own databases
@unique
class RamInterface(Enum):
DDR = 'DDR'
DDR2 = 'DDR2'
DDR3 = 'DDR3'
DDR4 = 'DDR4'
DDR5 = 'DDR5'
DDR6 = 'DDR6'
@unique
class RamFormat(Enum):
DIMM = 'DIMM'
SODIMM = 'SODIMM'
@unique
class DataStorageInterface(Enum):
ATA = 'ATA'
USB = 'USB'
PCI = 'PCI'

View File

@ -30,18 +30,37 @@ class JoinedTableMixin:
class Event(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
title = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
name = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
name.comment = """
A name or title for the event. Used when searching for events.
"""
type = Column(Unicode)
incidence = Column(Boolean, default=False, nullable=False)
closed = Column(Boolean, default=True, nullable=False)
incidence.comment = """
Should this event be reviewed due some anomaly?
"""
closed = Column(Boolean, default=True, nullable=False)
closed.comment = """
Whether the author has finished the event.
After this is set to True, no modifications are allowed.
"""
error = Column(Boolean, default=False, nullable=False)
error.comment = """
Did the event fail?
For example, a failure in ``Erase`` means that the data storage
unit did not erase correctly.
"""
description = Column(Unicode, default='', nullable=False)
description.comment = """
A comment about the event.
"""
date = Column(DateTime)
date.comment = """
When this event happened.
Leave it blank if it is happening now
(the field ``created`` is used instead).
This is used for example when creating events retroactively.
"""
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
use_alter=True,
name='snapshot_events'))
@ -347,6 +366,31 @@ class StressTest(Test):
pass
class Benchmark(EventWithOneDevice):
pass
class BenchmarkDataStorage(Benchmark):
readSpeed = Column(Float(decimal_return_scale=2), nullable=False)
writeSpeed = Column(Float(decimal_return_scale=2), nullable=False)
class BenchmarkWithRate(Benchmark):
rate = Column(SmallInteger, nullable=False)
class BenchmarkProcessor(BenchmarkWithRate):
pass
class BenchmarkProcessorSysbench(BenchmarkProcessor):
pass
class BenchmarkRamSysbench(BenchmarkWithRate):
pass
# Listeners
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
@event.listens_for(Install.device, 'set', retval=True, propagate=True)

View File

@ -17,9 +17,10 @@ from teal.db import Model
class Event(Thing):
id = ... # type: Column
title = ... # type: Column
name = ... # type: Column
date = ... # type: Column
type = ... # type: Column
error = ... # type: Column
incidence = ... # type: Column
description = ... # type: Column
finalized = ... # type: Column
@ -32,7 +33,7 @@ class Event(Thing):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.id = ... # type: UUID
self.title = ... # type: str
self.name = ... # type: str
self.type = ... # type: str
self.incidence = ... # type: bool
self.closed = ... # type: bool
@ -108,6 +109,9 @@ class SnapshotRequest(Model):
class Rate(EventWithOneDevice):
rating = ... # type: Column
appearance = ... # type: Column
functionality = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.rating = ... # type: float

View File

@ -8,6 +8,7 @@ from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device.schemas import Component, Device
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
RATE_POSITIVE, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
from ereuse_devicehub.resources.event import models as m
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.user.schemas import User
@ -17,18 +18,14 @@ from teal.resource import Schema
class Event(Thing):
id = Integer(dump_only=True)
title = String(default='',
validate=Length(STR_BIG_SIZE),
description='A name or title for the event. Used when searching for events.')
date = DateTime('iso', description='When this event happened. '
'Leave it blank if it is happening now. '
'This is used when creating events retroactively.')
error = Boolean(default=False, description='Did the event fail?')
incidence = Boolean(default=False,
description='Should this event be reviewed due some anomaly?')
name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment)
date = DateTime('iso', description=m.Event.date.comment)
error = Boolean(default=False, description=m.Event.error.comment)
incidence = Boolean(default=False, description=m.Event.incidence.comment)
snapshot = NestedOn('Snapshot', dump_only=True)
components = NestedOn(Component, dump_only=True, many=True)
description = String(default='', description='A comment about the event.')
description = String(default='', description=m.Event.description.comment)
author = NestedOn(User, dump_only=True, exclude=('token',))
class EventWithOneDevice(Event):

View File

@ -0,0 +1,52 @@
from flask import current_app as app, jsonify
from marshmallow import Schema as MarshmallowSchema
from marshmallow.fields import Float, Nested, Str
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.event.models import Rate
from ereuse_devicehub.resources.tag import Tag
from teal.marshmallow import IsType
from teal.query import Between, Equal, ILike, Or, Query
from teal.resource import Resource, Schema, View
class Inventory(Schema):
pass
class RateQ(Query):
rating = Between(Rate.rating, Float())
appearance = Between(Rate.appearance, Float())
functionality = Between(Rate.functionality, Float())
class TagQ(Query):
id = Or(ILike(Tag.id), required=True)
org = ILike(Tag.org)
class Filters(Query):
type = Or(Equal(Device.type, Str(validate=IsType(Device.t))))
model = ILike(Device.model)
manufacturer = ILike(Device.manufacturer)
serialNumber = ILike(Device.serial_number)
rating = Nested(RateQ) # todo db join
tag = Nested(TagQ) # todo db join
class InventoryView(View):
class FindArgs(MarshmallowSchema):
where = Nested(Filters, default={})
def find(self, args):
devices = Device.query.filter_by()
inventory = {
'devices': app.resources[Device.t].schema.dump()
}
return jsonify(inventory)
class InventoryDef(Resource):
SCHEMA = Inventory
VIEW = InventoryView
AUTH = True

View File

@ -11,4 +11,10 @@ STR_XSM_SIZE = 16
class Thing(db.Model):
__abstract__ = True
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
updated.comment = """
When this was last changed.
"""
created = db.Column(db.DateTime, default=datetime.utcnow)
created.comment = """
When Devicehub created this.
"""

View File

@ -3,7 +3,7 @@ from enum import Enum
from marshmallow import post_load
from marshmallow.fields import DateTime, List, String, URL
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources import models as m
from teal.resource import Schema
@ -22,9 +22,8 @@ class Thing(Schema):
type = String(description='Only required when it is nested.')
url = URL(dump_only=True, description='The URL of the resource.')
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
updated = DateTime('iso', dump_only=True)
created = DateTime('iso', dump_only=True)
author = NestedOn('User', dump_only=True, exclude=('token',))
updated = DateTime('iso', dump_only=True, description=m.Thing.updated)
created = DateTime('iso', dump_only=True, description=m.Thing.created)
@post_load
def remove_type(self, data: dict):

View File

@ -8,6 +8,7 @@ setup(
license='Affero',
author='eReuse.org team',
author_email='x.bustamante@ereuse.org',
include_package_data=True,
description='A system to manage devices focusing reuse.',
install_requires=[
'teal>=0.2.0a1',
@ -34,5 +35,5 @@ setup(
'Topic :: Internet :: WWW/HTTP :: HTTP Servers',
'Topic :: Software Development :: Libraries :: Python Modules',
'License :: OSI Approved :: GNU Affero General Public License v3'
],
]
)

49
tests/test_inventory.py Normal file
View File

@ -0,0 +1,49 @@
import pytest
from sqlalchemy.sql.elements import BinaryExpression
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.inventory import Filters, InventoryView
from teal.utils import compiled
@pytest.mark.usefixtures('app_context')
def test_inventory_filters():
schema = Filters()
q = schema.load({
'type': ['Microtower', 'Laptop'],
'manufacturer': 'Dell',
'rating': {
'rating': [3, 6],
'appearance': [2, 4]
},
'tag': {
'id': ['bcn-', 'activa-02']
}
})
s, params = compiled(Device, q)
# Order between query clauses can change
assert '(device.type = %(type_1)s OR device.type = %(type_2)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.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 params == {
'type_1': 'Microtower',
'rating_2': 6.0,
'manufacturer_1': 'Dell%',
'appearance_1': 2.0,
'appearance_2': 4.0,
'id_1': 'bcn-%',
'rating_1': 3.0,
'id_2': 'activa-02%',
'type_2': 'Laptop'
}
@pytest.mark.usefixtures('app_context')
def test_inventory_query():
schema = InventoryView.FindArgs()
args = schema.load({
'where': {'type': ['Computer']}
})
assert isinstance(args['where'], BinaryExpression), '``where`` must be a SQLAlchemy query'