2019-06-19 11:35:26 +00:00
|
|
|
|
"""This file contains all actions can apply to a device and is sorted according
|
2019-05-14 18:31:43 +00:00
|
|
|
|
to a structure based on:
|
2019-04-23 19:27:31 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
* Generic Actions
|
2019-04-23 19:27:31 +00:00
|
|
|
|
* Benchmarks
|
|
|
|
|
* Tests
|
|
|
|
|
* Rates
|
|
|
|
|
* Prices
|
|
|
|
|
|
|
|
|
|
Within the above general classes are subclasses in A order.
|
|
|
|
|
"""
|
|
|
|
|
|
2020-12-04 15:58:53 +00:00
|
|
|
|
import copy
|
2018-06-16 10:41:12 +00:00
|
|
|
|
from collections import Iterable
|
2019-04-30 00:02:23 +00:00
|
|
|
|
from contextlib import suppress
|
2019-05-10 16:00:38 +00:00
|
|
|
|
from datetime import datetime, timedelta, timezone
|
2018-10-15 09:21:21 +00:00
|
|
|
|
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP
|
2019-05-08 17:12:05 +00:00
|
|
|
|
from typing import Optional, Set, Union
|
2018-06-10 16:47:49 +00:00
|
|
|
|
from uuid import uuid4
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
import inflection
|
2018-11-08 16:37:14 +00:00
|
|
|
|
import teal.db
|
2018-10-05 15:13:23 +00:00
|
|
|
|
from boltons import urlutils
|
2018-09-30 10:29:33 +00:00
|
|
|
|
from citext import CIText
|
2018-07-14 14:41:22 +00:00
|
|
|
|
from flask import current_app as app, g
|
2019-05-10 16:00:38 +00:00
|
|
|
|
from sortedcontainers import SortedSet
|
2019-02-04 17:20:50 +00:00
|
|
|
|
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Enum as DBEnum, \
|
2018-11-08 16:37:14 +00:00
|
|
|
|
Float, ForeignKey, Integer, Interval, JSON, Numeric, SmallInteger, Unicode, event, orm
|
2018-07-02 10:52:54 +00:00
|
|
|
|
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, validates
|
|
|
|
|
from sqlalchemy.orm.events import AttributeEvents as Events
|
|
|
|
|
from sqlalchemy.util import OrderedSet
|
2021-03-19 10:53:04 +00:00
|
|
|
|
from teal.db import (CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID,
|
2021-01-13 17:11:41 +00:00
|
|
|
|
POLYMORPHIC_ON, StrictVersionType, URL, check_lower, check_range, ResourceNotFound)
|
2018-09-06 17:43:59 +00:00
|
|
|
|
from teal.enums import Country, Currency, Subdivision
|
|
|
|
|
from teal.marshmallow import ValidationError
|
2018-10-05 15:13:23 +00:00
|
|
|
|
from teal.resource import url_for_resource
|
2018-07-14 14:41:22 +00:00
|
|
|
|
|
|
|
|
|
from ereuse_devicehub.db import db
|
2018-08-03 16:15:08 +00:00
|
|
|
|
from ereuse_devicehub.resources.agent.models import Agent
|
2018-07-14 14:41:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
|
|
|
|
Device, Laptop, Server
|
2019-05-08 17:12:05 +00:00
|
|
|
|
from ereuse_devicehub.resources.enums import AppearanceRange, BatteryHealth, BiosAccessRange, \
|
|
|
|
|
ErasureStandards, FunctionalityRange, PhysicalErasureMethod, PriceSoftware, \
|
2020-11-21 18:10:31 +00:00
|
|
|
|
R_NEGATIVE, R_POSITIVE, RatingRange, Severity, SnapshotSoftware, \
|
2019-05-11 14:27:22 +00:00
|
|
|
|
TestDataStorageLength
|
2018-09-30 10:29:33 +00:00
|
|
|
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
2018-07-14 14:41:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.user.models import User
|
2021-05-14 10:58:56 +00:00
|
|
|
|
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
class JoinedTableMixin:
|
2018-06-10 16:47:49 +00:00
|
|
|
|
# noinspection PyMethodParameters
|
2018-04-10 15:06:39 +00:00
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(Action.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
_sorted_actions = {
|
|
|
|
|
'order_by': lambda: Action.end_time,
|
2019-05-10 16:00:38 +00:00
|
|
|
|
'collection_class': SortedSet
|
|
|
|
|
}
|
2019-05-11 14:27:22 +00:00
|
|
|
|
"""For db.backref, return the actions sorted by end_time."""
|
2019-05-10 16:00:38 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Action(Thing):
|
|
|
|
|
"""Action performed on a device.
|
2019-02-03 16:12:53 +00:00
|
|
|
|
|
|
|
|
|
This class extends `Schema's Action <https://schema.org/Action>`_.
|
|
|
|
|
"""
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
2019-02-07 12:47:42 +00:00
|
|
|
|
type = Column(Unicode, nullable=False)
|
2018-09-30 10:29:33 +00:00
|
|
|
|
name = Column(CIText(), default='', nullable=False)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
name.comment = """A name or title for the action. Used when searching
|
|
|
|
|
for actions.
|
2018-06-12 14:50:05 +00:00
|
|
|
|
"""
|
2018-11-08 16:37:14 +00:00
|
|
|
|
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
|
|
|
|
|
severity.comment = Severity.__doc__
|
2018-10-13 12:53:46 +00:00
|
|
|
|
closed = Column(Boolean, default=True, nullable=False)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
closed.comment = """Whether the author has finished the action.
|
|
|
|
|
After this is set to True, no modifications are allowed.
|
|
|
|
|
By default actions are closed when performed.
|
2018-06-10 16:47:49 +00:00
|
|
|
|
"""
|
2018-04-27 17:16:43 +00:00
|
|
|
|
description = Column(Unicode, default='', nullable=False)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
description.comment = """A comment about the action."""
|
2018-08-09 19:46:54 +00:00
|
|
|
|
start_time = Column(db.TIMESTAMP(timezone=True))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
start_time.comment = """When the action starts. For some actions like
|
|
|
|
|
reservations the time when they are available, for others like renting
|
|
|
|
|
when the renting starts.
|
2018-08-03 16:15:08 +00:00
|
|
|
|
"""
|
2018-08-09 19:46:54 +00:00
|
|
|
|
end_time = Column(db.TIMESTAMP(timezone=True))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
end_time.comment = """When the action ends. For some actions like reservations
|
|
|
|
|
the time when they expire, for others like renting
|
2020-11-21 18:10:31 +00:00
|
|
|
|
the time the end rents. For punctual actions it is the time
|
2019-06-19 11:35:26 +00:00
|
|
|
|
they are performed; it differs with ``created`` in which
|
|
|
|
|
created is the where the system received the action.
|
2018-06-12 14:50:05 +00:00
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
|
|
|
|
|
use_alter=True,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
name='snapshot_actions'))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
snapshot = relationship('Snapshot',
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions',
|
2018-05-30 10:49:40 +00:00
|
|
|
|
lazy=True,
|
2018-06-10 16:47:49 +00:00
|
|
|
|
cascade=CASCADE_OWN,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
**_sorted_actions),
|
|
|
|
|
primaryjoin='Action.snapshot_id == Snapshot.id')
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-04-30 17:58:19 +00:00
|
|
|
|
author_id = Column(UUID(as_uuid=True),
|
|
|
|
|
ForeignKey(User.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.id)
|
2018-08-03 16:15:08 +00:00
|
|
|
|
# todo compute the org
|
2018-04-10 15:06:39 +00:00
|
|
|
|
author = relationship(User,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('authored_actions', lazy=True, collection_class=set),
|
2018-04-10 15:06:39 +00:00
|
|
|
|
primaryjoin=author_id == User.id)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
author_id.comment = """The user that recorded this action in the system.
|
2020-11-21 18:10:31 +00:00
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
This does not necessarily has to be the person that produced
|
|
|
|
|
the action in the real world. For that purpose see
|
|
|
|
|
``agent``.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
agent_id = Column(UUID(as_uuid=True),
|
|
|
|
|
ForeignKey(Agent.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.individual.id)
|
|
|
|
|
# todo compute the org
|
|
|
|
|
agent = relationship(Agent,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions_agent', lazy=True, **_sorted_actions),
|
2018-08-08 19:25:53 +00:00
|
|
|
|
primaryjoin=agent_id == Agent.id)
|
2020-11-21 18:10:31 +00:00
|
|
|
|
agent_id.comment = """The direct performer or driver of the action. e.g. John wrote a book.
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
It can differ with the user that registered the action in the
|
|
|
|
|
system, which can be in their behalf.
|
|
|
|
|
"""
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
components = relationship(Component,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions_components', lazy=True, **_sorted_actions),
|
|
|
|
|
secondary=lambda: ActionComponent.__table__,
|
2018-06-10 16:47:49 +00:00
|
|
|
|
order_by=lambda: Component.id,
|
2018-05-30 10:49:40 +00:00
|
|
|
|
collection_class=OrderedSet)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
components.comment = """The components that are affected by the action.
|
2021-03-19 10:53:04 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
When performing actions to parent devices their components are
|
2018-06-10 16:47:49 +00:00
|
|
|
|
affected too.
|
2020-11-21 18:10:31 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
For example: an ``Allocate`` is performed to a Computer and this
|
|
|
|
|
relationship is filled with the components the computer had
|
2019-05-11 14:27:22 +00:00
|
|
|
|
at the time of the action.
|
2020-11-21 18:10:31 +00:00
|
|
|
|
|
2018-06-16 13:33:56 +00:00
|
|
|
|
For Add and Remove though, this has another meaning: the components
|
|
|
|
|
that are added or removed.
|
2018-06-10 16:47:49 +00:00
|
|
|
|
"""
|
2019-02-07 12:47:42 +00:00
|
|
|
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
2018-06-16 10:41:12 +00:00
|
|
|
|
parent = relationship(Computer,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions_parent', lazy=True, **_sorted_actions),
|
2018-06-16 10:41:12 +00:00
|
|
|
|
primaryjoin=parent_id == Computer.id)
|
2020-11-21 18:10:31 +00:00
|
|
|
|
parent_id.comment = """For actions that are performed to components,
|
2019-06-19 11:35:26 +00:00
|
|
|
|
the device parent at that time.
|
2021-03-19 10:53:04 +00:00
|
|
|
|
|
2018-06-16 10:41:12 +00:00
|
|
|
|
For example: for a ``EraseBasic`` performed on a data storage, this
|
|
|
|
|
would point to the computer that contained this data storage, if any.
|
|
|
|
|
"""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
__table_args__ = (
|
|
|
|
|
db.Index('ix_id', id, postgresql_using='hash'),
|
|
|
|
|
db.Index('ix_type', type, postgresql_using='hash'),
|
|
|
|
|
db.Index('ix_parent_id', parent_id, postgresql_using='hash')
|
|
|
|
|
)
|
|
|
|
|
|
2018-11-21 13:26:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def elapsed(self):
|
|
|
|
|
"""Returns the elapsed time with seconds precision."""
|
|
|
|
|
t = self.end_time - self.start_time
|
|
|
|
|
return timedelta(seconds=t.seconds)
|
|
|
|
|
|
2018-10-05 15:13:23 +00:00
|
|
|
|
@property
|
|
|
|
|
def url(self) -> urlutils.URL:
|
2019-05-11 14:27:22 +00:00
|
|
|
|
"""The URL where to GET this action."""
|
|
|
|
|
return urlutils.URL(url_for_resource(Action, item_id=self.id))
|
2018-10-05 15:13:23 +00:00
|
|
|
|
|
2018-11-21 13:26:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def certificate(self) -> Optional[urlutils.URL]:
|
|
|
|
|
return None
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
# noinspection PyMethodParameters
|
2018-04-10 15:06:39 +00:00
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
2018-05-13 13:13:12 +00:00
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
2019-05-11 14:27:22 +00:00
|
|
|
|
if cls.t == 'Action':
|
2018-04-10 15:06:39 +00:00
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
2018-11-08 16:37:14 +00:00
|
|
|
|
# noinspection PyUnresolvedReferences
|
2018-04-10 15:06:39 +00:00
|
|
|
|
if JoinedTableMixin in cls.mro():
|
2019-05-11 14:27:22 +00:00
|
|
|
|
args[INHERIT_COND] = cls.id == Action.id
|
2018-04-10 15:06:39 +00:00
|
|
|
|
return args
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
@validates('end_time')
|
|
|
|
|
def validate_end_time(self, _, end_time: datetime):
|
|
|
|
|
if self.start_time and end_time <= self.start_time:
|
2019-05-11 14:27:22 +00:00
|
|
|
|
raise ValidationError('The action cannot finish before it starts.')
|
2018-08-03 16:15:08 +00:00
|
|
|
|
return end_time
|
|
|
|
|
|
|
|
|
|
@validates('start_time')
|
|
|
|
|
def validate_start_time(self, _, start_time: datetime):
|
|
|
|
|
if self.end_time and start_time >= self.end_time:
|
2019-05-11 14:27:22 +00:00
|
|
|
|
raise ValidationError('The action cannot start after it finished.')
|
2018-08-03 16:15:08 +00:00
|
|
|
|
return start_time
|
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
@property
|
2018-11-21 13:26:56 +00:00
|
|
|
|
def date_str(self):
|
2019-05-10 16:00:38 +00:00
|
|
|
|
return '{:%c}'.format(self.end_time)
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
|
|
|
# sortedset forces us to do this before calling our parent init
|
|
|
|
|
self.end_time = kwargs.get('end_time', None)
|
|
|
|
|
if not self.end_time:
|
|
|
|
|
# Set default for end_time, make it the same of created
|
|
|
|
|
kwargs['created'] = self.end_time = datetime.now(timezone.utc)
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
|
return self.end_time < other.end_time
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2018-11-08 16:37:14 +00:00
|
|
|
|
return '{}'.format(self.severity)
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return '<{0.t} {0.id} {0.severity}>'.format(self)
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ActionComponent(db.Model):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
action_id = Column(UUID(as_uuid=True), ForeignKey(Action.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
class JoinedWithOneDeviceMixin:
|
|
|
|
|
# noinspection PyMethodParameters
|
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(ActionWithOneDevice.id), primary_key=True)
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ActionWithOneDevice(JoinedTableMixin, Action):
|
2019-02-07 12:47:42 +00:00
|
|
|
|
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
device = relationship(Device,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions_one',
|
2018-05-13 13:13:12 +00:00
|
|
|
|
lazy=True,
|
2018-11-11 20:52:55 +00:00
|
|
|
|
cascade=CASCADE_OWN,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
**_sorted_actions),
|
2018-04-10 15:06:39 +00:00
|
|
|
|
primaryjoin=Device.id == device_id)
|
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
__table_args__ = (
|
2019-05-11 14:27:22 +00:00
|
|
|
|
db.Index('action_one_device_id_index', device_id, postgresql_using='hash'),
|
2019-02-07 12:47:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
def __repr__(self) -> str:
|
2018-11-08 16:37:14 +00:00
|
|
|
|
return '<{0.t} {0.id} {0.severity} device={0.device!r}>'.format(self)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
2018-09-06 17:43:59 +00:00
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2018-09-06 17:43:59 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
2019-05-11 14:27:22 +00:00
|
|
|
|
if cls.t == 'ActionWithOneDevice':
|
2018-09-06 17:43:59 +00:00
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ActionWithMultipleDevices(Action):
|
2018-04-10 15:06:39 +00:00
|
|
|
|
devices = relationship(Device,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
backref=backref('actions_multiple', lazy=True, **_sorted_actions),
|
|
|
|
|
secondary=lambda: ActionDevice.__table__,
|
2018-06-16 10:41:12 +00:00
|
|
|
|
order_by=lambda: Device.id,
|
|
|
|
|
collection_class=OrderedSet)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
2018-11-08 16:37:14 +00:00
|
|
|
|
return '<{0.t} {0.id} {0.severity} devices={0.devices!r}>'.format(self)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ActionDevice(db.Model):
|
2018-04-10 15:06:39 +00:00
|
|
|
|
device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
action_id = Column(UUID(as_uuid=True), ForeignKey(ActionWithMultipleDevices.id),
|
|
|
|
|
primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-14 10:58:56 +00:00
|
|
|
|
class ActionWithMultipleTradeDocuments(ActionWithMultipleDevices):
|
|
|
|
|
documents = relationship(TradeDocument,
|
2021-05-13 11:35:46 +00:00
|
|
|
|
backref=backref('actions_multiple_docs', lazy=True, **_sorted_actions),
|
2021-05-14 10:58:56 +00:00
|
|
|
|
secondary=lambda: ActionTradeDocument.__table__,
|
|
|
|
|
order_by=lambda: TradeDocument.id,
|
2021-05-12 09:48:52 +00:00
|
|
|
|
collection_class=OrderedSet)
|
|
|
|
|
|
|
|
|
|
|
2021-05-14 10:58:56 +00:00
|
|
|
|
class ActionTradeDocument(db.Model):
|
|
|
|
|
document_id = Column(BigInteger, ForeignKey(TradeDocument.id), primary_key=True)
|
|
|
|
|
action_id = Column(UUID(as_uuid=True), ForeignKey(ActionWithMultipleTradeDocuments.id),
|
2021-05-12 09:48:52 +00:00
|
|
|
|
primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Add(ActionWithOneDevice):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of adding components to a device.
|
|
|
|
|
|
|
|
|
|
It is usually used internally from a :class:`.Snapshot`, for
|
|
|
|
|
example, when adding a secondary data storage to a computer.
|
|
|
|
|
"""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Remove(ActionWithOneDevice):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of removing components from a device.
|
|
|
|
|
|
|
|
|
|
It is usually used internally from a :class:`.Snapshot`, for
|
|
|
|
|
example, when removing a component from a broken computer.
|
|
|
|
|
"""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Allocate(JoinedTableMixin, ActionWithMultipleDevices):
|
2020-11-21 12:17:23 +00:00
|
|
|
|
"""The act of allocate one list of devices to one person
|
2020-11-18 17:13:13 +00:00
|
|
|
|
"""
|
2020-12-01 14:33:49 +00:00
|
|
|
|
final_user_code = Column(CIText(), default='', nullable=True)
|
|
|
|
|
final_user_code.comment = """This is a internal code for mainteing the secrets of the
|
|
|
|
|
personal datas of the new holder"""
|
2020-11-30 17:47:17 +00:00
|
|
|
|
transaction = Column(CIText(), default='', nullable=True)
|
|
|
|
|
transaction.comment = "The code used from the owner for relation with external tool."
|
2020-11-26 17:44:08 +00:00
|
|
|
|
end_users = Column(Numeric(precision=4), check_range('end_users', 0), nullable=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Deallocate(JoinedTableMixin, ActionWithMultipleDevices):
|
2020-11-21 12:17:23 +00:00
|
|
|
|
"""The act of deallocate one list of devices to one person of the system or not
|
|
|
|
|
"""
|
2020-11-30 17:17:47 +00:00
|
|
|
|
transaction= Column(CIText(), default='', nullable=True)
|
2020-11-30 17:47:17 +00:00
|
|
|
|
transaction.comment = "The code used from the owner for relation with external tool."
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|
|
|
|
"""An erasure attempt to a ``DataStorage``. The action contains
|
2018-11-12 17:15:24 +00:00
|
|
|
|
information about success and nature of the erasure.
|
|
|
|
|
|
|
|
|
|
EraseBasic is a software-based fast non-100%-secured way of
|
|
|
|
|
erasing data storage, performed
|
|
|
|
|
by Workbench Computer when executing the open-source
|
|
|
|
|
`shred <https://en.wikipedia.org/wiki/Shred_(Unix)>`_.
|
|
|
|
|
|
|
|
|
|
Users can generate erasure certificates from successful erasures.
|
|
|
|
|
|
|
|
|
|
Erasures are an accumulation of **erasure steps**, that are performed
|
|
|
|
|
as separate actions, called ``StepRandom``, for an erasure step
|
|
|
|
|
that has overwritten data with random bits, and ``StepZero``,
|
|
|
|
|
for an erasure step that has overwritten data with zeros.
|
|
|
|
|
|
2018-11-15 12:35:19 +00:00
|
|
|
|
Erasure standards define steps and methodologies to use.
|
|
|
|
|
Devicehub automatically shows the standards that each erasure
|
|
|
|
|
follows.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""
|
2018-11-21 13:26:56 +00:00
|
|
|
|
method = 'Shred'
|
|
|
|
|
"""The method or software used to destroy the data."""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-11-15 12:35:19 +00:00
|
|
|
|
@property
|
|
|
|
|
def standards(self):
|
|
|
|
|
"""A set of standards that this erasure follows."""
|
|
|
|
|
return ErasureStandards.from_data_storage(self)
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
2018-11-21 13:26:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def certificate(self):
|
|
|
|
|
"""The URL of this erasure certificate."""
|
2019-06-19 11:35:26 +00:00
|
|
|
|
# todo will this url_for_resource work for other resources?
|
2018-11-21 13:26:56 +00:00
|
|
|
|
return urlutils.URL(url_for_resource('Document', item_id=self.id))
|
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
def __str__(self) -> str:
|
2018-11-21 13:26:56 +00:00
|
|
|
|
return '{} on {}.'.format(self.severity, self.date_str)
|
|
|
|
|
|
|
|
|
|
def __format__(self, format_spec: str) -> str:
|
|
|
|
|
v = ''
|
|
|
|
|
if 't' in format_spec:
|
|
|
|
|
v += '{} {}'.format(self.type, self.severity)
|
|
|
|
|
if 't' in format_spec and 's' in format_spec:
|
|
|
|
|
v += '. '
|
|
|
|
|
if 's' in format_spec:
|
|
|
|
|
if self.standards:
|
|
|
|
|
std = 'with standards {}'.format(self.standards)
|
|
|
|
|
else:
|
|
|
|
|
std = 'no standard'
|
|
|
|
|
v += 'Method used: {}, {}. '.format(self.method, std)
|
2019-01-29 15:29:08 +00:00
|
|
|
|
if self.end_time and self.start_time:
|
|
|
|
|
v += '{} elapsed. '.format(self.elapsed)
|
|
|
|
|
|
|
|
|
|
v += 'On {}'.format(self.date_str)
|
2018-11-21 13:26:56 +00:00
|
|
|
|
return v
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
class EraseSectors(EraseBasic):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""A secured-way of erasing data storages, checking sector-by-sector
|
|
|
|
|
the erasure, using `badblocks <https://en.wikipedia.org/wiki/Badblocks>`_.
|
|
|
|
|
"""
|
2018-11-21 13:26:56 +00:00
|
|
|
|
method = 'Badblocks'
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
class ErasePhysical(EraseBasic):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of physically destroying a data storage unit."""
|
2018-11-15 12:35:19 +00:00
|
|
|
|
method = Column(DBEnum(PhysicalErasureMethod))
|
2018-11-09 10:22:13 +00:00
|
|
|
|
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
class Step(db.Model):
|
2019-05-03 12:31:49 +00:00
|
|
|
|
erasure_id = Column(UUID(as_uuid=True),
|
|
|
|
|
ForeignKey(EraseBasic.id, ondelete='CASCADE'),
|
|
|
|
|
primary_key=True)
|
2018-06-10 16:47:49 +00:00
|
|
|
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
|
|
|
|
num = Column(SmallInteger, primary_key=True)
|
2018-11-08 16:37:14 +00:00
|
|
|
|
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
|
2019-02-04 17:20:50 +00:00
|
|
|
|
start_time = Column(db.TIMESTAMP(timezone=True), nullable=False)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
start_time.comment = Action.start_time.comment
|
2019-02-04 17:20:50 +00:00
|
|
|
|
end_time = Column(db.TIMESTAMP(timezone=True), CheckConstraint('end_time > start_time'),
|
|
|
|
|
nullable=False)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
end_time.comment = Action.end_time.comment
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
erasure = relationship(EraseBasic,
|
|
|
|
|
backref=backref('steps',
|
|
|
|
|
cascade=CASCADE_OWN,
|
|
|
|
|
order_by=num,
|
|
|
|
|
collection_class=ordering_list('num')))
|
|
|
|
|
|
2018-11-21 13:26:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def elapsed(self):
|
|
|
|
|
"""Returns the elapsed time with seconds precision."""
|
|
|
|
|
t = self.end_time - self.start_time
|
|
|
|
|
return timedelta(seconds=t.seconds)
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
# noinspection PyMethodParameters
|
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
|
|
|
|
if cls.t == 'Step':
|
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
2018-11-21 13:26:56 +00:00
|
|
|
|
def __format__(self, format_spec: str) -> str:
|
|
|
|
|
return '{} – {} {}'.format(self.severity, self.type, self.elapsed)
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
|
|
|
|
class StepZero(Step):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StepRandom(Step):
|
|
|
|
|
pass
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""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
|
|
|
|
|
to the device.
|
|
|
|
|
|
|
|
|
|
When receiving a Snapshot, the DeviceHub creates, adds and removes
|
|
|
|
|
components to match the Snapshot. For example, if a Snapshot of a computer
|
|
|
|
|
contains a new component, the system searches for the component in its
|
|
|
|
|
database and, if not found, its creates it; finally linking it to the
|
|
|
|
|
computer.
|
|
|
|
|
|
|
|
|
|
A Snapshot is used with Remove to represent changes in components for
|
|
|
|
|
a device:
|
|
|
|
|
|
|
|
|
|
1. ``Snapshot`` creates a device if it does not exist, and the same
|
|
|
|
|
for its components. This is all done in one ``Snapshot``.
|
|
|
|
|
2. If the device exists, it updates its component composition by
|
|
|
|
|
*adding* and *removing* them. If,
|
|
|
|
|
for example, this new Snasphot doesn't have a component, it means that
|
|
|
|
|
this component is not present anymore in the device, thus removing it
|
|
|
|
|
from it. Then we have that:
|
|
|
|
|
|
|
|
|
|
- Components that are added to the device: snapshot2.components -
|
|
|
|
|
snapshot1.components
|
|
|
|
|
- Components that are removed to the device: snapshot1.components -
|
|
|
|
|
snapshot2.components
|
|
|
|
|
|
|
|
|
|
When adding a component, there may be the case this component existed
|
|
|
|
|
before and it was inside another device. In such case, DeviceHub will
|
|
|
|
|
perform ``Remove`` on the old parent.
|
|
|
|
|
|
|
|
|
|
**Snapshots from Workbench**
|
|
|
|
|
|
|
|
|
|
When processing a device from the Workbench, this one performs a Snapshot
|
2019-05-11 14:27:22 +00:00
|
|
|
|
and then performs more actions (like testings, benchmarking...).
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
|
|
|
|
There are two ways of sending this information. In an async way,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
this is, submitting actions as soon as Workbench performs then, or
|
|
|
|
|
submitting only one Snapshot action with all the other actions embedded.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
|
|
|
|
**Asynced**
|
|
|
|
|
|
|
|
|
|
The use case, which is represented in the ``test_workbench_phases``,
|
|
|
|
|
is as follows:
|
|
|
|
|
|
|
|
|
|
1. In **T1**, WorkbenchServer (as the middleware from Workbench and
|
|
|
|
|
Devicehub) submits:
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
- A ``Snapshot`` action with the required information to **synchronize**
|
2018-11-12 17:15:24 +00:00
|
|
|
|
and **rate** the device. This is:
|
|
|
|
|
|
|
|
|
|
- Identification information about the device and components
|
|
|
|
|
(S/N, model, physical characteristics...)
|
|
|
|
|
- ``Tags`` in a ``tags`` property in the ``device``.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
- ``Rate`` in an ``actions`` property in the ``device``.
|
|
|
|
|
- ``Benchmarks`` in an ``actions`` property in each ``component``
|
2018-11-12 17:15:24 +00:00
|
|
|
|
or ``device``.
|
|
|
|
|
- ``TestDataStorage`` as in ``Benchmarks``.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
- An ordered set of **expected actions**, defining which are the next
|
|
|
|
|
actions that Workbench will perform to the device in ideal
|
2018-11-12 17:15:24 +00:00
|
|
|
|
conditions (device doesn't fail, no Internet drop...).
|
|
|
|
|
|
|
|
|
|
Devicehub **syncs** the device with the database and perform the
|
|
|
|
|
``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
This leaves the Snapshot **open** to wait for the next actions
|
2018-11-12 17:15:24 +00:00
|
|
|
|
to come.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
2. Assuming that we expect all actions, in **T2**, WorkbenchServer
|
2018-11-12 17:15:24 +00:00
|
|
|
|
submits a ``StressTest`` with a ``snapshot`` field containing the
|
2019-05-11 14:27:22 +00:00
|
|
|
|
ID of the Snapshot in 1, and Devicehub links the action with such
|
2018-11-12 17:15:24 +00:00
|
|
|
|
``Snapshot``.
|
|
|
|
|
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
|
|
|
|
and ``component`` IDs from 1, linking it to them. It repeats
|
|
|
|
|
this for all the erased data storage devices; **T3+Tn** being
|
|
|
|
|
*n* the erased data storage devices.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
4. WorkbenchServer does like in 3. but for the action ``Install``,
|
2018-11-12 17:15:24 +00:00
|
|
|
|
finishing in **T3+Tn+Tx**, being *x* the number of data storage
|
|
|
|
|
devices with an OS installed into.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
5. In **T3+Tn+Tx**, when all *expected actions* have been performed,
|
2018-11-12 17:15:24 +00:00
|
|
|
|
Devicehub **closes** the ``Snapshot`` from 1.
|
|
|
|
|
|
|
|
|
|
**Synced**
|
|
|
|
|
|
|
|
|
|
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
2019-05-11 14:27:22 +00:00
|
|
|
|
the actions in an ``actions`` property inside each affected ``component``
|
2018-11-12 17:15:24 +00:00
|
|
|
|
or ``device``.
|
|
|
|
|
"""
|
2018-06-20 21:18:15 +00:00
|
|
|
|
uuid = Column(UUID(as_uuid=True), unique=True)
|
2018-06-10 16:47:49 +00:00
|
|
|
|
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
|
|
|
|
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
2018-06-20 21:18:15 +00:00
|
|
|
|
elapsed = Column(Interval)
|
2020-11-21 18:10:31 +00:00
|
|
|
|
elapsed.comment = """For Snapshots made with Workbench, the total amount
|
2019-06-19 11:35:26 +00:00
|
|
|
|
of time it took to complete.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2021-01-13 17:11:41 +00:00
|
|
|
|
def get_last_lifetimes(self):
|
|
|
|
|
"""We get the lifetime and serial_number of the first disk"""
|
|
|
|
|
hdds = []
|
2021-01-14 16:32:46 +00:00
|
|
|
|
components = [c for c in self.components]
|
2021-01-13 17:11:41 +00:00
|
|
|
|
components.sort(key=lambda x: x.created)
|
|
|
|
|
for hd in components:
|
|
|
|
|
data = {'serial_number': None, 'lifetime': 0}
|
|
|
|
|
if not isinstance(hd, DataStorage):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
data['serial_number'] = hd.serial_number
|
|
|
|
|
for act in hd.actions:
|
|
|
|
|
if not act.type == "TestDataStorage":
|
|
|
|
|
continue
|
2021-01-14 17:31:33 +00:00
|
|
|
|
if not act.lifetime:
|
|
|
|
|
continue
|
2021-01-19 15:06:46 +00:00
|
|
|
|
data['lifetime'] = act.lifetime.total_seconds()/3600
|
2021-01-13 17:11:41 +00:00
|
|
|
|
break
|
|
|
|
|
hdds.append(data)
|
|
|
|
|
|
|
|
|
|
return hdds
|
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
def __str__(self) -> str:
|
2018-11-08 16:37:14 +00:00
|
|
|
|
return '{}. {} version {}.'.format(self.severity, self.software, self.version)
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Install(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The action of installing an Operative System to a data
|
|
|
|
|
storage unit.
|
|
|
|
|
"""
|
2018-06-10 16:47:49 +00:00
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
2018-11-26 12:11:07 +00:00
|
|
|
|
address = Column(SmallInteger, check_range('address', 8, 256))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SnapshotRequest(db.Model):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
request = Column(JSON, nullable=False)
|
2018-05-30 10:49:40 +00:00
|
|
|
|
snapshot = relationship(Snapshot,
|
|
|
|
|
backref=backref('request',
|
|
|
|
|
lazy=True,
|
|
|
|
|
uselist=False,
|
|
|
|
|
cascade=CASCADE_OWN))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Benchmark(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""The act of gauging the performance of a device."""
|
|
|
|
|
elapsed = Column(Interval)
|
|
|
|
|
|
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
|
|
|
|
if cls.t == 'Benchmark':
|
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class BenchmarkMixin:
|
|
|
|
|
# noinspection PyMethodParameters
|
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
|
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
class BenchmarkDataStorage(Benchmark):
|
|
|
|
|
"""Benchmarks the data storage unit reading and writing speeds."""
|
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
|
|
|
|
read_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
|
|
|
|
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2021-02-11 19:00:57 +00:00
|
|
|
|
return 'Read: {0:.2f} MB/s, write: {0:.2f} MB/s'.format(
|
|
|
|
|
self.read_speed, self.write_speed)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkWithRate(Benchmark):
|
|
|
|
|
"""The act of benchmarking a device with a single rate."""
|
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)
|
|
|
|
|
rate = Column(Float, nullable=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2021-02-11 19:00:57 +00:00
|
|
|
|
return '{0:.2f} points'.format(self.rate)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkProcessor(BenchmarkWithRate):
|
|
|
|
|
"""Benchmarks a processor by executing `BogoMips
|
|
|
|
|
<https://en.wikipedia.org/wiki/BogoMips>`_. Note that this is not
|
|
|
|
|
a reliable way of rating processors and we keep it for compatibility
|
|
|
|
|
purposes.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkProcessorSysbench(BenchmarkProcessor):
|
|
|
|
|
"""Benchmarks a processor by using the processor benchmarking
|
|
|
|
|
utility of `sysbench <https://github.com/akopytov/sysbench>`_.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkRamSysbench(BenchmarkWithRate):
|
2019-04-23 19:27:31 +00:00
|
|
|
|
"""Benchmarks a RAM by using the ram benchmarking
|
|
|
|
|
utility of `sysbench <https://github.com/akopytov/sysbench>`_.
|
|
|
|
|
"""
|
2019-03-10 19:41:10 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkGraphicCard(BenchmarkWithRate):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Test(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""The act of documenting the functionality of a device, as
|
|
|
|
|
for the R2 Standard (R2 Provision 6 pag.19).
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
:attr:`.severity` in :class:`Action` defines a passing or failing
|
|
|
|
|
test, and
|
|
|
|
|
:attr:`ereuse_devicehub.resources.device.models.Device.working`
|
|
|
|
|
in Device gets all tests with warnings or errors for a device.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
|
|
|
|
if cls.t == 'Test':
|
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestMixin:
|
|
|
|
|
# noinspection PyMethodParameters
|
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
|
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
2019-05-03 13:02:09 +00:00
|
|
|
|
class MeasureBattery(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""A sample of the status of the battery.
|
|
|
|
|
|
2019-05-14 18:31:43 +00:00
|
|
|
|
Ref in R2 Provision 6 pag.22 Example:
|
|
|
|
|
Length of charge; Expected results: Minimum 40 minutes.
|
2019-05-03 13:02:09 +00:00
|
|
|
|
|
|
|
|
|
Operative Systems keep a record of several aspects of a battery.
|
|
|
|
|
This is a sample of those.
|
2019-06-19 11:35:26 +00:00
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: whether the health are Dead, Overheat or OverVoltage.
|
|
|
|
|
* :attr:`Severity.Warning`: whether the health are UnspecifiedValue or Cold.
|
2019-05-03 13:02:09 +00:00
|
|
|
|
"""
|
|
|
|
|
size = db.Column(db.Integer, nullable=False)
|
|
|
|
|
size.comment = """Maximum battery capacity, in mAh."""
|
|
|
|
|
voltage = db.Column(db.Integer, nullable=False)
|
|
|
|
|
voltage.comment = """The actual voltage of the battery, in mV."""
|
|
|
|
|
cycle_count = db.Column(db.Integer)
|
2020-11-21 18:10:31 +00:00
|
|
|
|
cycle_count.comment = """The number of full charges – discharges
|
2019-05-03 13:02:09 +00:00
|
|
|
|
cycles.
|
|
|
|
|
"""
|
|
|
|
|
health = db.Column(db.Enum(BatteryHealth))
|
2020-11-21 18:10:31 +00:00
|
|
|
|
health.comment = """The health of the Battery.
|
2019-05-03 13:02:09 +00:00
|
|
|
|
Only reported in Android.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestDataStorage(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""The act of testing the data storage.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
Testing is done using the `S.M.A.R.T self test
|
|
|
|
|
<https://en.wikipedia.org/wiki/S.M.A.R.T.#Self-tests>`_. Note
|
|
|
|
|
that not all data storage units, specially some new PCIe ones, do not
|
|
|
|
|
support SMART testing.
|
|
|
|
|
|
|
|
|
|
The test takes to other SMART values indicators of the overall health
|
|
|
|
|
of the data storage.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
Failing and warning conditions are as follows:
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-19 11:35:26 +00:00
|
|
|
|
* :attr:`Severity.Error`: whether the SMART test failed.
|
2019-06-12 14:49:55 +00:00
|
|
|
|
* :attr:`Severity.Warning`: if there is a significant chance for
|
|
|
|
|
the data storage to fail in the following year.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
|
|
|
|
length = Column(DBEnum(TestDataStorageLength), nullable=False) # todo from type
|
|
|
|
|
status = Column(Unicode(), check_lower('status'), nullable=False)
|
|
|
|
|
lifetime = Column(Interval)
|
|
|
|
|
assessment = Column(Boolean)
|
|
|
|
|
reallocated_sector_count = Column(SmallInteger)
|
|
|
|
|
power_cycle_count = Column(SmallInteger)
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_reported_uncorrectable_errors = Column('reported_uncorrectable_errors', Integer)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
command_timeout = Column(Integer)
|
2021-01-21 16:13:36 +00:00
|
|
|
|
current_pending_sector_count = Column(Integer)
|
|
|
|
|
offline_uncorrectable = Column(Integer)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
remaining_lifetime_percentage = Column(SmallInteger)
|
2019-04-23 19:27:31 +00:00
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
# Define severity
|
|
|
|
|
# As of https://www.backblaze.com/blog/hard-drive-smart-stats/ and
|
|
|
|
|
# https://www.backblaze.com/blog-smart-stats-2014-8.html
|
|
|
|
|
# We can guess some future disk failures by analyzing some SMART data.
|
|
|
|
|
if self.severity is None:
|
|
|
|
|
# Test finished successfully
|
|
|
|
|
if not self.assessment:
|
|
|
|
|
self.severity = Severity.Error
|
|
|
|
|
elif self.current_pending_sector_count and self.current_pending_sector_count > 40 \
|
|
|
|
|
or self.reallocated_sector_count and self.reallocated_sector_count > 10:
|
|
|
|
|
self.severity = Severity.Warning
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
t = inflection.humanize(self.status)
|
|
|
|
|
if self.lifetime:
|
|
|
|
|
t += ' with a lifetime of {:.1f} years.'.format(self.lifetime.days / 365)
|
|
|
|
|
t += self.description
|
|
|
|
|
return t
|
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
@property
|
|
|
|
|
def reported_uncorrectable_errors(self):
|
|
|
|
|
return self._reported_uncorrectable_errors
|
|
|
|
|
|
|
|
|
|
@reported_uncorrectable_errors.setter
|
|
|
|
|
def reported_uncorrectable_errors(self, value):
|
|
|
|
|
# We assume that a huge number is not meaningful
|
|
|
|
|
# So we keep it like this
|
|
|
|
|
self._reported_uncorrectable_errors = min(value, db.PSQL_INT_MAX)
|
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class StressTest(TestMixin, Test):
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""The act of stressing (putting to the maximum capacity)
|
2019-06-12 14:49:55 +00:00
|
|
|
|
a device for an amount of minutes.
|
2019-06-19 11:35:26 +00:00
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: whether failed StressTest.
|
|
|
|
|
* :attr:`Severity.Warning`: if stress test are less than 5 minutes.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
2019-04-23 19:27:31 +00:00
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
@validates('elapsed')
|
|
|
|
|
def is_minute_and_bigger_than_1_minute(self, _, value: timedelta):
|
|
|
|
|
seconds = value.total_seconds()
|
|
|
|
|
assert not bool(seconds % 60)
|
|
|
|
|
assert seconds >= 60
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return '{}. Computing for {}'.format(self.severity, self.elapsed)
|
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestAudio(TestMixin, Test):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""The act of checking the audio aspects of the device.
|
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: whether speaker or microphone variables fail.
|
|
|
|
|
* :attr:`Severity.Warning`: .
|
|
|
|
|
"""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_speaker = Column('speaker', Boolean)
|
|
|
|
|
_speaker.comment = """Whether the speaker works as expected."""
|
|
|
|
|
_microphone = Column('microphone', Boolean)
|
|
|
|
|
_microphone.comment = """Whether the microphone works as expected."""
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def speaker(self):
|
|
|
|
|
return self._speaker
|
|
|
|
|
|
|
|
|
|
@speaker.setter
|
|
|
|
|
def speaker(self, x):
|
|
|
|
|
self._speaker = x
|
|
|
|
|
self._check()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def microphone(self):
|
|
|
|
|
return self._microphone
|
|
|
|
|
|
|
|
|
|
@microphone.setter
|
|
|
|
|
def microphone(self, x):
|
|
|
|
|
self._microphone = x
|
|
|
|
|
self._check()
|
|
|
|
|
|
|
|
|
|
def _check(self):
|
|
|
|
|
"""Sets ``severity`` to ``error`` if any of the variables fail."""
|
|
|
|
|
if not self._speaker or not self._microphone:
|
|
|
|
|
self.severity = Severity.Error
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestConnectivity(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""The act of testing the connectivity of the device.
|
2019-05-08 17:12:05 +00:00
|
|
|
|
|
|
|
|
|
A failing test means that at least one connection of the device
|
|
|
|
|
is not working well. A comment should get into more detail.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestCamera(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""Tests the working conditions of the camera of the device,
|
2019-05-08 17:12:05 +00:00
|
|
|
|
specially when taking pictures or recording video.
|
2019-06-12 14:49:55 +00:00
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
2019-06-19 11:35:26 +00:00
|
|
|
|
* :attr:`Severity.Error`: whether the camera cannot turn on or
|
2019-06-12 14:49:55 +00:00
|
|
|
|
has significant visual problems.
|
2019-06-19 11:35:26 +00:00
|
|
|
|
* :attr:`Severity.Warning`: whether there are small visual problems
|
2019-06-12 14:49:55 +00:00
|
|
|
|
with the camera (like dust) that it still allows it to be used.
|
2019-03-19 23:08:05 +00:00
|
|
|
|
"""
|
2019-04-10 08:46:43 +00:00
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestKeyboard(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""Whether the keyboard works correctly.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: if at least one key does not produce
|
|
|
|
|
a character on screen. This follows R2 Provision 6 pag.22.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
"""
|
2019-04-10 08:46:43 +00:00
|
|
|
|
|
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
class TestTrackpad(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""Whether the trackpad works correctly.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
Failing and warning conditions are as follows:
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
* :attr:`Severity.Error`: if the cursor does not move on screen.
|
|
|
|
|
This follows R2 Provision 6 pag.22.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
class TestDisplayHinge(TestMixin, Test):
|
|
|
|
|
"""Whether display hinge works correctly.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
Failing and warning conditions are as follows:
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-19 11:35:26 +00:00
|
|
|
|
* :attr:`Severity.Error`: whether the laptop does not stay open
|
2019-06-12 14:49:55 +00:00
|
|
|
|
or closed at desired angles. From R2 Provision 6 pag.22.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
"""
|
2019-03-19 23:08:05 +00:00
|
|
|
|
|
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
class TestPowerAdapter(TestMixin, Test):
|
|
|
|
|
"""Whether power adapter charge battery device without problems.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
Failing and warning conditions are as follows:
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
* :attr:`Severity.Error`: if the laptop does not charge battery.
|
|
|
|
|
This follows R2 Provision 6 pag.22.
|
|
|
|
|
"""
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
class TestBios(TestMixin, Test):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Tests the working condition and grades the usability of the BIOS.
|
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: whether Bios beeps or access range is D or E.
|
|
|
|
|
* :attr:`Severity.Warning`: whether access range is B or C.
|
|
|
|
|
"""
|
2019-05-14 18:31:43 +00:00
|
|
|
|
beeps_power_on = Column(Boolean)
|
|
|
|
|
beeps_power_on.comment = """Whether there are no beeps or error
|
2019-05-08 17:12:05 +00:00
|
|
|
|
codes when booting up.
|
2020-11-21 18:10:31 +00:00
|
|
|
|
|
2019-05-14 18:31:43 +00:00
|
|
|
|
Reference: R2 provision 6 page 23.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
2019-04-30 00:02:23 +00:00
|
|
|
|
access_range = Column(DBEnum(BiosAccessRange))
|
2019-05-08 17:12:05 +00:00
|
|
|
|
access_range.comment = """Difficulty to modify the boot menu.
|
2020-11-21 18:10:31 +00:00
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
This is used as an usability measure for accessing and modifying
|
|
|
|
|
a bios, specially as something as important as modifying the boot
|
2019-06-19 11:35:26 +00:00
|
|
|
|
menu.
|
|
|
|
|
"""
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
class VisualTest(TestMixin, Test):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""The act of visually inspecting the appearance and functionality
|
2019-05-08 17:12:05 +00:00
|
|
|
|
of the device.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
|
|
|
|
Reference R2 provision 6 Templates Ready for Resale Checklist (Desktop)
|
|
|
|
|
https://sustainableelectronics.org/sites/default/files/6.c.2%20Desktop%20R2-Ready%20for%20Resale%20Checklist.docx
|
2019-06-19 11:35:26 +00:00
|
|
|
|
Physical condition grade.
|
|
|
|
|
|
|
|
|
|
Failing and warning conditions are as follows:
|
|
|
|
|
|
|
|
|
|
* :attr:`Severity.Error`: whether appearance range is less than B or
|
|
|
|
|
functionality range is less than B.
|
|
|
|
|
* :attr:`Severity.Warning`: whether appearance range is B or A and
|
|
|
|
|
functionality range is B.
|
|
|
|
|
* :attr:`Severity.Info`: whether appearance range is B or A and
|
|
|
|
|
functionality range is A.
|
2019-03-10 19:41:10 +00:00
|
|
|
|
"""
|
2019-10-22 13:53:44 +00:00
|
|
|
|
appearance_range = Column(DBEnum(AppearanceRange), nullable=True)
|
2019-03-13 12:06:58 +00:00
|
|
|
|
appearance_range.comment = AppearanceRange.__doc__
|
2019-10-22 13:53:44 +00:00
|
|
|
|
functionality_range = Column(DBEnum(FunctionalityRange), nullable=True)
|
2019-03-19 23:08:05 +00:00
|
|
|
|
functionality_range.comment = FunctionalityRange.__doc__
|
2019-04-30 00:02:23 +00:00
|
|
|
|
labelling = Column(Boolean)
|
2019-05-08 17:12:05 +00:00
|
|
|
|
labelling.comment = """Whether there are tags to be removed."""
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return super().__str__() + '. Appearance {} and functionality {}'.format(
|
|
|
|
|
self.appearance_range,
|
|
|
|
|
self.functionality_range
|
|
|
|
|
)
|
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Rate(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2019-06-12 14:49:55 +00:00
|
|
|
|
"""The act of computing a rate based on different categories:
|
2019-05-14 18:31:43 +00:00
|
|
|
|
|
2019-06-12 14:49:55 +00:00
|
|
|
|
* Functionality (F). Tests, the act of testing usage condition of a device
|
|
|
|
|
* Appearance (A). Visual evaluation, surface deterioration.
|
|
|
|
|
* Performance (Q). Components characteristics and components benchmarks.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
"""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
N = 2
|
|
|
|
|
"""The number of significant digits for rates.
|
|
|
|
|
Values are rounded and stored to it.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""
|
2019-03-19 23:08:05 +00:00
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_rating = Column('rating', Float(decimal_return_scale=N), check_range('rating', *R_POSITIVE))
|
|
|
|
|
_rating.comment = """The rating for the content."""
|
2018-07-14 14:41:22 +00:00
|
|
|
|
version = Column(StrictVersionType)
|
2018-10-14 18:10:52 +00:00
|
|
|
|
version.comment = """The version of the software."""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_appearance = Column('appearance',
|
|
|
|
|
Float(decimal_return_scale=N),
|
|
|
|
|
check_range('appearance', *R_NEGATIVE))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
_appearance.comment = """Subjective value representing aesthetic aspects."""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_functionality = Column('functionality',
|
|
|
|
|
Float(decimal_return_scale=N),
|
|
|
|
|
check_range('functionality', *R_NEGATIVE))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
_functionality.comment = """Subjective value representing usage aspects."""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def rating(self):
|
|
|
|
|
return self._rating
|
|
|
|
|
|
|
|
|
|
@rating.setter
|
|
|
|
|
def rating(self, x):
|
|
|
|
|
self._rating = round(max(x, 0), self.N)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def appearance(self):
|
|
|
|
|
return self._appearance
|
|
|
|
|
|
|
|
|
|
@appearance.setter
|
|
|
|
|
def appearance(self, x):
|
|
|
|
|
self._appearance = round(x, self.N)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def functionality(self):
|
|
|
|
|
return self._functionality
|
|
|
|
|
|
|
|
|
|
@functionality.setter
|
|
|
|
|
def functionality(self, x):
|
|
|
|
|
self._functionality = round(x, self.N)
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def rating_range(self) -> RatingRange:
|
2019-05-08 17:12:05 +00:00
|
|
|
|
""""""
|
|
|
|
|
return RatingRange.from_score(self.rating) if self.rating else None
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2018-06-20 21:18:15 +00:00
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
|
|
|
|
if cls.t == 'Rate':
|
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
def __str__(self) -> str:
|
2021-02-11 19:00:57 +00:00
|
|
|
|
if self.version:
|
|
|
|
|
return '{} (v.{})'.format(self.rating_range, self.version)
|
|
|
|
|
|
|
|
|
|
return '{}'.format(self.rating_range)
|
2019-03-07 16:07:59 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
def compute(cls, device) -> 'RateComputer':
|
2019-04-23 19:27:31 +00:00
|
|
|
|
raise NotImplementedError()
|
2019-03-07 16:07:59 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
class RateMixin:
|
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
|
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RateComputer(RateMixin, Rate):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""The act of rating a computer type devices.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
It's the starting point for calculating the rate.
|
2019-06-19 11:35:26 +00:00
|
|
|
|
Algorithm explained in v1.0 file.
|
2019-05-14 18:31:43 +00:00
|
|
|
|
"""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
_processor = Column('processor',
|
|
|
|
|
Float(decimal_return_scale=Rate.N),
|
|
|
|
|
check_range('processor', *R_POSITIVE))
|
|
|
|
|
_processor.comment = """The rate of the Processor."""
|
|
|
|
|
_ram = Column('ram', Float(decimal_return_scale=Rate.N), check_range('ram', *R_POSITIVE))
|
|
|
|
|
_ram.comment = """The rate of the RAM."""
|
|
|
|
|
_data_storage = Column('data_storage',
|
|
|
|
|
Float(decimal_return_scale=Rate.N),
|
|
|
|
|
check_range('data_storage', *R_POSITIVE))
|
|
|
|
|
_data_storage.comment = """'Data storage rate, like HHD, SSD.'"""
|
|
|
|
|
_graphic_card = Column('graphic_card',
|
|
|
|
|
Float(decimal_return_scale=Rate.N),
|
|
|
|
|
check_range('graphic_card', *R_POSITIVE))
|
|
|
|
|
_graphic_card.comment = 'Graphic card rate.'
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
@property
|
|
|
|
|
def processor(self):
|
|
|
|
|
return self._processor
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-05-08 17:12:05 +00:00
|
|
|
|
@processor.setter
|
|
|
|
|
def processor(self, x):
|
|
|
|
|
self._processor = round(x, self.N)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def ram(self):
|
|
|
|
|
return self._ram
|
|
|
|
|
|
|
|
|
|
@ram.setter
|
|
|
|
|
def ram(self, x):
|
|
|
|
|
self._ram = round(x, self.N)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def data_storage(self):
|
|
|
|
|
return self._data_storage
|
|
|
|
|
|
|
|
|
|
@data_storage.setter
|
|
|
|
|
def data_storage(self, x):
|
|
|
|
|
self._data_storage = round(x, self.N)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def graphic_card(self):
|
|
|
|
|
return self._graphic_card
|
|
|
|
|
|
|
|
|
|
@graphic_card.setter
|
|
|
|
|
def graphic_card(self, x):
|
|
|
|
|
self._graphic_card = round(x, self.N)
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
@property
|
|
|
|
|
def data_storage_range(self):
|
2019-05-08 17:12:05 +00:00
|
|
|
|
return RatingRange.from_score(self.data_storage) if self.data_storage else None
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
@property
|
|
|
|
|
def ram_range(self):
|
2019-05-08 17:12:05 +00:00
|
|
|
|
return RatingRange.from_score(self.ram) if self.ram else None
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
@property
|
|
|
|
|
def processor_range(self):
|
2019-05-08 17:12:05 +00:00
|
|
|
|
return RatingRange.from_score(self.processor) if self.processor else None
|
2019-02-27 22:36:26 +00:00
|
|
|
|
|
2019-04-30 00:02:23 +00:00
|
|
|
|
@property
|
|
|
|
|
def graphic_card_range(self):
|
2019-05-08 17:12:05 +00:00
|
|
|
|
return RatingRange.from_score(self.graphic_card) if self.graphic_card else None
|
2019-04-30 00:02:23 +00:00
|
|
|
|
|
2019-04-23 19:27:31 +00:00
|
|
|
|
@classmethod
|
2019-05-08 17:12:05 +00:00
|
|
|
|
def compute(cls, device):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""The act of compute general computer rate."""
|
2019-06-12 14:49:55 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.rate.v1_0 import rate_algorithm
|
2019-04-30 00:02:23 +00:00
|
|
|
|
rate = rate_algorithm.compute(device)
|
|
|
|
|
price = None
|
|
|
|
|
with suppress(InvalidRangeForPrice): # We will have exception if range == VERY_LOW
|
|
|
|
|
price = EreusePrice(rate)
|
|
|
|
|
return rate, price
|
2019-04-23 19:27:31 +00:00
|
|
|
|
|
2019-03-07 16:07:59 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Price(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
# TODO rewrite Class comment change AggregateRate..
|
2018-11-17 16:03:03 +00:00
|
|
|
|
"""The act of setting a trading price for the device.
|
|
|
|
|
|
|
|
|
|
This does not imply that the device is ultimately traded for that
|
|
|
|
|
price. Use the :class:`.Sell` for that.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
|
|
|
|
Devicehub automatically computes a price from ``AggregateRating``
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions. As in a **Rate**, price can have **software** and **version**,
|
2018-11-12 17:15:24 +00:00
|
|
|
|
and there is an **official** price that is used to automatically
|
|
|
|
|
compute the price from an ``AggregateRating``. Only the official price
|
|
|
|
|
is computed from an ``AggregateRating``.
|
|
|
|
|
"""
|
2018-10-15 09:21:21 +00:00
|
|
|
|
SCALE = 4
|
|
|
|
|
ROUND = ROUND_HALF_EVEN
|
2018-07-14 14:41:22 +00:00
|
|
|
|
currency = Column(DBEnum(Currency), nullable=False)
|
2018-10-14 18:10:52 +00:00
|
|
|
|
currency.comment = """The currency of this price as for ISO 4217."""
|
2018-10-15 09:21:21 +00:00
|
|
|
|
price = Column(Numeric(precision=19, scale=SCALE), check_range('price', 0), nullable=False)
|
2018-10-14 18:10:52 +00:00
|
|
|
|
price.comment = """The value."""
|
2018-07-14 14:41:22 +00:00
|
|
|
|
software = Column(DBEnum(PriceSoftware))
|
2018-10-14 18:10:52 +00:00
|
|
|
|
software.comment = """The software used to compute this price,
|
|
|
|
|
if the price was computed automatically. This field is None
|
|
|
|
|
if the price has been manually set.
|
|
|
|
|
"""
|
2018-07-14 14:41:22 +00:00
|
|
|
|
version = Column(StrictVersionType)
|
2018-10-14 18:10:52 +00:00
|
|
|
|
version.comment = """The version of the software, or None."""
|
2019-04-16 15:47:28 +00:00
|
|
|
|
rating_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id))
|
2019-04-23 19:27:31 +00:00
|
|
|
|
rating_id.comment = """The Rate used to auto-compute
|
2019-06-19 11:35:26 +00:00
|
|
|
|
this price, if it has not been set manually.
|
|
|
|
|
"""
|
2019-04-11 16:29:51 +00:00
|
|
|
|
rating = relationship(Rate,
|
2018-07-14 14:41:22 +00:00
|
|
|
|
backref=backref('price',
|
|
|
|
|
lazy=True,
|
|
|
|
|
cascade=CASCADE_OWN,
|
|
|
|
|
uselist=False),
|
2019-04-16 15:47:28 +00:00
|
|
|
|
primaryjoin=Rate.id == rating_id)
|
2018-07-14 14:41:22 +00:00
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
2018-10-15 09:21:21 +00:00
|
|
|
|
if 'price' in kwargs:
|
|
|
|
|
assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal'
|
2018-10-16 06:46:55 +00:00
|
|
|
|
super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), *args,
|
|
|
|
|
**kwargs)
|
2018-10-15 09:21:21 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
|
|
|
|
"""Returns a Decimal value with the correct scale for Price.price."""
|
2019-05-08 17:12:05 +00:00
|
|
|
|
if isinstance(value, (float, int)):
|
2018-10-15 09:21:21 +00:00
|
|
|
|
value = Decimal(value)
|
|
|
|
|
# equation from marshmallow.fields.Decimal
|
|
|
|
|
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
|
2018-07-14 14:41:22 +00:00
|
|
|
|
|
2018-10-16 06:46:55 +00:00
|
|
|
|
@declared_attr
|
|
|
|
|
def __mapper_args__(cls):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Defines inheritance.
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
|
|
|
|
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
|
|
|
|
|
extensions/declarative/api.html
|
|
|
|
|
#sqlalchemy.ext.declarative.declared_attr>`_
|
|
|
|
|
"""
|
|
|
|
|
args = {POLYMORPHIC_ID: cls.t}
|
|
|
|
|
if cls.t == 'Price':
|
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return '{0:0.2f} {1}'.format(self.price, self.currency)
|
|
|
|
|
|
2018-07-14 14:41:22 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
class EreusePrice(Price):
|
|
|
|
|
"""The act of setting a price by guessing it using the eReuse.org
|
|
|
|
|
algorithm.
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
This algorithm states that the price is the use value of the device
|
|
|
|
|
(represented by its last :class:`.Rate`) multiplied by a constants
|
|
|
|
|
value agreed by a circuit or platform.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""
|
2019-03-10 19:41:10 +00:00
|
|
|
|
MULTIPLIER = {
|
2020-10-15 11:19:47 +00:00
|
|
|
|
Computer: 20,
|
2019-03-10 19:41:10 +00:00
|
|
|
|
Desktop: 20,
|
2021-01-22 12:19:29 +00:00
|
|
|
|
Laptop: 30,
|
|
|
|
|
Server: 40
|
2019-03-10 19:41:10 +00:00
|
|
|
|
}
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
class Type:
|
|
|
|
|
def __init__(self, percentage: float, price: Decimal) -> None:
|
|
|
|
|
# see https://stackoverflow.com/a/29651462 for the - 0.005
|
|
|
|
|
self.amount = EreusePrice.to_price(price * Decimal(percentage))
|
|
|
|
|
self.percentage = EreusePrice.to_price(price * Decimal(percentage))
|
|
|
|
|
self.percentage = round(percentage - 0.005, 2)
|
2018-07-02 10:52:54 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
class Service:
|
|
|
|
|
REFURBISHER, PLATFORM, RETAILER = 0, 1, 2
|
|
|
|
|
STANDARD, WARRANTY2 = 'STD', 'WR2'
|
|
|
|
|
SCHEMA = {
|
|
|
|
|
Desktop: {
|
|
|
|
|
RatingRange.HIGH: {
|
|
|
|
|
STANDARD: (0.35125, 0.204375, 0.444375),
|
|
|
|
|
WARRANTY2: (0.47425, 0.275875, 0.599875)
|
|
|
|
|
},
|
|
|
|
|
RatingRange.MEDIUM: {
|
|
|
|
|
STANDARD: (0.385, 0.2558333333, 0.3591666667),
|
|
|
|
|
WARRANTY2: (0.539, 0.3581666667, 0.5028333333)
|
|
|
|
|
},
|
|
|
|
|
RatingRange.LOW: {
|
|
|
|
|
STANDARD: (0.5025, 0.30875, 0.18875),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Laptop: {
|
|
|
|
|
RatingRange.HIGH: {
|
|
|
|
|
STANDARD: (0.3469230769, 0.195, 0.4580769231),
|
|
|
|
|
WARRANTY2: (0.4522307692, 0.2632307692, 0.6345384615)
|
|
|
|
|
},
|
|
|
|
|
RatingRange.MEDIUM: {
|
|
|
|
|
STANDARD: (0.382, 0.1735, 0.4445),
|
|
|
|
|
WARRANTY2: (0.5108, 0.2429, 0.6463)
|
|
|
|
|
},
|
|
|
|
|
RatingRange.LOW: {
|
|
|
|
|
STANDARD: (0.4528571429, 0.2264285714, 0.3207142857),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-10-15 11:19:47 +00:00
|
|
|
|
SCHEMA[Server] = SCHEMA[Computer] = SCHEMA[Desktop]
|
2018-10-16 06:46:55 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
def __init__(self, device, rating_range, role, price: Decimal) -> None:
|
|
|
|
|
cls = device.__class__ if device.__class__ != Server else Desktop
|
|
|
|
|
rate = self.SCHEMA[cls][rating_range]
|
|
|
|
|
self.standard = EreusePrice.Type(rate[self.STANDARD][role], price)
|
|
|
|
|
if self.WARRANTY2 in rate:
|
|
|
|
|
self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price)
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2019-04-11 16:29:51 +00:00
|
|
|
|
def __init__(self, rating: RateComputer, **kwargs) -> None:
|
2019-05-08 17:12:05 +00:00
|
|
|
|
if not rating.rating_range or rating.rating_range == RatingRange.VERY_LOW:
|
2019-04-30 00:02:23 +00:00
|
|
|
|
raise InvalidRangeForPrice()
|
2019-03-10 19:41:10 +00:00
|
|
|
|
# We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts
|
|
|
|
|
price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP)
|
|
|
|
|
super().__init__(rating=rating,
|
|
|
|
|
device=rating.device,
|
|
|
|
|
price=price,
|
|
|
|
|
software=kwargs.pop('software', app.config['PRICE_SOFTWARE']),
|
|
|
|
|
version=kwargs.pop('version', app.config['PRICE_VERSION']),
|
|
|
|
|
**kwargs)
|
|
|
|
|
self._compute()
|
2018-06-12 14:50:05 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
@orm.reconstructor
|
|
|
|
|
def _compute(self):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Calculates eReuse.org prices when initializing the instance
|
|
|
|
|
from the price and other properties.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2019-03-10 19:41:10 +00:00
|
|
|
|
self.refurbisher = self._service(self.Service.REFURBISHER)
|
|
|
|
|
self.retailer = self._service(self.Service.RETAILER)
|
|
|
|
|
self.platform = self._service(self.Service.PLATFORM)
|
|
|
|
|
if hasattr(self.refurbisher, 'warranty2'):
|
|
|
|
|
self.warranty2 = round(self.refurbisher.warranty2.amount
|
|
|
|
|
+ self.retailer.warranty2.amount
|
|
|
|
|
+ self.platform.warranty2.amount, 2)
|
2018-06-12 14:50:05 +00:00
|
|
|
|
|
2019-03-10 19:41:10 +00:00
|
|
|
|
def _service(self, role):
|
|
|
|
|
return self.Service(self.device, self.rating.rating_range, role, self.price)
|
2018-06-12 14:50:05 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ToRepair(ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""Select a device to be repaired."""
|
2018-07-22 20:42:49 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Repair(ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""Repair is the act of performing reparations.
|
|
|
|
|
|
|
|
|
|
If a repair without an error is performed,
|
|
|
|
|
it represents that the reparation has been successful.
|
|
|
|
|
"""
|
2018-07-22 20:42:49 +00:00
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class Ready(ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The device is ready to be used.
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
This involves greater preparation from the ``Prepare`` action,
|
|
|
|
|
and users should only use a device after this action is performed.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Users usually require devices with this action before shipping them
|
2018-11-12 17:15:24 +00:00
|
|
|
|
to costumers.
|
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class ToPrepare(ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The device has been selected for preparation.
|
|
|
|
|
|
|
|
|
|
See Prepare for more info.
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Usually **ToPrepare** is the next action done after registering the
|
2018-11-12 17:15:24 +00:00
|
|
|
|
device.
|
|
|
|
|
"""
|
2018-07-22 20:42:49 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Prepare(ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""Work has been performed to the device to a defined point of
|
|
|
|
|
acceptance.
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Users using this action have to agree what is this point
|
2018-11-12 17:15:24 +00:00
|
|
|
|
of acceptance; for some is when the device just works, for others
|
|
|
|
|
when some testing has been performed.
|
|
|
|
|
"""
|
2018-07-22 20:42:49 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Live(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""A keep-alive from a device connected to the Internet with
|
2019-05-11 14:27:22 +00:00
|
|
|
|
information about its state (in the form of a ``Snapshot`` action)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
and usage statistics.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""
|
2020-11-23 17:35:50 +00:00
|
|
|
|
serial_number = Column(Unicode(), check_lower('serial_number'))
|
2020-11-23 17:42:31 +00:00
|
|
|
|
serial_number.comment = """The serial number of the Hard Disk in lower case."""
|
2020-12-04 15:58:53 +00:00
|
|
|
|
usage_time_hdd = Column(Interval, nullable=True)
|
|
|
|
|
snapshot_uuid = Column(UUID(as_uuid=True))
|
2020-12-28 14:31:57 +00:00
|
|
|
|
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
|
|
|
|
software_version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
|
|
|
|
licence_version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
@property
|
2020-12-01 21:03:15 +00:00
|
|
|
|
def final_user_code(self):
|
|
|
|
|
""" show the final_user_code of the last action Allocate."""
|
|
|
|
|
actions = self.device.actions
|
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
for e in reversed(actions):
|
|
|
|
|
if isinstance(e, Allocate) and e.created < self.created:
|
|
|
|
|
return e.final_user_code
|
2020-12-04 15:58:53 +00:00
|
|
|
|
return ''
|
2020-12-01 21:03:15 +00:00
|
|
|
|
|
|
|
|
|
@property
|
2020-12-04 15:58:53 +00:00
|
|
|
|
def usage_time_allocate(self):
|
2020-12-01 21:03:15 +00:00
|
|
|
|
"""Show how many hours is used one device from the last check"""
|
2020-12-04 15:58:53 +00:00
|
|
|
|
self.sort_actions()
|
|
|
|
|
if self.usage_time_hdd is None:
|
|
|
|
|
return self.last_usage_time_allocate()
|
|
|
|
|
|
|
|
|
|
delta_zero = timedelta(0)
|
|
|
|
|
diff_time = self.diff_time()
|
|
|
|
|
if diff_time is None:
|
|
|
|
|
return delta_zero
|
|
|
|
|
|
|
|
|
|
if diff_time < delta_zero:
|
|
|
|
|
return delta_zero
|
|
|
|
|
return diff_time
|
|
|
|
|
|
|
|
|
|
def sort_actions(self):
|
|
|
|
|
self.actions = copy.copy(self.device.actions)
|
|
|
|
|
self.actions.sort(key=lambda x: x.created)
|
|
|
|
|
self.actions.reverse()
|
|
|
|
|
|
|
|
|
|
def last_usage_time_allocate(self):
|
2021-03-19 10:53:04 +00:00
|
|
|
|
"""If we don't have self.usage_time_hdd then we need search the last
|
2020-12-04 18:33:05 +00:00
|
|
|
|
action Live with usage_time_allocate valid"""
|
2020-12-04 15:58:53 +00:00
|
|
|
|
for e in self.actions:
|
2020-12-01 21:03:15 +00:00
|
|
|
|
if isinstance(e, Live) and e.created < self.created:
|
2020-12-04 15:58:53 +00:00
|
|
|
|
if not e.usage_time_allocate:
|
|
|
|
|
continue
|
|
|
|
|
return e.usage_time_allocate
|
|
|
|
|
return timedelta(0)
|
|
|
|
|
|
2021-01-19 15:06:46 +00:00
|
|
|
|
def get_last_snapshot_lifetime(self):
|
|
|
|
|
for e in self.actions:
|
|
|
|
|
if e.created > self.created:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if isinstance(e, Snapshot):
|
|
|
|
|
last_time = self.get_last_lifetime(e)
|
|
|
|
|
if not last_time:
|
|
|
|
|
continue
|
|
|
|
|
return last_time
|
|
|
|
|
|
2020-12-04 15:58:53 +00:00
|
|
|
|
def diff_time(self):
|
|
|
|
|
for e in self.actions:
|
|
|
|
|
if e.created > self.created:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if isinstance(e, Snapshot):
|
|
|
|
|
last_time = self.get_last_lifetime(e)
|
|
|
|
|
if not last_time:
|
|
|
|
|
continue
|
|
|
|
|
return self.usage_time_hdd - last_time
|
|
|
|
|
|
|
|
|
|
if isinstance(e, Live):
|
|
|
|
|
if e.snapshot_uuid == self.snapshot_uuid:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not e.usage_time_hdd:
|
|
|
|
|
continue
|
|
|
|
|
return self.usage_time_hdd - e.usage_time_hdd
|
|
|
|
|
return None
|
2020-12-01 21:03:15 +00:00
|
|
|
|
|
2020-12-04 15:58:53 +00:00
|
|
|
|
def get_last_lifetime(self, snapshot):
|
2020-12-02 11:53:15 +00:00
|
|
|
|
for a in snapshot.actions:
|
|
|
|
|
if a.type == 'TestDataStorage' and a.device.serial_number == self.serial_number:
|
2020-12-04 15:58:53 +00:00
|
|
|
|
return a.lifetime
|
|
|
|
|
return None
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Organize(JoinedTableMixin, ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of manipulating/administering/supervising/controlling
|
|
|
|
|
one or more devices.
|
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Reserve(Organize):
|
2018-11-17 16:03:03 +00:00
|
|
|
|
"""The act of reserving devices.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
After this action is performed, the user is the **reservee** of the
|
2018-11-12 17:15:24 +00:00
|
|
|
|
devices. There can only be one non-cancelled reservation for
|
|
|
|
|
a device, and a reservation can only have one reservee.
|
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CancelReservation(Organize):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of cancelling a reservation."""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-14 10:58:56 +00:00
|
|
|
|
class Confirm(JoinedTableMixin, ActionWithMultipleTradeDocuments):
|
2021-05-10 09:51:28 +00:00
|
|
|
|
"""Users confirm the one action trade this confirmation it's link to trade
|
|
|
|
|
and the devices that confirm
|
|
|
|
|
"""
|
2021-04-19 17:33:05 +00:00
|
|
|
|
user_id = db.Column(UUID(as_uuid=True),
|
2021-04-22 09:11:23 +00:00
|
|
|
|
db.ForeignKey(User.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.id)
|
2021-04-19 17:33:05 +00:00
|
|
|
|
user = db.relationship(User, primaryjoin=user_id == User.id)
|
|
|
|
|
user_comment = """The user that accept the offer."""
|
2021-04-22 09:11:23 +00:00
|
|
|
|
action_id = db.Column(UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey('action.id'),
|
2021-04-19 17:33:05 +00:00
|
|
|
|
nullable=False)
|
2021-04-22 09:11:23 +00:00
|
|
|
|
action = db.relationship('Action',
|
2021-04-19 17:33:05 +00:00
|
|
|
|
backref=backref('acceptances',
|
2021-04-27 11:54:13 +00:00
|
|
|
|
uselist=True,
|
|
|
|
|
lazy=True,
|
|
|
|
|
order_by=lambda: Action.end_time,
|
|
|
|
|
collection_class=list),
|
2021-04-22 09:11:23 +00:00
|
|
|
|
primaryjoin='Confirm.action_id == Action.id')
|
2021-04-19 17:33:05 +00:00
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
2021-04-30 10:44:32 +00:00
|
|
|
|
if self.action.t in ['Trade']:
|
2021-04-22 09:11:23 +00:00
|
|
|
|
origin = 'To'
|
|
|
|
|
if self.user == self.action.user_from:
|
|
|
|
|
origin = 'From'
|
|
|
|
|
return '<{0.t} {0.id} accepted by {1}>'.format(self, origin)
|
2021-04-19 17:33:05 +00:00
|
|
|
|
|
2021-04-30 15:54:03 +00:00
|
|
|
|
|
|
|
|
|
class Revoke(Confirm):
|
2021-05-10 09:51:28 +00:00
|
|
|
|
"""Users can revoke one confirmation of one action trade"""
|
2021-04-30 15:54:03 +00:00
|
|
|
|
|
|
|
|
|
|
2021-04-30 10:44:32 +00:00
|
|
|
|
class ConfirmRevoke(Confirm):
|
2021-05-10 09:51:28 +00:00
|
|
|
|
"""Users can confirm and accept one action revoke"""
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return '<{0.t} {0.id} accepted by {0.user}>'.format(self)
|
2021-04-30 10:44:32 +00:00
|
|
|
|
|
2021-04-19 17:33:05 +00:00
|
|
|
|
|
2021-05-14 10:58:56 +00:00
|
|
|
|
class Trade(JoinedTableMixin, ActionWithMultipleTradeDocuments):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""Trade actions log the political exchange of devices between users.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Every time a trade action is performed, the old user looses its
|
2018-11-12 17:15:24 +00:00
|
|
|
|
political possession, for example ownership, in favor of another
|
|
|
|
|
user.
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Performing trade actions changes the *Trading* state of the
|
2018-11-12 17:15:24 +00:00
|
|
|
|
device —:class:`ereuse_devicehub.resources.device.states.Trading`.
|
2019-02-03 16:12:53 +00:00
|
|
|
|
|
|
|
|
|
This class and its inheritors
|
|
|
|
|
extend `Schema's Trade <http://schema.org/TradeAction>`_.
|
2021-03-24 13:17:18 +00:00
|
|
|
|
"""
|
|
|
|
|
user_from_id = db.Column(UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey(User.id),
|
2021-04-07 08:49:28 +00:00
|
|
|
|
nullable=False)
|
2021-03-24 13:17:18 +00:00
|
|
|
|
user_from = db.relationship(User, primaryjoin=user_from_id == User.id)
|
|
|
|
|
user_from_comment = """The user that offers the device due this deal."""
|
|
|
|
|
user_to_id = db.Column(UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey(User.id),
|
2021-04-07 08:49:28 +00:00
|
|
|
|
nullable=False)
|
2021-03-24 13:17:18 +00:00
|
|
|
|
user_to = db.relationship(User, primaryjoin=user_to_id == User.id)
|
|
|
|
|
user_to_comment = """The user that gets the device due this deal."""
|
|
|
|
|
price = Column(Float(decimal_return_scale=2), nullable=True)
|
2021-04-03 13:09:35 +00:00
|
|
|
|
currency = Column(DBEnum(Currency), nullable=False, default=Currency.EUR.name)
|
2021-04-03 11:33:56 +00:00
|
|
|
|
currency.comment = """The currency of this price as for ISO 4217."""
|
2021-03-24 13:17:18 +00:00
|
|
|
|
date = Column(db.TIMESTAMP(timezone=True))
|
2021-04-07 08:49:28 +00:00
|
|
|
|
confirm = Column(Boolean, default=False, nullable=False)
|
|
|
|
|
confirm.comment = """If you need confirmation of the user, you need actevate this field"""
|
|
|
|
|
code = Column(CIText(), nullable=True)
|
|
|
|
|
code.comment = """If the user not exist, you need a code to be able to do the traceability"""
|
2021-03-19 10:53:04 +00:00
|
|
|
|
lot_id = db.Column(UUID(as_uuid=True),
|
2021-04-03 11:33:56 +00:00
|
|
|
|
db.ForeignKey('lot.id',
|
|
|
|
|
use_alter=True,
|
2021-04-19 17:33:05 +00:00
|
|
|
|
name='lot_trade'),
|
2021-03-19 10:53:04 +00:00
|
|
|
|
nullable=True)
|
2021-04-03 11:33:56 +00:00
|
|
|
|
lot = relationship('Lot',
|
2021-04-19 17:33:05 +00:00
|
|
|
|
backref=backref('trade',
|
2021-04-03 11:33:56 +00:00
|
|
|
|
lazy=True,
|
|
|
|
|
uselist=False,
|
|
|
|
|
cascade=CASCADE_OWN),
|
2021-04-19 17:33:05 +00:00
|
|
|
|
primaryjoin='Trade.lot_id == Lot.id')
|
|
|
|
|
|
2021-05-10 09:51:28 +00:00
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return '<{0.t} {0.id} executed by {0.author}>'.format(self)
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
2019-12-12 20:17:35 +00:00
|
|
|
|
class InitTransfer(Trade):
|
|
|
|
|
"""The act of transfer ownership of devices between two agents"""
|
|
|
|
|
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
class Sell(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of taking money from a buyer in exchange of a device."""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Donate(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of giving devices without compensation."""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Rent(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of giving money in return for temporary use, but not
|
|
|
|
|
ownership, of a device.
|
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CancelTrade(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of cancelling a `Sell`_, `Donate`_ or `Rent`_."""
|
|
|
|
|
# todo cancelTrade does not do anything
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToDisposeProduct(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of setting a device for being disposed.
|
|
|
|
|
|
|
|
|
|
See :class:`.DisposeProduct`.
|
|
|
|
|
"""
|
|
|
|
|
# todo test this
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DisposeProduct(Trade):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""The act of getting rid of devices by giving (selling, donating)
|
|
|
|
|
to another organization, like a waste manager.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See :class:`.ToDispose` and :class:`.DisposeProduct` for
|
|
|
|
|
disposing without trading the device. See :class:`.DisposeWaste`
|
|
|
|
|
and :class:`.Recover` for disposing in-house, this is,
|
|
|
|
|
without trading the device.
|
|
|
|
|
"""
|
|
|
|
|
# todo For usability purposes, users might not directly perform
|
|
|
|
|
# *DisposeProduct*, but this could automatically be done when
|
|
|
|
|
# performing :class:`.ToDispose` + :class:`.Receive` to a
|
|
|
|
|
# ``RecyclingCenter``.
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
2020-07-07 15:17:41 +00:00
|
|
|
|
|
2019-12-21 15:41:23 +00:00
|
|
|
|
class TransferOwnershipBlockchain(Trade):
|
|
|
|
|
""" The act of change owenership of devices between two users (ethereum address)"""
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class MakeAvailable(ActionWithMultipleDevices):
|
|
|
|
|
"""The act of setting willingness for trading."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
class Migrate(JoinedTableMixin, ActionWithMultipleDevices):
|
2018-11-12 17:15:24 +00:00
|
|
|
|
"""Moves the devices to a new database/inventory. Devices cannot be
|
|
|
|
|
modified anymore at the previous database.
|
|
|
|
|
"""
|
2018-08-03 16:15:08 +00:00
|
|
|
|
other = Column(URL(), nullable=False)
|
|
|
|
|
other.comment = """
|
|
|
|
|
The URL of the Migrate in the other end.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MigrateTo(Migrate):
|
2018-07-22 20:42:49 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-08-03 16:15:08 +00:00
|
|
|
|
class MigrateFrom(Migrate):
|
2018-07-22 20:42:49 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
# Listeners
|
2018-06-16 10:41:12 +00:00
|
|
|
|
# Listeners validate values and keep relationships synced
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
# The following listeners avoids setting values to actions that
|
2018-07-14 14:41:22 +00:00
|
|
|
|
# do not make sense. For example, EraseBasic to a graphic card.
|
|
|
|
|
|
2018-06-16 10:41:12 +00:00
|
|
|
|
@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)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def validate_device_is_data_storage(target: Action, value: DataStorage, old_value, initiator):
|
|
|
|
|
"""Validates that the device for data-storage actions is effectively a data storage."""
|
2018-06-16 10:41:12 +00:00
|
|
|
|
if value and not isinstance(value, DataStorage):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
|
|
|
|
|
2018-06-16 10:41:12 +00:00
|
|
|
|
|
2018-07-02 10:52:54 +00:00
|
|
|
|
@event.listens_for(BenchmarkRamSysbench.device, Events.set.__name__, propagate=True)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def actions_not_for_components(target: Action, value: Device, old_value, initiator):
|
|
|
|
|
"""Validates actions that cannot be performed to components."""
|
2018-07-02 10:52:54 +00:00
|
|
|
|
if isinstance(value, Component):
|
|
|
|
|
raise TypeError('{!r} cannot be performed to a component ({!r}).'.format(target, value))
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
# The following listeners keep relationships with device <-> components synced with the action
|
|
|
|
|
# So, if you add or remove devices from actions these listeners will
|
|
|
|
|
# automatically add/remove the ``components`` and ``parent`` of such actions
|
2018-06-16 10:41:12 +00:00
|
|
|
|
# See the tests for examples
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
@event.listens_for(ActionWithOneDevice.device, Events.set.__name__, propagate=True)
|
|
|
|
|
def update_components_action_one(target: ActionWithOneDevice, device: Device, __, ___):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Syncs the :attr:`.Action.components` with the components in
|
2018-06-16 10:41:12 +00:00
|
|
|
|
:attr:`ereuse_devicehub.resources.device.models.Computer.components`.
|
|
|
|
|
"""
|
2018-06-16 13:33:56 +00:00
|
|
|
|
# For Add and Remove, ``components`` have different meanings
|
2019-05-11 14:27:22 +00:00
|
|
|
|
# see Action.components for more info
|
2018-06-16 13:33:56 +00:00
|
|
|
|
if not isinstance(target, (Add, Remove)):
|
|
|
|
|
target.components.clear()
|
|
|
|
|
if isinstance(device, Computer):
|
|
|
|
|
target.components |= device.components
|
2020-11-25 17:42:36 +00:00
|
|
|
|
elif isinstance(device, Computer):
|
2020-11-12 19:58:38 +00:00
|
|
|
|
device.add_mac_to_hid()
|
2018-06-16 10:41:12 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
@event.listens_for(ActionWithMultipleDevices.devices, Events.init_collection.__name__,
|
2018-06-16 10:41:12 +00:00
|
|
|
|
propagate=True)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
@event.listens_for(ActionWithMultipleDevices.devices, Events.bulk_replace.__name__, propagate=True)
|
|
|
|
|
@event.listens_for(ActionWithMultipleDevices.devices, Events.append.__name__, propagate=True)
|
|
|
|
|
def update_components_action_multiple(target: ActionWithMultipleDevices,
|
|
|
|
|
value: Union[Set[Device], Device], _):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Syncs the :attr:`.Action.components` with the components in
|
2018-06-16 10:41:12 +00:00
|
|
|
|
: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
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
@event.listens_for(ActionWithMultipleDevices.devices, Events.remove.__name__, propagate=True)
|
|
|
|
|
def remove_components_action_multiple(target: ActionWithMultipleDevices, device: Device, __):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Syncs the :attr:`.Action.components` with the components in
|
2018-06-16 10:41:12 +00:00
|
|
|
|
: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, _, __):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Syncs the :attr:`Action.parent` with the parent of the device."""
|
2018-06-16 10:41:12 +00:00
|
|
|
|
target.parent = None
|
|
|
|
|
if isinstance(device, Component):
|
|
|
|
|
target.parent = device.parent
|
2019-04-30 00:02:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidRangeForPrice(ValueError):
|
|
|
|
|
pass
|