2018-06-10 16:47:49 +00:00
|
|
|
from uuid import uuid4
|
2018-04-10 15:06:39 +00:00
|
|
|
|
2018-04-30 17:58:19 +00:00
|
|
|
from flask import g
|
2018-04-10 15:06:39 +00:00
|
|
|
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \
|
2018-06-10 16:47:49 +00:00
|
|
|
Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event
|
2018-04-10 15:06:39 +00:00
|
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
from sqlalchemy.ext.declarative import declared_attr
|
2018-06-10 16:47:49 +00:00
|
|
|
from sqlalchemy.ext.orderinglist import ordering_list
|
|
|
|
from sqlalchemy.orm import backref, relationship
|
2018-05-30 10:49:40 +00:00
|
|
|
from sqlalchemy.util import OrderedSet
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
from ereuse_devicehub.db import db
|
2018-06-10 16:47:49 +00:00
|
|
|
from ereuse_devicehub.resources.device.models import Component, 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
|
|
|
|
from ereuse_devicehub.resources.image.models import Image
|
2018-05-30 10:49:40 +00:00
|
|
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
|
2018-04-27 17:16:43 +00:00
|
|
|
from ereuse_devicehub.resources.user.models import User
|
2018-06-10 16:47:49 +00:00
|
|
|
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \
|
|
|
|
POLYMORPHIC_ON, StrictVersionType, check_range
|
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):
|
2018-06-10 16:47:49 +00:00
|
|
|
return Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Event(Thing):
|
2018-06-10 16:47:49 +00:00
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
2018-04-27 17:16:43 +00:00
|
|
|
title = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
|
2018-04-10 15:06:39 +00:00
|
|
|
type = Column(Unicode)
|
|
|
|
incidence = Column(Boolean, default=False, nullable=False)
|
2018-06-10 16:47:49 +00:00
|
|
|
closed = Column(Boolean, default=True, nullable=False)
|
|
|
|
"""
|
|
|
|
Whether the author has finished the event.
|
|
|
|
After this is set to True, no modifications are allowed.
|
|
|
|
"""
|
|
|
|
error = Column(Boolean, default=False, nullable=False)
|
2018-04-27 17:16:43 +00:00
|
|
|
description = Column(Unicode, default='', nullable=False)
|
2018-06-10 16:47:49 +00:00
|
|
|
date = Column(DateTime)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
|
|
|
|
use_alter=True,
|
|
|
|
name='snapshot_events'))
|
2018-04-10 15:06:39 +00:00
|
|
|
snapshot = relationship('Snapshot',
|
2018-05-30 10:49:40 +00:00
|
|
|
backref=backref('events',
|
|
|
|
lazy=True,
|
2018-06-10 16:47:49 +00:00
|
|
|
cascade=CASCADE_OWN,
|
|
|
|
collection_class=set),
|
2018-04-10 15:06:39 +00:00
|
|
|
primaryjoin='Event.snapshot_id == Snapshot.id')
|
|
|
|
|
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-04-10 15:06:39 +00:00
|
|
|
author = relationship(User,
|
2018-05-30 10:49:40 +00:00
|
|
|
backref=backref('events', lazy=True, collection_class=set),
|
2018-04-10 15:06:39 +00:00
|
|
|
primaryjoin=author_id == User.id)
|
2018-05-13 13:13:12 +00:00
|
|
|
components = relationship(Component,
|
|
|
|
backref=backref('events_components',
|
|
|
|
lazy=True,
|
2018-06-10 16:47:49 +00:00
|
|
|
order_by=lambda: Event.created,
|
2018-05-30 10:49:40 +00:00
|
|
|
collection_class=OrderedSet),
|
2018-05-13 13:13:12 +00:00
|
|
|
secondary=lambda: EventComponent.__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)
|
2018-06-10 16:47:49 +00:00
|
|
|
"""
|
|
|
|
The components that are affected by the event.
|
|
|
|
|
|
|
|
When performing events to parent devices their components are
|
|
|
|
affected too.
|
|
|
|
|
|
|
|
For example: an ``Allocate`` is performed to a Computer and this
|
|
|
|
relationship is filled with the components the computer had
|
|
|
|
at the time of the event.
|
|
|
|
"""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
# noinspection PyMethodParameters
|
2018-04-10 15:06:39 +00:00
|
|
|
@declared_attr
|
|
|
|
def __mapper_args__(cls):
|
|
|
|
"""
|
|
|
|
Defines inheritance.
|
|
|
|
|
|
|
|
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}
|
|
|
|
if cls.t == 'Event':
|
2018-04-10 15:06:39 +00:00
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
if JoinedTableMixin in cls.mro():
|
|
|
|
args[INHERIT_COND] = cls.id == Event.id
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
|
class EventComponent(db.Model):
|
2018-06-10 16:47:49 +00:00
|
|
|
device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
|
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class EventWithOneDevice(Event):
|
|
|
|
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
|
|
|
device = relationship(Device,
|
2018-05-13 13:13:12 +00:00
|
|
|
backref=backref('events_one',
|
|
|
|
lazy=True,
|
|
|
|
cascade=CASCADE,
|
2018-06-10 16:47:49 +00:00
|
|
|
order_by=lambda: EventWithOneDevice.created,
|
2018-05-30 10:49:40 +00:00
|
|
|
collection_class=OrderedSet),
|
2018-04-10 15:06:39 +00:00
|
|
|
primaryjoin=Device.id == device_id)
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
def __repr__(self) -> str:
|
2018-06-10 16:47:49 +00:00
|
|
|
return '<{0.t} {0.id!r} device={0.device_id}>'.format(self)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
class EventWithMultipleDevices(Event):
|
|
|
|
devices = relationship(Device,
|
2018-05-13 13:13:12 +00:00
|
|
|
backref=backref('events_multiple',
|
|
|
|
lazy=True,
|
2018-06-10 16:47:49 +00:00
|
|
|
order_by=lambda: EventWithMultipleDevices.created,
|
2018-05-30 10:49:40 +00:00
|
|
|
collection_class=OrderedSet),
|
2018-05-13 13:13:12 +00:00
|
|
|
secondary=lambda: EventDevice.__table__,
|
|
|
|
order_by=lambda: Device.id)
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
return '<{0.t} {0.id!r} devices={0.devices!r}>'.format(self)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class EventDevice(db.Model):
|
|
|
|
device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2018-06-10 16:47:49 +00:00
|
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey(EventWithMultipleDevices.id),
|
|
|
|
primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Add(EventWithOneDevice):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Remove(EventWithOneDevice):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Allocate(JoinedTableMixin, EventWithMultipleDevices):
|
2018-04-27 17:16:43 +00:00
|
|
|
to_id = Column(UUID, ForeignKey(User.id))
|
2018-04-10 15:06:39 +00:00
|
|
|
to = relationship(User, primaryjoin=User.id == to_id)
|
2018-04-27 17:16:43 +00:00
|
|
|
organization = Column(Unicode(STR_SIZE))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Deallocate(JoinedTableMixin, EventWithMultipleDevices):
|
2018-04-27 17:16:43 +00:00
|
|
|
from_id = Column(UUID, ForeignKey(User.id))
|
2018-04-10 15:06:39 +00:00
|
|
|
from_rel = relationship(User, primaryjoin=User.id == from_id)
|
2018-04-27 17:16:43 +00:00
|
|
|
organization = Column(Unicode(STR_SIZE))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class EraseBasic(JoinedTableMixin, EventWithOneDevice):
|
2018-06-10 16:47:49 +00:00
|
|
|
start_time = Column(DateTime, nullable=False)
|
|
|
|
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
|
|
|
secure_random_steps = Column(SmallInteger,
|
|
|
|
check_range('secure_random_steps', min=0),
|
2018-04-10 15:06:39 +00:00
|
|
|
nullable=False)
|
|
|
|
clean_with_zeros = Column(Boolean, nullable=False)
|
|
|
|
|
|
|
|
|
|
|
|
class EraseSectors(EraseBasic):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Step(db.Model):
|
2018-06-10 16:47:49 +00:00
|
|
|
erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True)
|
|
|
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
|
|
|
num = Column(SmallInteger, primary_key=True)
|
|
|
|
error = Column(Boolean, default=False, nullable=False)
|
|
|
|
start_time = Column(DateTime, nullable=False)
|
|
|
|
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
|
|
|
secure_random_steps = Column(SmallInteger,
|
|
|
|
check_range('secure_random_steps', min=0),
|
2018-04-10 15:06:39 +00:00
|
|
|
nullable=False)
|
|
|
|
clean_with_zeros = Column(Boolean, nullable=False)
|
|
|
|
|
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')))
|
|
|
|
|
|
|
|
# noinspection PyMethodParameters
|
|
|
|
@declared_attr
|
|
|
|
def __mapper_args__(cls):
|
|
|
|
"""
|
|
|
|
Defines inheritance.
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
class StepZero(Step):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class StepRandom(Step):
|
|
|
|
pass
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Snapshot(JoinedTableMixin, EventWithOneDevice):
|
2018-06-10 16:47:49 +00:00
|
|
|
uuid = Column(UUID(as_uuid=True), nullable=False, unique=True)
|
|
|
|
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
|
|
|
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
|
|
|
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
|
|
|
|
|
|
|
|
|
|
|
|
class Install(JoinedTableMixin, EventWithOneDevice):
|
|
|
|
name = Column(Unicode(STR_BIG_SIZE), nullable=False)
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
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
|
|
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
class Rate(JoinedTableMixin, EventWithOneDevice):
|
|
|
|
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
|
|
|
algorithm_software = Column(DBEnum(RatingSoftware), nullable=False)
|
|
|
|
algorithm_version = Column(StrictVersionType, nullable=False)
|
|
|
|
appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE))
|
|
|
|
functionality = Column(Float(decimal_return_scale=2),
|
|
|
|
check_range('functionality', *RATE_NEGATIVE))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def rating_range(self) -> RatingRange:
|
|
|
|
return RatingRange.from_score(self.rating)
|
|
|
|
|
|
|
|
|
|
|
|
class IndividualRate(Rate):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class AggregateRate(Rate):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
|
|
|
ratings = relationship(IndividualRate,
|
|
|
|
backref=backref('aggregated_ratings',
|
|
|
|
lazy=True,
|
|
|
|
order_by=lambda: IndividualRate.created,
|
|
|
|
collection_class=OrderedSet),
|
|
|
|
secondary=lambda: RateAggregateRate.__table__,
|
|
|
|
order_by=lambda: IndividualRate.created,
|
|
|
|
collection_class=OrderedSet)
|
|
|
|
"""The ratings this aggregateRate aggregates."""
|
|
|
|
|
|
|
|
|
|
|
|
class RateAggregateRate(db.Model):
|
|
|
|
"""
|
|
|
|
Represents the ``many to many`` relationship between
|
|
|
|
``Rate`` and ``AggregateRate``.
|
|
|
|
"""
|
|
|
|
rate_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
|
|
|
aggregate_rate_id = Column(UUID(as_uuid=True),
|
|
|
|
ForeignKey(AggregateRate.id),
|
|
|
|
primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
class WorkbenchRate(IndividualRate):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
|
|
|
processor = Column(Float(decimal_return_scale=2), check_range('processor', *RATE_POSITIVE))
|
|
|
|
ram = Column(Float(decimal_return_scale=2), check_range('ram', *RATE_POSITIVE))
|
|
|
|
data_storage = Column(Float(decimal_return_scale=2),
|
|
|
|
check_range('data_storage', *RATE_POSITIVE))
|
|
|
|
graphic_card = Column(Float(decimal_return_scale=2),
|
|
|
|
check_range('graphic_card', *RATE_POSITIVE))
|
|
|
|
labelling = Column(Boolean)
|
|
|
|
bios = Column(DBEnum(Bios))
|
|
|
|
appearance_range = Column(DBEnum(AppearanceRange))
|
|
|
|
functionality_range = Column(DBEnum(FunctionalityRange))
|
|
|
|
|
|
|
|
|
|
|
|
class PhotoboxRate(IndividualRate):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
|
|
|
image_id = Column(UUID(as_uuid=True), ForeignKey(Image.id), nullable=False)
|
|
|
|
image = relationship(Image,
|
|
|
|
uselist=False,
|
|
|
|
cascade=CASCADE_OWN,
|
|
|
|
single_parent=True,
|
|
|
|
primaryjoin=Image.id == image_id)
|
|
|
|
|
|
|
|
# todo how to ensure phtoboxrate.device == image.image_list.device?
|
|
|
|
|
|
|
|
|
|
|
|
class PhotoboxUserRate(PhotoboxRate):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
|
|
|
|
assembling = Column(SmallInteger, check_range('assembling', *BOX_RATE_5), nullable=False)
|
|
|
|
parts = Column(SmallInteger, check_range('parts', *BOX_RATE_5), nullable=False)
|
|
|
|
buttons = Column(SmallInteger, check_range('buttons', *BOX_RATE_5), nullable=False)
|
|
|
|
dents = Column(SmallInteger, check_range('dents', *BOX_RATE_5), nullable=False)
|
|
|
|
decolorization = Column(SmallInteger,
|
|
|
|
check_range('decolorization', *BOX_RATE_5),
|
|
|
|
nullable=False)
|
|
|
|
scratches = Column(SmallInteger, check_range('scratches', *BOX_RATE_5), nullable=False)
|
|
|
|
tag_alignment = Column(SmallInteger,
|
|
|
|
check_range('tag_alignment', *BOX_RATE_3),
|
|
|
|
nullable=False)
|
|
|
|
tag_adhesive = Column(SmallInteger, check_range('tag_adhesive', *BOX_RATE_3), nullable=False)
|
|
|
|
dirt = Column(SmallInteger, check_range('dirt', *BOX_RATE_3), nullable=False)
|
|
|
|
|
|
|
|
|
|
|
|
class PhotoboxSystemRate(PhotoboxRate):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
|
|
|
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
class Test(JoinedTableMixin, EventWithOneDevice):
|
|
|
|
elapsed = Column(Interval, nullable=False)
|
|
|
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
class TestDataStorage(Test):
|
|
|
|
id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
length = Column(DBEnum(TestHardDriveLength), nullable=False) # todo from type
|
|
|
|
status = Column(Unicode(STR_SIZE), nullable=False)
|
|
|
|
lifetime = Column(Interval, nullable=False)
|
2018-06-10 16:47:49 +00:00
|
|
|
first_error = Column(SmallInteger, nullable=False, default=0)
|
|
|
|
passed_lifetime = Column(Interval)
|
|
|
|
assessment = Column(Boolean)
|
|
|
|
reallocated_sector_count = Column(SmallInteger)
|
|
|
|
power_cycle_count = Column(SmallInteger)
|
|
|
|
reported_uncorrectable_errors = Column(SmallInteger)
|
|
|
|
command_timeout = Column(SmallInteger)
|
|
|
|
current_pending_sector_count = Column(SmallInteger)
|
|
|
|
offline_uncorrectable = Column(SmallInteger)
|
|
|
|
remaining_lifetime_percentage = Column(SmallInteger)
|
|
|
|
|
|
|
|
# todo remove lifetime / passed_lifetime as I think they are the same
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
class StressTest(Test):
|
|
|
|
pass
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
# 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, value, old_value, initiator):
|
|
|
|
if not isinstance(value, DataStorage):
|
|
|
|
raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value))
|
|
|
|
return value
|
|
|
|
|
|
|
|
# todo finish adding events
|
|
|
|
# @event.listens_for(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))
|