Sync snapshot; add event listeners to auto-update device relationships
This commit is contained in:
parent
5538e5ac69
commit
d2af894174
|
@ -1,5 +1,5 @@
|
|||
Events
|
||||
======
|
||||
######
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
@ -8,7 +8,7 @@ Events
|
|||
|
||||
|
||||
Rate
|
||||
----
|
||||
****
|
||||
Devicehub generates an rating for a device taking into consideration the
|
||||
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?
|
||||
|
||||
Snapshot
|
||||
--------
|
||||
********
|
||||
The Snapshot sets the physical information of the device (S/N, model...)
|
||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||
composition of its components (adding / removing them), and links tags
|
||||
|
@ -106,10 +106,16 @@ a device:
|
|||
perform ``Remove`` on the old parent.
|
||||
|
||||
Snapshots from Workbench
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
========================
|
||||
When processing a device from the Workbench, this one performs a Snapshot
|
||||
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``,
|
||||
is as follows:
|
||||
|
||||
|
@ -121,10 +127,11 @@ is as follows:
|
|||
|
||||
- Identification information about the device and components
|
||||
(S/N, model, physical characteristics...)
|
||||
- Tags.
|
||||
- Rate.
|
||||
- Benchmarks.
|
||||
- TestDataStorage.
|
||||
- ``Tags`` in a ``tags`` property in the ``device``.
|
||||
- ``Rate`` in an ``events`` property in the ``device``.
|
||||
- ``Benchmarks`` in an ``events`` property in each ``component``
|
||||
or ``device``.
|
||||
- ``TestDataStorage`` as in ``Benchmarks``.
|
||||
- An ordered set of **expected events**, defining which are the next
|
||||
events that Workbench will perform to the device in ideal
|
||||
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,
|
||||
Devicehub **closes** the ``Snapshot`` from 1.
|
||||
|
||||
Synced
|
||||
------
|
||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||
the events the following way:
|
||||
|
||||
- ``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``.
|
||||
|
||||
the events in an ``events`` property inside each affected ``component``
|
||||
or ``device``.
|
||||
|
||||
ToDispose and DisposeProduct
|
||||
----------------------------
|
||||
****************************
|
||||
There are four events for getting rid of devices:
|
||||
|
||||
- ``ToDispose``: The device is marked to be disposed.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Inventory
|
||||
=======
|
||||
#########
|
||||
|
||||
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``.
|
||||
|
||||
Query
|
||||
-----
|
||||
*****
|
||||
The query consists of 4 optional params:
|
||||
|
||||
- **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.
|
||||
|
||||
Result
|
||||
------
|
||||
******
|
||||
The result is a JSON object with the following fields:
|
||||
|
||||
- **devices**: A list of devices.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Tags
|
||||
====
|
||||
####
|
||||
Devicehub can generate tags, which are synthetic identifiers that
|
||||
identify a device in an organization. A tag has minimally two fields:
|
||||
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).
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Tag ID design
|
||||
~~~~~~~~~~~~~
|
||||
=============
|
||||
The eTag has a fixed schema for its ID: ``XXX-YYYYYYYYYYYYYY``, where:
|
||||
|
||||
- *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``.
|
||||
|
||||
Creating tags
|
||||
-------------
|
||||
*************
|
||||
You need to create a tag before linking it to a device. There are
|
||||
two ways of creating a tag:
|
||||
|
||||
|
@ -74,7 +74,7 @@ two ways of creating a tag:
|
|||
Note that tags cannot have a slash ``/``.
|
||||
|
||||
Linking a tag
|
||||
-------------
|
||||
*************
|
||||
Linking a tag is joining the tag with the device.
|
||||
|
||||
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.
|
||||
|
||||
Getting a device through its tag
|
||||
--------------------------------
|
||||
********************************
|
||||
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
|
||||
tag-id. In such case you should use ``GET /tags/<ngo>/<tag-id>/device``
|
||||
to inequivocally get the correct device (to develop).
|
||||
|
||||
Tags and migrations
|
||||
-------------------
|
||||
*******************
|
||||
Tags travel with the devices they are linked when migrating them. Future
|
||||
implementations can parameterize this.
|
||||
|
||||
|
|
|
@ -83,6 +83,10 @@ class Device(Thing):
|
|||
class Computer(Device):
|
||||
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):
|
||||
pass
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Dict, List, Set
|
|||
from colour import Color
|
||||
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, \
|
||||
EventWithOneDevice
|
||||
from ereuse_devicehub.resources.image.models import ImageList
|
||||
|
@ -49,6 +49,7 @@ class Computer(Device):
|
|||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.components = ... # type: Set[Component]
|
||||
self.events_parent = ... # type: Set[Event]
|
||||
|
||||
|
||||
class Desktop(Computer):
|
||||
|
@ -92,12 +93,12 @@ class GraphicCard(Component):
|
|||
|
||||
class DataStorage(Component):
|
||||
size = ... # type: Column
|
||||
interface = ... # type: Column
|
||||
interface = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.size = ... # type: int
|
||||
self.interface = ... # type: DataStorageInterface
|
||||
self.interface = ... # type: DataStorageInterface
|
||||
|
||||
|
||||
class HardDrive(DataStorage):
|
||||
|
@ -147,12 +148,12 @@ class Processor(Component):
|
|||
class RamModule(Component):
|
||||
size = ... # type: Column
|
||||
speed = ... # type: Column
|
||||
interface = ... # type: Column
|
||||
format = ... # type: Column
|
||||
interface = ... # type: Column
|
||||
format = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.size = ... # type: int
|
||||
self.speed = ... # type: float
|
||||
self.interface = ... # type: RamInterface
|
||||
self.format = ... # type: RamFormat
|
||||
self.interface = ... # type: RamInterface
|
||||
self.format = ... # type: RamFormat
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from marshmallow import post_load, pre_load
|
||||
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.enums import RamFormat, RamInterface
|
||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
||||
from teal.marshmallow import ValidationError
|
||||
|
||||
|
||||
class Device(Thing):
|
||||
|
@ -30,6 +32,31 @@ class Device(Thing):
|
|||
unit=UnitCodes.m,
|
||||
description='The height of the device in meters.')
|
||||
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):
|
||||
|
|
|
@ -108,7 +108,7 @@ class Sync:
|
|||
blacklist.add(db_component.id)
|
||||
except ResourceNotFound:
|
||||
db.session.add(component)
|
||||
db.session.flush()
|
||||
# db.session.flush()
|
||||
db_component = component
|
||||
is_new = True
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from collections import Iterable
|
||||
from typing import Set, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import g
|
||||
|
@ -7,10 +9,11 @@ from sqlalchemy.dialects.postgresql import UUID
|
|||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import backref, relationship
|
||||
from sqlalchemy.orm.events import AttributeEvents as Events
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
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, \
|
||||
FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
||||
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
|
||||
|
@ -96,6 +99,20 @@ class Event(Thing):
|
|||
relationship is filled with the components the computer had
|
||||
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
|
||||
@declared_attr
|
||||
|
@ -131,7 +148,7 @@ class EventWithOneDevice(Event):
|
|||
primaryjoin=Device.id == device_id)
|
||||
|
||||
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):
|
||||
|
@ -141,7 +158,8 @@ class EventWithMultipleDevices(Event):
|
|||
order_by=lambda: EventWithMultipleDevices.created,
|
||||
collection_class=OrderedSet),
|
||||
secondary=lambda: EventDevice.__table__,
|
||||
order_by=lambda: Device.id)
|
||||
order_by=lambda: Device.id,
|
||||
collection_class=OrderedSet)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
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)
|
||||
|
||||
|
||||
class Ready(EventWithMultipleDevices):
|
||||
pass
|
||||
|
||||
|
||||
class EraseSectors(EraseBasic):
|
||||
pass
|
||||
|
||||
|
@ -394,16 +416,70 @@ class BenchmarkRamSysbench(BenchmarkWithRate):
|
|||
|
||||
|
||||
# Listeners
|
||||
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
|
||||
@event.listens_for(Install.device, 'set', retval=True, propagate=True)
|
||||
@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True)
|
||||
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
|
||||
# Listeners validate values and keep relationships synced
|
||||
|
||||
# todo finish adding events
|
||||
# @event.listens_for(Install.snapshot, 'before_insert', propagate=True)
|
||||
# def validate_required_snapshot(mapper, connection, target: Event):
|
||||
# if not target.snapshot:
|
||||
# raise ValidationError('{0!r} must be linked to a Snapshot.'.format(target))
|
||||
@event.listens_for(TestDataStorage.device, Events.set.__name__, propagate=True)
|
||||
@event.listens_for(Install.device, Events.set.__name__, propagate=True)
|
||||
@event.listens_for(EraseBasic.device, Events.set.__name__, propagate=True)
|
||||
def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator):
|
||||
"""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 = ... # type: relationship
|
||||
components = ... # type: relationship
|
||||
parent_id = ... # type: Column
|
||||
parent = ... # type: relationship
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
@ -45,6 +47,8 @@ class Event(Thing):
|
|||
self.author_id = ... # type: UUID
|
||||
self.author = ... # type: User
|
||||
self.components = ... # type: Set[Component]
|
||||
self.parent_id = ... # type: Computer
|
||||
self.parent = ... # type: Computer
|
||||
|
||||
|
||||
class EventWithOneDevice(Event):
|
||||
|
@ -214,6 +218,10 @@ class EraseBasic(EventWithOneDevice):
|
|||
self.success = ... # type: bool
|
||||
|
||||
|
||||
class Ready(EventWithMultipleDevices):
|
||||
pass
|
||||
|
||||
|
||||
class EraseSectors(EraseBasic):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -29,11 +29,11 @@ class Event(Thing):
|
|||
|
||||
|
||||
class EventWithOneDevice(Event):
|
||||
device = NestedOn(Device, only='id')
|
||||
device = NestedOn(Device)
|
||||
|
||||
|
||||
class EventWithMultipleDevices(Event):
|
||||
devices = NestedOn(Device, many=True, only='id')
|
||||
devices = NestedOn(Device, many=True)
|
||||
|
||||
|
||||
class Add(EventWithOneDevice):
|
||||
|
@ -73,7 +73,6 @@ class EraseSectors(EraseBasic):
|
|||
|
||||
|
||||
class Step(Schema):
|
||||
id = Integer(dump_only=True)
|
||||
type = String(description='Only required when it is nested.')
|
||||
start_time = DateTime(required=True, data_key='startTime')
|
||||
end_time = DateTime(required=True, data_key='endTime')
|
||||
|
@ -176,7 +175,7 @@ class Snapshot(EventWithOneDevice):
|
|||
required=True,
|
||||
description='The software that generated this Snapshot.')
|
||||
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,
|
||||
many=True,
|
||||
data_key='expectedEvents',
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
from distutils.version import StrictVersion
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.device.models import Computer
|
||||
from ereuse_devicehub.resources.enums import SnapshotSoftware
|
||||
from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage
|
||||
from ereuse_devicehub.resources.device.models import Component, Computer
|
||||
from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware
|
||||
from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage, WorkbenchRate
|
||||
from teal.resource import View
|
||||
|
||||
|
||||
|
@ -33,18 +34,42 @@ class SnapshotView(View):
|
|||
# model object, when we flush them to the db we will flush
|
||||
# snapshot, and we want to wait to flush snapshot at the end
|
||||
device = s.pop('device') # type: Computer
|
||||
components = s.pop('components') if s['software'] == SnapshotSoftware.Workbench else None
|
||||
if 'events' in s:
|
||||
events = s.pop('events')
|
||||
# todo perform events
|
||||
# noinspection PyArgumentList
|
||||
components = s.pop('components') \
|
||||
if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component]
|
||||
snapshot = Snapshot(**s)
|
||||
snapshot.device, snapshot.events = self.resource_def.sync.run(device, components)
|
||||
snapshot.components = snapshot.device.components
|
||||
# todo compute rating
|
||||
|
||||
# Remove new events from devices so they don't interfere with sync
|
||||
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
|
||||
# the DB wants. Let's get a copy of the list so we preserve order
|
||||
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.commit()
|
||||
# todo we are setting snapshot dirty again with this components but
|
||||
|
|
|
@ -2,18 +2,18 @@ type: 'Snapshot'
|
|||
uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
||||
version: '11.0'
|
||||
software: 'Workbench'
|
||||
events:
|
||||
- type: 'WorkbenchRate'
|
||||
appearanceRange: 'A'
|
||||
functionalityRange: 'B'
|
||||
labelling: True
|
||||
bios: 'B'
|
||||
elapsed: 4
|
||||
device:
|
||||
type: 'Microtower'
|
||||
serialNumber: 'd1s'
|
||||
model: 'd1ml'
|
||||
manufacturer: 'd1mr'
|
||||
events:
|
||||
- type: 'WorkbenchRate'
|
||||
appearanceRange: 'A'
|
||||
functionalityRange: 'B'
|
||||
labelling: True
|
||||
bios: 'B'
|
||||
components:
|
||||
- type: 'GraphicCard'
|
||||
serialNumber: 'gc1s'
|
||||
|
|
|
@ -12,26 +12,26 @@ components:
|
|||
- type: 'SolidStateDrive'
|
||||
serialNumber: 'c1s'
|
||||
model: 'c1ml'
|
||||
manufacturer: 'pc1mr'
|
||||
erasure:
|
||||
type: 'EraseSectors'
|
||||
cleanWithZeros: True
|
||||
startTime: '2018-06-01T08:12:06'
|
||||
endTime: '2018-06-01T09:12:06'
|
||||
secureRandomSteps: 20
|
||||
steps:
|
||||
- type: 'StepZero'
|
||||
error: False
|
||||
startTime: '2018-06-01T08:15:00'
|
||||
endTime: '2018-06-01T09:16:00'
|
||||
secureRandomSteps: 1
|
||||
cleanWithZeros: True
|
||||
- type: 'StepZero'
|
||||
error: False
|
||||
startTime: '2018-06-01T08:16:00'
|
||||
endTime: '2018-06-01T09:17:00'
|
||||
secureRandomSteps: 1
|
||||
cleanWithZeros: True
|
||||
manufacturer: 'c1mr'
|
||||
events:
|
||||
- type: 'EraseSectors'
|
||||
cleanWithZeros: True
|
||||
startTime: '2018-06-01T08:12:06'
|
||||
endTime: '2018-06-01T09:12:06'
|
||||
secureRandomSteps: 20
|
||||
steps:
|
||||
- type: 'StepZero'
|
||||
error: False
|
||||
startTime: '2018-06-01T08:15:00'
|
||||
endTime: '2018-06-01T09:16:00'
|
||||
secureRandomSteps: 1
|
||||
cleanWithZeros: True
|
||||
- type: 'StepZero'
|
||||
error: False
|
||||
startTime: '2018-06-01T08:16:00'
|
||||
endTime: '2018-06-01T09:17:00'
|
||||
secureRandomSteps: 1
|
||||
cleanWithZeros: True
|
||||
- type: 'GraphicCard'
|
||||
serialNumber: 'gc1s'
|
||||
model: 'gc1ml'
|
||||
|
|
|
@ -2,13 +2,14 @@ from datetime import datetime, timedelta
|
|||
|
||||
import pytest
|
||||
from flask import g
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, \
|
||||
SolidStateDrive
|
||||
from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, Microtower, \
|
||||
RamModule, SolidStateDrive
|
||||
from ereuse_devicehub.resources.enums import TestHardDriveLength
|
||||
from ereuse_devicehub.resources.event.models import EraseBasic, EraseSectors, \
|
||||
EventWithOneDevice, Install, StepZero, TestDataStorage
|
||||
from ereuse_devicehub.resources.event.models import BenchmarkDataStorage, EraseBasic, EraseSectors, \
|
||||
EventWithOneDevice, Install, Ready, StepZero, StressTest, TestDataStorage
|
||||
from tests.conftest import create_user
|
||||
|
||||
|
||||
|
@ -125,3 +126,71 @@ def test_install():
|
|||
device=hdd)
|
||||
db.session.add(install)
|
||||
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'
|
||||
if perform_second_snapshot:
|
||||
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:
|
||||
return snapshot
|
||||
|
||||
|
@ -126,18 +126,27 @@ def test_snapshot_schema(app: Devicehub):
|
|||
|
||||
def test_snapshot_post(user: UserClient):
|
||||
"""
|
||||
Tests the post snapshot endpoint (validation, etc)
|
||||
and data correctness.
|
||||
Tests the post snapshot endpoint (validation, etc), 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['version'] == '11.0'
|
||||
assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9'
|
||||
assert snapshot['events'] == []
|
||||
assert snapshot['elapsed'] == 4
|
||||
assert snapshot['author']['id'] == user.user['id']
|
||||
assert 'events' 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):
|
||||
|
@ -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."""
|
||||
b = file('basic.snapshot')
|
||||
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():
|
||||
tag, *_ = Tag.query.all() # type: Tag
|
||||
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']
|
||||
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
|
||||
_snapshot1, _snapshot2, erasure = storage['events']
|
||||
assert erasure['type'] == 'EraseSectors'
|
||||
# order: creation time descending
|
||||
_snapshot1, erasure1, _snapshot2, erasure2 = storage['events']
|
||||
assert erasure1['type'] == erasure2['type'] == 'EraseSectors'
|
||||
assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot'
|
||||
assert snapshot == _snapshot2
|
||||
erasure, _ = user.get(res=EraseBasic, item=erasure['id'])
|
||||
assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0]
|
||||
erasure, _ = user.get(res=EraseBasic, item=erasure1['id'])
|
||||
assert len(erasure['steps']) == 2
|
||||
assert erasure['steps'][0]['startingTime'] == '2018-06-01T08:15:00'
|
||||
assert erasure['steps'][0]['endingTime'] == '2018-06-01T09:16:00'
|
||||
assert erasure['steps'][1]['endingTime'] == '2018-06-01T08:16:00'
|
||||
assert erasure['steps'][1]['endingTime'] == '2018-06-01T09:17:00'
|
||||
assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00'
|
||||
assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00'
|
||||
assert erasure['steps'][1]['startTime'] == '2018-06-01T08:16:00+00:00'
|
||||
assert erasure['steps'][1]['endTime'] == '2018-06-01T09:17:00+00:00'
|
||||
assert erasure['device']['id'] == storage['id']
|
||||
for step in erasure['steps']:
|
||||
assert step['type'] == 'StepZero'
|
||||
|
@ -320,4 +330,3 @@ def test_erase(user: UserClient):
|
|||
assert step['secureRandomSteps'] == 1
|
||||
assert step['cleanWithZeros'] is True
|
||||
assert 'num' not in step
|
||||
assert step['erasure'] == erasure['id']
|
||||
|
|
Reference in New Issue