Sync snapshot; add event listeners to auto-update device relationships
This commit is contained in:
parent
5538e5ac69
commit
d2af894174
|
@ -1,5 +1,5 @@
|
||||||
Events
|
Events
|
||||||
======
|
######
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
@ -8,7 +8,7 @@ Events
|
||||||
|
|
||||||
|
|
||||||
Rate
|
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.
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ The same ``ImageSet`` can be rated multiple times, generating a new
|
||||||
.. todo:: which info does photobox provide for each picture?
|
.. todo:: which info does photobox provide for each picture?
|
||||||
|
|
||||||
Snapshot
|
Snapshot
|
||||||
--------
|
********
|
||||||
The Snapshot sets the physical information of the device (S/N, model...)
|
The Snapshot sets the physical information of the device (S/N, model...)
|
||||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||||
composition of its components (adding / removing them), and links tags
|
composition of its components (adding / removing them), and links tags
|
||||||
|
@ -106,10 +106,16 @@ a device:
|
||||||
perform ``Remove`` on the old parent.
|
perform ``Remove`` on the old parent.
|
||||||
|
|
||||||
Snapshots from Workbench
|
Snapshots from Workbench
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
========================
|
||||||
When processing a device from the Workbench, this one performs a Snapshot
|
When processing a device from the Workbench, this one performs a Snapshot
|
||||||
and then performs more events (like testings, benchmarking...).
|
and then performs more events (like testings, benchmarking...).
|
||||||
|
|
||||||
|
There are two ways of sending this information. In an async way,
|
||||||
|
this is, submitting events as soon as Workbench performs then, or
|
||||||
|
submitting only one Snapshot event with all the other events embedded.
|
||||||
|
|
||||||
|
Asynced
|
||||||
|
-------
|
||||||
The use case, which is represented in the ``test_workbench_phases``,
|
The use case, which is represented in the ``test_workbench_phases``,
|
||||||
is as follows:
|
is as follows:
|
||||||
|
|
||||||
|
@ -121,10 +127,11 @@ is as follows:
|
||||||
|
|
||||||
- Identification information about the device and components
|
- Identification information about the device and components
|
||||||
(S/N, model, physical characteristics...)
|
(S/N, model, physical characteristics...)
|
||||||
- Tags.
|
- ``Tags`` in a ``tags`` property in the ``device``.
|
||||||
- Rate.
|
- ``Rate`` in an ``events`` property in the ``device``.
|
||||||
- Benchmarks.
|
- ``Benchmarks`` in an ``events`` property in each ``component``
|
||||||
- TestDataStorage.
|
or ``device``.
|
||||||
|
- ``TestDataStorage`` as in ``Benchmarks``.
|
||||||
- An ordered set of **expected events**, defining which are the next
|
- An ordered set of **expected events**, defining which are the next
|
||||||
events that Workbench will perform to the device in ideal
|
events that Workbench will perform to the device in ideal
|
||||||
conditions (device doesn't fail, no Internet drop...).
|
conditions (device doesn't fail, no Internet drop...).
|
||||||
|
@ -147,18 +154,14 @@ is as follows:
|
||||||
5. In **T3+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.
|
Devicehub **closes** the ``Snapshot`` from 1.
|
||||||
|
|
||||||
|
Synced
|
||||||
|
------
|
||||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||||
the events the following way:
|
the events in an ``events`` property inside each affected ``component``
|
||||||
|
or ``device``.
|
||||||
- ``Install`` embedded in a ``installation`` field in its respective
|
|
||||||
``DataStorage`` component in the ``Snapshot``.
|
|
||||||
- ``Erase`` embedded in ``erasure`` field in its respective
|
|
||||||
``DataStorage`` in the ``Snapshot``.
|
|
||||||
- ``StressTest`` in an ``events`` field in the ``Snapshot``.
|
|
||||||
|
|
||||||
|
|
||||||
ToDispose and DisposeProduct
|
ToDispose and DisposeProduct
|
||||||
----------------------------
|
****************************
|
||||||
There are four events for getting rid of devices:
|
There are four events for getting rid of devices:
|
||||||
|
|
||||||
- ``ToDispose``: The device is marked to be disposed.
|
- ``ToDispose``: The device is marked to be disposed.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Inventory
|
Inventory
|
||||||
=======
|
#########
|
||||||
|
|
||||||
Devicehub uses the same path to get devices and lots.
|
Devicehub uses the same path to get devices and lots.
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ groups in a page. Select the actual page by ``GET /inventory?page=3``.
|
||||||
By default you get the page number ``1``.
|
By default you get the page number ``1``.
|
||||||
|
|
||||||
Query
|
Query
|
||||||
-----
|
*****
|
||||||
The query consists of 4 optional params:
|
The query consists of 4 optional params:
|
||||||
|
|
||||||
- **search**: Filters devices by performing a full-text search over their
|
- **search**: Filters devices by performing a full-text search over their
|
||||||
|
@ -47,7 +47,7 @@ The query consists of 4 optional params:
|
||||||
By default is ``1``; the first page.
|
By default is ``1``; the first page.
|
||||||
|
|
||||||
Result
|
Result
|
||||||
------
|
******
|
||||||
The result is a JSON object with the following fields:
|
The result is a JSON object with the following fields:
|
||||||
|
|
||||||
- **devices**: A list of devices.
|
- **devices**: A list of devices.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Tags
|
Tags
|
||||||
====
|
####
|
||||||
Devicehub can generate tags, which are synthetic identifiers that
|
Devicehub can generate tags, which are synthetic identifiers that
|
||||||
identify a device in an organization. A tag has minimally two fields:
|
identify a device in an organization. A tag has minimally two fields:
|
||||||
the ID and the Registration Number of the organization that generated
|
the ID and the Registration Number of the organization that generated
|
||||||
|
@ -26,7 +26,7 @@ Note that these virtual tags don't have to forcefully be printed or
|
||||||
have a physical representation (this is not imposed at system level).
|
have a physical representation (this is not imposed at system level).
|
||||||
|
|
||||||
The eReuse.org tags (eTag)
|
The eReuse.org tags (eTag)
|
||||||
--------------------------
|
**************************
|
||||||
We recognize a special type of tag, the **eReuse.org tags (eTag)**.
|
We recognize a special type of tag, the **eReuse.org tags (eTag)**.
|
||||||
These are tags defined by eReuse.org and that can be issued only
|
These are tags defined by eReuse.org and that can be issued only
|
||||||
by tag providers that comply with the eReuse.org requisites.
|
by tag providers that comply with the eReuse.org requisites.
|
||||||
|
@ -41,7 +41,7 @@ software, eReuse.org certified tag providers can create and manage
|
||||||
the tags, and send them to Devicehubs of their choice.
|
the tags, and send them to Devicehubs of their choice.
|
||||||
|
|
||||||
Tag ID design
|
Tag ID design
|
||||||
~~~~~~~~~~~~~
|
=============
|
||||||
The eTag has a fixed schema for its ID: ``XXX-YYYYYYYYYYYYYY``, where:
|
The eTag has a fixed schema for its ID: ``XXX-YYYYYYYYYYYYYY``, where:
|
||||||
|
|
||||||
- *XX* is the **eReuse.org Tag Provider ID (eTagPId)**.
|
- *XX* is the **eReuse.org Tag Provider ID (eTagPId)**.
|
||||||
|
@ -59,7 +59,7 @@ As an example, ``FO-A4CZ2`` is a tag from the ``FO`` tag provider
|
||||||
and ID ``A4CZ2``.
|
and ID ``A4CZ2``.
|
||||||
|
|
||||||
Creating tags
|
Creating tags
|
||||||
-------------
|
*************
|
||||||
You need to create a tag before linking it to a device. There are
|
You need to create a tag before linking it to a device. There are
|
||||||
two ways of creating a tag:
|
two ways of creating a tag:
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ two ways of creating a tag:
|
||||||
Note that tags cannot have a slash ``/``.
|
Note that tags cannot have a slash ``/``.
|
||||||
|
|
||||||
Linking a tag
|
Linking a tag
|
||||||
-------------
|
*************
|
||||||
Linking a tag is joining the tag with the device.
|
Linking a tag is joining the tag with the device.
|
||||||
|
|
||||||
In Devicehub this process is done when performing a Snapshot (POST
|
In Devicehub this process is done when performing a Snapshot (POST
|
||||||
|
@ -91,14 +91,14 @@ too in finding devices when these don't generate a ``HID``. Find more
|
||||||
in the ``Snapshot`` docs.
|
in the ``Snapshot`` docs.
|
||||||
|
|
||||||
Getting a device through its tag
|
Getting a device through its tag
|
||||||
--------------------------------
|
********************************
|
||||||
When performing ``GET /tags/<tag-id>/device`` you will get directly the
|
When performing ``GET /tags/<tag-id>/device`` you will get directly the
|
||||||
device of such tag, as long as there are not two tags with the same
|
device of such tag, as long as there are not two tags with the same
|
||||||
tag-id. In such case you should use ``GET /tags/<ngo>/<tag-id>/device``
|
tag-id. In such case you should use ``GET /tags/<ngo>/<tag-id>/device``
|
||||||
to inequivocally get the correct device (to develop).
|
to inequivocally get the correct device (to develop).
|
||||||
|
|
||||||
Tags and migrations
|
Tags and migrations
|
||||||
-------------------
|
*******************
|
||||||
Tags travel with the devices they are linked when migrating them. Future
|
Tags travel with the devices they are linked when migrating them. Future
|
||||||
implementations can parameterize this.
|
implementations can parameterize this.
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,10 @@ class Device(Thing):
|
||||||
class Computer(Device):
|
class Computer(Device):
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def events(self) -> list:
|
||||||
|
return sorted(chain(super().events, self.events_parent), key=attrgetter('created'))
|
||||||
|
|
||||||
|
|
||||||
class Desktop(Computer):
|
class Desktop(Computer):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -3,7 +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.enums import DataStorageInterface, RamFormat, RamInterface
|
||||||
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
|
||||||
|
@ -49,6 +49,7 @@ class Computer(Device):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.components = ... # type: Set[Component]
|
self.components = ... # type: Set[Component]
|
||||||
|
self.events_parent = ... # type: Set[Event]
|
||||||
|
|
||||||
|
|
||||||
class Desktop(Computer):
|
class Desktop(Computer):
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
from marshmallow import post_load, pre_load
|
||||||
from marshmallow.fields import Float, Integer, Str
|
from marshmallow.fields import Float, Integer, Str
|
||||||
from marshmallow.validate import Length, OneOf, Range
|
from marshmallow.validate import Length, OneOf, Range
|
||||||
from marshmallow_enum import EnumField
|
from marshmallow_enum import EnumField
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.enums import RamInterface, RamFormat
|
from ereuse_devicehub.resources.enums import RamFormat, RamInterface
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||||
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
||||||
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class Device(Thing):
|
class Device(Thing):
|
||||||
|
@ -30,6 +32,31 @@ class Device(Thing):
|
||||||
unit=UnitCodes.m,
|
unit=UnitCodes.m,
|
||||||
description='The height of the device in meters.')
|
description='The height of the device in meters.')
|
||||||
events = NestedOn('Event', many=True, dump_only=True)
|
events = NestedOn('Event', many=True, dump_only=True)
|
||||||
|
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def from_events_to_events_one(self, data: dict):
|
||||||
|
"""
|
||||||
|
Not an elegant way of allowing submitting events to a device
|
||||||
|
(in the context of Snapshots) without creating an ``events``
|
||||||
|
field at the model (which is not possible).
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Note that it is secure to allow uploading events_one
|
||||||
|
# as the only time an user can send a device object is
|
||||||
|
# in snapshots.
|
||||||
|
data['events_one'] = data.pop('events', [])
|
||||||
|
return data
|
||||||
|
|
||||||
|
@post_load
|
||||||
|
def validate_snapshot_events(self, data):
|
||||||
|
"""Validates that only snapshot-related events can be uploaded."""
|
||||||
|
from ereuse_devicehub.resources.event.models import EraseBasic, Test, Rate, Install
|
||||||
|
for event in data['events_one']:
|
||||||
|
if not isinstance(event, (Install, EraseBasic, Rate, Test)):
|
||||||
|
raise ValidationError('You cannot upload {}'.format(event['type']),
|
||||||
|
field_names='events')
|
||||||
|
|
||||||
|
|
||||||
class Computer(Device):
|
class Computer(Device):
|
||||||
|
|
|
@ -108,7 +108,7 @@ class Sync:
|
||||||
blacklist.add(db_component.id)
|
blacklist.add(db_component.id)
|
||||||
except ResourceNotFound:
|
except ResourceNotFound:
|
||||||
db.session.add(component)
|
db.session.add(component)
|
||||||
db.session.flush()
|
# db.session.flush()
|
||||||
db_component = component
|
db_component = component
|
||||||
is_new = True
|
is_new = True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from collections import Iterable
|
||||||
|
from typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
@ -7,10 +9,11 @@ from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
from sqlalchemy.orm import backref, relationship
|
from sqlalchemy.orm import backref, relationship
|
||||||
|
from sqlalchemy.orm.events import AttributeEvents as Events
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Component, DataStorage, Device
|
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
|
||||||
FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
||||||
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
||||||
|
@ -96,6 +99,20 @@ class Event(Thing):
|
||||||
relationship is filled with the components the computer had
|
relationship is filled with the components the computer had
|
||||||
at the time of the event.
|
at the time of the event.
|
||||||
"""
|
"""
|
||||||
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||||
|
parent = relationship(Computer,
|
||||||
|
backref=backref('events_parent',
|
||||||
|
lazy=True,
|
||||||
|
order_by=lambda: Event.created,
|
||||||
|
collection_class=OrderedSet),
|
||||||
|
primaryjoin=parent_id == Computer.id)
|
||||||
|
"""
|
||||||
|
For events that are performed to components, the device parent
|
||||||
|
at that time.
|
||||||
|
|
||||||
|
For example: for a ``EraseBasic`` performed on a data storage, this
|
||||||
|
would point to the computer that contained this data storage, if any.
|
||||||
|
"""
|
||||||
|
|
||||||
# noinspection PyMethodParameters
|
# noinspection PyMethodParameters
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -131,7 +148,7 @@ class EventWithOneDevice(Event):
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id!r} device={0.device_id}>'.format(self)
|
return '<{0.t} {0.id!r} device={0.device!r}>'.format(self)
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
|
@ -141,7 +158,8 @@ class EventWithMultipleDevices(Event):
|
||||||
order_by=lambda: EventWithMultipleDevices.created,
|
order_by=lambda: EventWithMultipleDevices.created,
|
||||||
collection_class=OrderedSet),
|
collection_class=OrderedSet),
|
||||||
secondary=lambda: EventDevice.__table__,
|
secondary=lambda: EventDevice.__table__,
|
||||||
order_by=lambda: Device.id)
|
order_by=lambda: Device.id,
|
||||||
|
collection_class=OrderedSet)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id!r} devices={0.devices!r}>'.format(self)
|
return '<{0.t} {0.id!r} devices={0.devices!r}>'.format(self)
|
||||||
|
@ -182,6 +200,10 @@ class EraseBasic(JoinedTableMixin, EventWithOneDevice):
|
||||||
clean_with_zeros = Column(Boolean, nullable=False)
|
clean_with_zeros = Column(Boolean, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Ready(EventWithMultipleDevices):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EraseSectors(EraseBasic):
|
class EraseSectors(EraseBasic):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -394,16 +416,70 @@ class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||||
|
|
||||||
|
|
||||||
# Listeners
|
# Listeners
|
||||||
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
|
# Listeners validate values and keep relationships synced
|
||||||
@event.listens_for(Install.device, 'set', retval=True, propagate=True)
|
|
||||||
@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True)
|
|
||||||
def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator):
|
|
||||||
if not isinstance(value, DataStorage):
|
|
||||||
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
# todo finish adding events
|
@event.listens_for(TestDataStorage.device, Events.set.__name__, propagate=True)
|
||||||
# @event.listens_for(Install.snapshot, 'before_insert', propagate=True)
|
@event.listens_for(Install.device, Events.set.__name__, propagate=True)
|
||||||
# def validate_required_snapshot(mapper, connection, target: Event):
|
@event.listens_for(EraseBasic.device, Events.set.__name__, propagate=True)
|
||||||
# if not target.snapshot:
|
def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator):
|
||||||
# raise ValidationError('{0!r} must be linked to a Snapshot.'.format(target))
|
"""Validates that the device for data-storage events is effectively a data storage."""
|
||||||
|
if value and not isinstance(value, DataStorage):
|
||||||
|
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
||||||
|
|
||||||
|
|
||||||
|
# The following listeners keep relationships with device <-> components synced with the event
|
||||||
|
# So, if you add or remove devices from events these listeners will
|
||||||
|
# automatically add/remove the ``components`` and ``parent`` of such events
|
||||||
|
# See the tests for examples
|
||||||
|
|
||||||
|
@event.listens_for(EventWithOneDevice.device, Events.set.__name__, propagate=True)
|
||||||
|
def update_components_event_one(target: EventWithOneDevice, device: Device, __, ___):
|
||||||
|
"""
|
||||||
|
Syncs the :attr:`.Event.components` with the components in
|
||||||
|
:attr:`ereuse_devicehub.resources.device.models.Computer.components`.
|
||||||
|
"""
|
||||||
|
target.components.clear()
|
||||||
|
if isinstance(device, Computer):
|
||||||
|
target.components |= device.components
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(EventWithMultipleDevices.devices, Events.init_collection.__name__,
|
||||||
|
propagate=True)
|
||||||
|
@event.listens_for(EventWithMultipleDevices.devices, Events.bulk_replace.__name__, propagate=True)
|
||||||
|
@event.listens_for(EventWithMultipleDevices.devices, Events.append.__name__, propagate=True)
|
||||||
|
def update_components_event_multiple(target: EventWithMultipleDevices,
|
||||||
|
value: Union[Set[Device], Device], _):
|
||||||
|
"""
|
||||||
|
Syncs the :attr:`.Event.components` with the components in
|
||||||
|
:attr:`ereuse_devicehub.resources.device.models.Computer.components`.
|
||||||
|
"""
|
||||||
|
target.components.clear()
|
||||||
|
devices = value if isinstance(value, Iterable) else {value}
|
||||||
|
for device in devices:
|
||||||
|
if isinstance(device, Computer):
|
||||||
|
target.components |= device.components
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(EventWithMultipleDevices.devices, Events.remove.__name__, propagate=True)
|
||||||
|
def remove_components_event_multiple(target: EventWithMultipleDevices, device: Device, __):
|
||||||
|
"""
|
||||||
|
Syncs the :attr:`.Event.components` with the components in
|
||||||
|
:attr:`ereuse_devicehub.resources.device.models.Computer.components`.
|
||||||
|
"""
|
||||||
|
target.components.clear()
|
||||||
|
for device in target.devices - {device}:
|
||||||
|
if isinstance(device, Computer):
|
||||||
|
target.components |= device.components
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(EraseBasic.device, Events.set.__name__, propagate=True)
|
||||||
|
@event.listens_for(Test.device, Events.set.__name__, propagate=True)
|
||||||
|
@event.listens_for(Install.device, Events.set.__name__, propagate=True)
|
||||||
|
@event.listens_for(Benchmark.device, Events.set.__name__, propagate=True)
|
||||||
|
def update_parent(target: Union[EraseBasic, Test, Install], device: Device, _, __):
|
||||||
|
"""
|
||||||
|
Syncs the :attr:`Event.parent` with the parent of the device.
|
||||||
|
"""
|
||||||
|
target.parent = None
|
||||||
|
if isinstance(device, Component):
|
||||||
|
target.parent = device.parent
|
||||||
|
|
|
@ -29,6 +29,8 @@ class Event(Thing):
|
||||||
author_id = ... # type: Column
|
author_id = ... # type: Column
|
||||||
author = ... # type: relationship
|
author = ... # type: relationship
|
||||||
components = ... # type: relationship
|
components = ... # type: relationship
|
||||||
|
parent_id = ... # type: Column
|
||||||
|
parent = ... # type: relationship
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -45,6 +47,8 @@ class Event(Thing):
|
||||||
self.author_id = ... # type: UUID
|
self.author_id = ... # type: UUID
|
||||||
self.author = ... # type: User
|
self.author = ... # type: User
|
||||||
self.components = ... # type: Set[Component]
|
self.components = ... # type: Set[Component]
|
||||||
|
self.parent_id = ... # type: Computer
|
||||||
|
self.parent = ... # type: Computer
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
|
@ -214,6 +218,10 @@ class EraseBasic(EventWithOneDevice):
|
||||||
self.success = ... # type: bool
|
self.success = ... # type: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Ready(EventWithMultipleDevices):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EraseSectors(EraseBasic):
|
class EraseSectors(EraseBasic):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
|
@ -29,11 +29,11 @@ class Event(Thing):
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(Event):
|
class EventWithOneDevice(Event):
|
||||||
device = NestedOn(Device, only='id')
|
device = NestedOn(Device)
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
devices = NestedOn(Device, many=True, only='id')
|
devices = NestedOn(Device, many=True)
|
||||||
|
|
||||||
|
|
||||||
class Add(EventWithOneDevice):
|
class Add(EventWithOneDevice):
|
||||||
|
@ -73,7 +73,6 @@ class EraseSectors(EraseBasic):
|
||||||
|
|
||||||
|
|
||||||
class Step(Schema):
|
class Step(Schema):
|
||||||
id = Integer(dump_only=True)
|
|
||||||
type = String(description='Only required when it is nested.')
|
type = String(description='Only required when it is nested.')
|
||||||
start_time = DateTime(required=True, data_key='startTime')
|
start_time = DateTime(required=True, data_key='startTime')
|
||||||
end_time = DateTime(required=True, data_key='endTime')
|
end_time = DateTime(required=True, data_key='endTime')
|
||||||
|
@ -176,7 +175,7 @@ class Snapshot(EventWithOneDevice):
|
||||||
required=True,
|
required=True,
|
||||||
description='The software that generated this Snapshot.')
|
description='The software that generated this Snapshot.')
|
||||||
version = Version(required=True, description='The version of the software.')
|
version = Version(required=True, description='The version of the software.')
|
||||||
events = NestedOn(Event, many=True) # todo ensure only specific events are submitted
|
events = NestedOn(Event, many=True, dump_only=True)
|
||||||
expected_events = EnumField(SnapshotExpectedEvents,
|
expected_events = EnumField(SnapshotExpectedEvents,
|
||||||
many=True,
|
many=True,
|
||||||
data_key='expectedEvents',
|
data_key='expectedEvents',
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Computer
|
from ereuse_devicehub.resources.device.models import Component, Computer
|
||||||
from ereuse_devicehub.resources.enums import SnapshotSoftware
|
from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware
|
||||||
from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage
|
from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage, WorkbenchRate
|
||||||
from teal.resource import View
|
from teal.resource import View
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,18 +34,42 @@ class SnapshotView(View):
|
||||||
# model object, when we flush them to the db we will flush
|
# model object, when we flush them to the db we will flush
|
||||||
# snapshot, and we want to wait to flush snapshot at the end
|
# snapshot, and we want to wait to flush snapshot at the end
|
||||||
device = s.pop('device') # type: Computer
|
device = s.pop('device') # type: Computer
|
||||||
components = s.pop('components') if s['software'] == SnapshotSoftware.Workbench else None
|
components = s.pop('components') \
|
||||||
if 'events' in s:
|
if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component]
|
||||||
events = s.pop('events')
|
|
||||||
# todo perform events
|
|
||||||
# noinspection PyArgumentList
|
|
||||||
snapshot = Snapshot(**s)
|
snapshot = Snapshot(**s)
|
||||||
snapshot.device, snapshot.events = self.resource_def.sync.run(device, components)
|
|
||||||
snapshot.components = snapshot.device.components
|
# Remove new events from devices so they don't interfere with sync
|
||||||
# todo compute rating
|
events_device = set(e for e in device.events_one)
|
||||||
|
events_components = tuple(set(e for e in component.events_one) for component in components)
|
||||||
|
device.events_one.clear()
|
||||||
|
for component in components:
|
||||||
|
component.events_one.clear()
|
||||||
|
|
||||||
|
# noinspection PyArgumentList
|
||||||
|
assert not device.events_one
|
||||||
|
assert all(not c.events_one for c in components)
|
||||||
|
db_device, remove_events = self.resource_def.sync.run(device, components)
|
||||||
|
snapshot.device = db_device
|
||||||
|
snapshot.events |= remove_events | events_device
|
||||||
# commit will change the order of the components by what
|
# commit will change the order of the components by what
|
||||||
# the DB wants. Let's get a copy of the list so we preserve order
|
# the DB wants. Let's get a copy of the list so we preserve order
|
||||||
ordered_components = OrderedSet(x for x in snapshot.components)
|
ordered_components = OrderedSet(x for x in snapshot.components)
|
||||||
|
|
||||||
|
for event in events_device:
|
||||||
|
if isinstance(event, WorkbenchRate):
|
||||||
|
# todo process workbench rate
|
||||||
|
event.data_storage = 2
|
||||||
|
event.graphic_card = 4
|
||||||
|
event.processor = 1
|
||||||
|
event.algorithm_software = RatingSoftware.Ereuse
|
||||||
|
event.algorithm_version = StrictVersion('1.0')
|
||||||
|
|
||||||
|
# Add the new events to the db-existing devices and components
|
||||||
|
db_device.events_one |= events_device
|
||||||
|
for component, events in zip(ordered_components, events_components):
|
||||||
|
component.events_one |= events
|
||||||
|
snapshot.events |= events
|
||||||
|
|
||||||
db.session.add(snapshot)
|
db.session.add(snapshot)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# todo we are setting snapshot dirty again with this components but
|
# todo we are setting snapshot dirty again with this components but
|
||||||
|
|
|
@ -2,18 +2,18 @@ type: 'Snapshot'
|
||||||
uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
||||||
version: '11.0'
|
version: '11.0'
|
||||||
software: 'Workbench'
|
software: 'Workbench'
|
||||||
events:
|
|
||||||
- type: 'WorkbenchRate'
|
|
||||||
appearanceRange: 'A'
|
|
||||||
functionalityRange: 'B'
|
|
||||||
labelling: True
|
|
||||||
bios: 'B'
|
|
||||||
elapsed: 4
|
elapsed: 4
|
||||||
device:
|
device:
|
||||||
type: 'Microtower'
|
type: 'Microtower'
|
||||||
serialNumber: 'd1s'
|
serialNumber: 'd1s'
|
||||||
model: 'd1ml'
|
model: 'd1ml'
|
||||||
manufacturer: 'd1mr'
|
manufacturer: 'd1mr'
|
||||||
|
events:
|
||||||
|
- type: 'WorkbenchRate'
|
||||||
|
appearanceRange: 'A'
|
||||||
|
functionalityRange: 'B'
|
||||||
|
labelling: True
|
||||||
|
bios: 'B'
|
||||||
components:
|
components:
|
||||||
- type: 'GraphicCard'
|
- type: 'GraphicCard'
|
||||||
serialNumber: 'gc1s'
|
serialNumber: 'gc1s'
|
||||||
|
|
|
@ -12,9 +12,9 @@ components:
|
||||||
- type: 'SolidStateDrive'
|
- type: 'SolidStateDrive'
|
||||||
serialNumber: 'c1s'
|
serialNumber: 'c1s'
|
||||||
model: 'c1ml'
|
model: 'c1ml'
|
||||||
manufacturer: 'pc1mr'
|
manufacturer: 'c1mr'
|
||||||
erasure:
|
events:
|
||||||
type: 'EraseSectors'
|
- type: 'EraseSectors'
|
||||||
cleanWithZeros: True
|
cleanWithZeros: True
|
||||||
startTime: '2018-06-01T08:12:06'
|
startTime: '2018-06-01T08:12:06'
|
||||||
endTime: '2018-06-01T09:12:06'
|
endTime: '2018-06-01T09:12:06'
|
||||||
|
|
|
@ -2,13 +2,14 @@ from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import g
|
from flask import g
|
||||||
|
from sqlalchemy.util import OrderedSet
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, \
|
from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, Microtower, \
|
||||||
SolidStateDrive
|
RamModule, SolidStateDrive
|
||||||
from ereuse_devicehub.resources.enums import TestHardDriveLength
|
from ereuse_devicehub.resources.enums import TestHardDriveLength
|
||||||
from ereuse_devicehub.resources.event.models import EraseBasic, EraseSectors, \
|
from ereuse_devicehub.resources.event.models import BenchmarkDataStorage, EraseBasic, EraseSectors, \
|
||||||
EventWithOneDevice, Install, StepZero, TestDataStorage
|
EventWithOneDevice, Install, Ready, StepZero, StressTest, TestDataStorage
|
||||||
from tests.conftest import create_user
|
from tests.conftest import create_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,3 +126,71 @@ def test_install():
|
||||||
device=hdd)
|
device=hdd)
|
||||||
db.session.add(install)
|
db.session.add(install)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('auth_app_context')
|
||||||
|
def test_update_components_event_one():
|
||||||
|
computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
|
computer.components.add(hdd)
|
||||||
|
|
||||||
|
# Add event
|
||||||
|
test = StressTest(elapsed=timedelta(seconds=1))
|
||||||
|
computer.events_one.add(test)
|
||||||
|
assert test.device == computer
|
||||||
|
assert next(iter(test.components)) == hdd, 'Event has to have new components'
|
||||||
|
|
||||||
|
# Remove event
|
||||||
|
computer.events_one.clear()
|
||||||
|
assert not test.device
|
||||||
|
assert not test.components, 'Event has to loose the components'
|
||||||
|
|
||||||
|
# If we add a component to a device AFTER assigning the event
|
||||||
|
# to the device, the event doesn't get the new component
|
||||||
|
computer.events_one.add(test)
|
||||||
|
ram = RamModule()
|
||||||
|
computer.components.add(ram)
|
||||||
|
assert len(test.components) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('auth_app_context')
|
||||||
|
def test_update_components_event_multiple():
|
||||||
|
computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
|
computer.components.add(hdd)
|
||||||
|
|
||||||
|
ready = Ready()
|
||||||
|
assert not ready.devices
|
||||||
|
assert not ready.components
|
||||||
|
|
||||||
|
# Add
|
||||||
|
computer.events_multiple.add(ready)
|
||||||
|
assert ready.devices == OrderedSet([computer])
|
||||||
|
assert next(iter(ready.components)) == hdd
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
computer.events_multiple.remove(ready)
|
||||||
|
assert not ready.devices
|
||||||
|
assert not ready.components
|
||||||
|
|
||||||
|
# init / replace collection
|
||||||
|
ready.devices = OrderedSet([computer])
|
||||||
|
assert ready.devices
|
||||||
|
assert ready.components
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('auth_app_context')
|
||||||
|
def test_update_parent():
|
||||||
|
computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1')
|
||||||
|
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
|
||||||
|
computer.components.add(hdd)
|
||||||
|
|
||||||
|
# Add
|
||||||
|
benchmark = BenchmarkDataStorage()
|
||||||
|
benchmark.device = hdd
|
||||||
|
assert benchmark.parent == computer
|
||||||
|
assert not benchmark.components
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
benchmark.device = None
|
||||||
|
assert not benchmark.parent
|
||||||
|
|
|
@ -74,7 +74,7 @@ def snapshot_and_check(user: UserClient,
|
||||||
'Components must be in their parent'
|
'Components must be in their parent'
|
||||||
if perform_second_snapshot:
|
if perform_second_snapshot:
|
||||||
input_snapshot['uuid'] = uuid4()
|
input_snapshot['uuid'] = uuid4()
|
||||||
return snapshot_and_check(user, input_snapshot, perform_second_snapshot=False)
|
return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False)
|
||||||
else:
|
else:
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
@ -126,18 +126,27 @@ def test_snapshot_schema(app: Devicehub):
|
||||||
|
|
||||||
def test_snapshot_post(user: UserClient):
|
def test_snapshot_post(user: UserClient):
|
||||||
"""
|
"""
|
||||||
Tests the post snapshot endpoint (validation, etc)
|
Tests the post snapshot endpoint (validation, etc), data correctness,
|
||||||
and data correctness.
|
and relationship correctness.
|
||||||
"""
|
"""
|
||||||
snapshot = snapshot_and_check(user, file('basic.snapshot'), perform_second_snapshot=False)
|
snapshot = snapshot_and_check(user, file('basic.snapshot'),
|
||||||
|
event_types=('WorkbenchRate',),
|
||||||
|
perform_second_snapshot=False)
|
||||||
assert snapshot['software'] == 'Workbench'
|
assert snapshot['software'] == 'Workbench'
|
||||||
assert snapshot['version'] == '11.0'
|
assert snapshot['version'] == '11.0'
|
||||||
assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
||||||
assert snapshot['events'] == []
|
|
||||||
assert snapshot['elapsed'] == 4
|
assert snapshot['elapsed'] == 4
|
||||||
assert snapshot['author']['id'] == user.user['id']
|
assert snapshot['author']['id'] == user.user['id']
|
||||||
assert 'events' not in snapshot['device']
|
assert 'events' not in snapshot['device']
|
||||||
assert 'author' not in snapshot['device']
|
assert 'author' not in snapshot['device']
|
||||||
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
|
assert snapshot['components'] == device['components']
|
||||||
|
|
||||||
|
assert tuple(c['type'] for c in snapshot['components']) == ('GraphicCard', 'RamModule')
|
||||||
|
rate, _ = user.get(res=Event, item=snapshot['events'][0]['id'])
|
||||||
|
assert rate['device']['id'] == snapshot['device']['id']
|
||||||
|
assert rate['components'] == snapshot['components']
|
||||||
|
assert rate['snapshot']['id'] == snapshot['id']
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_component_add_remove(user: UserClient):
|
def test_snapshot_component_add_remove(user: UserClient):
|
||||||
|
@ -279,7 +288,7 @@ def test_snapshot_tag_inner_tag(tag_id: str, user: UserClient, app: Devicehub):
|
||||||
"""Tests a posting Snapshot with a local tag."""
|
"""Tests a posting Snapshot with a local tag."""
|
||||||
b = file('basic.snapshot')
|
b = file('basic.snapshot')
|
||||||
b['device']['tags'] = [{'type': 'Tag', 'id': tag_id}]
|
b['device']['tags'] = [{'type': 'Tag', 'id': tag_id}]
|
||||||
snapshot_and_check(user, b)
|
snapshot_and_check(user, b, event_types=('WorkbenchRate',))
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
tag, *_ = Tag.query.all() # type: Tag
|
tag, *_ = Tag.query.all() # type: Tag
|
||||||
assert tag.device_id == 1, 'Tag should be linked to the first device'
|
assert tag.device_id == 1, 'Tag should be linked to the first device'
|
||||||
|
@ -303,16 +312,17 @@ def test_erase(user: UserClient):
|
||||||
storage, *_ = snapshot['components']
|
storage, *_ = snapshot['components']
|
||||||
assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order'
|
assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order'
|
||||||
storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too
|
storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too
|
||||||
_snapshot1, _snapshot2, erasure = storage['events']
|
# order: creation time descending
|
||||||
assert erasure['type'] == 'EraseSectors'
|
_snapshot1, erasure1, _snapshot2, erasure2 = storage['events']
|
||||||
|
assert erasure1['type'] == erasure2['type'] == 'EraseSectors'
|
||||||
assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot'
|
assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot'
|
||||||
assert snapshot == _snapshot2
|
assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0]
|
||||||
erasure, _ = user.get(res=EraseBasic, item=erasure['id'])
|
erasure, _ = user.get(res=EraseBasic, item=erasure1['id'])
|
||||||
assert len(erasure['steps']) == 2
|
assert len(erasure['steps']) == 2
|
||||||
assert erasure['steps'][0]['startingTime'] == '2018-06-01T08:15:00'
|
assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00'
|
||||||
assert erasure['steps'][0]['endingTime'] == '2018-06-01T09:16:00'
|
assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00'
|
||||||
assert erasure['steps'][1]['endingTime'] == '2018-06-01T08:16:00'
|
assert erasure['steps'][1]['startTime'] == '2018-06-01T08:16:00+00:00'
|
||||||
assert erasure['steps'][1]['endingTime'] == '2018-06-01T09:17:00'
|
assert erasure['steps'][1]['endTime'] == '2018-06-01T09:17:00+00:00'
|
||||||
assert erasure['device']['id'] == storage['id']
|
assert erasure['device']['id'] == storage['id']
|
||||||
for step in erasure['steps']:
|
for step in erasure['steps']:
|
||||||
assert step['type'] == 'StepZero'
|
assert step['type'] == 'StepZero'
|
||||||
|
@ -320,4 +330,3 @@ def test_erase(user: UserClient):
|
||||||
assert step['secureRandomSteps'] == 1
|
assert step['secureRandomSteps'] == 1
|
||||||
assert step['cleanWithZeros'] is True
|
assert step['cleanWithZeros'] is True
|
||||||
assert 'num' not in step
|
assert 'num' not in step
|
||||||
assert step['erasure'] == erasure['id']
|
|
||||||
|
|
Reference in New Issue