Sync snapshot; add event listeners to auto-update device relationships

This commit is contained in:
Xavier Bustamante Talavera 2018-06-16 12:41:12 +02:00
parent 5538e5ac69
commit d2af894174
15 changed files with 332 additions and 111 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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):
@ -92,12 +93,12 @@ class GraphicCard(Component):
class DataStorage(Component): class DataStorage(Component):
size = ... # type: Column size = ... # type: Column
interface = ... # 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 self.interface = ... # type: DataStorageInterface
class HardDrive(DataStorage): class HardDrive(DataStorage):
@ -147,12 +148,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 interface = ... # type: Column
format = ... # 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.interface = ... # type: RamInterface
self.format = ... # type: RamFormat self.format = ... # type: RamFormat

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -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

View File

@ -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'

View File

@ -12,26 +12,26 @@ 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'
secureRandomSteps: 20 secureRandomSteps: 20
steps: steps:
- type: 'StepZero' - type: 'StepZero'
error: False error: False
startTime: '2018-06-01T08:15:00' startTime: '2018-06-01T08:15:00'
endTime: '2018-06-01T09:16:00' endTime: '2018-06-01T09:16:00'
secureRandomSteps: 1 secureRandomSteps: 1
cleanWithZeros: True cleanWithZeros: True
- type: 'StepZero' - type: 'StepZero'
error: False error: False
startTime: '2018-06-01T08:16:00' startTime: '2018-06-01T08:16:00'
endTime: '2018-06-01T09:17:00' endTime: '2018-06-01T09:17:00'
secureRandomSteps: 1 secureRandomSteps: 1
cleanWithZeros: True cleanWithZeros: True
- type: 'GraphicCard' - type: 'GraphicCard'
serialNumber: 'gc1s' serialNumber: 'gc1s'
model: 'gc1ml' model: 'gc1ml'

View File

@ -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

View File

@ -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']