2018-10-05 12:35:51 +00:00
|
|
|
|
import csv
|
2018-09-30 17:40:28 +00:00
|
|
|
|
import pathlib
|
2018-04-27 17:16:43 +00:00
|
|
|
|
from contextlib import suppress
|
2018-08-03 16:15:08 +00:00
|
|
|
|
from itertools import chain
|
2018-05-13 13:13:12 +00:00
|
|
|
|
from operator import attrgetter
|
2018-10-03 12:51:22 +00:00
|
|
|
|
from typing import Dict, List, Set
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-10-05 15:13:23 +00:00
|
|
|
|
from boltons import urlutils
|
2018-09-30 17:40:28 +00:00
|
|
|
|
from citext import CIText
|
2018-09-07 10:38:02 +00:00
|
|
|
|
from ereuse_utils.naming import Naming
|
2018-11-09 10:22:13 +00:00
|
|
|
|
from more_itertools import unique_everseen
|
2018-08-03 16:15:08 +00:00
|
|
|
|
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
2018-10-08 15:32:45 +00:00
|
|
|
|
Sequence, SmallInteger, Unicode, inspect, text
|
2018-04-10 15:06:39 +00:00
|
|
|
|
from sqlalchemy.ext.declarative import declared_attr
|
2018-06-26 13:36:21 +00:00
|
|
|
|
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
|
2018-05-30 10:49:40 +00:00
|
|
|
|
from sqlalchemy.util import OrderedSet
|
2018-06-10 16:47:49 +00:00
|
|
|
|
from sqlalchemy_utils import ColorType
|
2018-06-26 13:36:21 +00:00
|
|
|
|
from stdnum import imei, meid
|
2018-11-11 20:52:55 +00:00
|
|
|
|
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \
|
|
|
|
|
check_lower, check_range
|
2018-10-23 13:37:37 +00:00
|
|
|
|
from teal.enums import Layouts
|
2018-09-07 10:38:02 +00:00
|
|
|
|
from teal.marshmallow import ValidationError
|
2018-10-05 15:13:23 +00:00
|
|
|
|
from teal.resource import url_for_resource
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-09-30 17:40:28 +00:00
|
|
|
|
from ereuse_devicehub.db import db
|
2018-11-09 10:22:13 +00:00
|
|
|
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
|
|
|
|
PrinterTechnology, RamFormat, RamInterface, Severity
|
2018-09-30 10:29:33 +00:00
|
|
|
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
2018-07-02 10:52:54 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
class Device(Thing):
|
2018-06-24 14:57:49 +00:00
|
|
|
|
"""
|
|
|
|
|
Base class for any type of physical object that can be identified.
|
|
|
|
|
"""
|
2018-11-09 10:22:13 +00:00
|
|
|
|
EVENT_SORT_KEY = attrgetter('created')
|
2018-06-24 14:57:49 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
2018-06-20 21:18:15 +00:00
|
|
|
|
id.comment = """
|
|
|
|
|
The identifier of the device for this database.
|
|
|
|
|
"""
|
2018-11-04 22:00:51 +00:00
|
|
|
|
type = Column(Unicode(STR_SM_SIZE), nullable=False, index=True)
|
2018-09-30 10:29:33 +00:00
|
|
|
|
hid = Column(Unicode(), check_lower('hid'), unique=True)
|
2018-06-20 21:18:15 +00:00
|
|
|
|
hid.comment = """
|
|
|
|
|
The Hardware ID (HID) is the unique ID traceability systems
|
|
|
|
|
use to ID a device globally.
|
|
|
|
|
"""
|
2018-09-30 10:29:33 +00:00
|
|
|
|
model = Column(Unicode(), check_lower('model'))
|
|
|
|
|
manufacturer = Column(Unicode(), check_lower('manufacturer'))
|
|
|
|
|
serial_number = Column(Unicode(), check_lower('serial_number'))
|
2018-10-13 12:53:46 +00:00
|
|
|
|
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
|
2018-06-20 21:18:15 +00:00
|
|
|
|
weight.comment = """
|
|
|
|
|
The weight of the device in Kgm.
|
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
|
2018-06-20 21:18:15 +00:00
|
|
|
|
width.comment = """
|
|
|
|
|
The width of the device in meters.
|
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
|
2018-06-20 21:18:15 +00:00
|
|
|
|
height.comment = """
|
|
|
|
|
The height of the device in meters.
|
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
|
|
|
|
|
depth.comment = """
|
|
|
|
|
The depth of the device in meters.
|
|
|
|
|
"""
|
2018-06-10 16:47:49 +00:00
|
|
|
|
color = Column(ColorType)
|
2018-10-05 15:13:23 +00:00
|
|
|
|
color.comment = """The predominant color of the device."""
|
2018-10-23 13:37:37 +00:00
|
|
|
|
production_date = Column(db.TIMESTAMP(timezone=True))
|
|
|
|
|
production_date.comment = """The date of production of the item."""
|
|
|
|
|
|
|
|
|
|
_NON_PHYSICAL_PROPS = {
|
|
|
|
|
'id',
|
|
|
|
|
'type',
|
|
|
|
|
'created',
|
|
|
|
|
'updated',
|
|
|
|
|
'parent_id',
|
|
|
|
|
'hid',
|
|
|
|
|
'production_date',
|
|
|
|
|
'color'
|
|
|
|
|
}
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
def __init__(self, **kw) -> None:
|
|
|
|
|
super().__init__(**kw)
|
|
|
|
|
with suppress(TypeError):
|
|
|
|
|
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
@property
|
|
|
|
|
def events(self) -> list:
|
2018-06-10 16:47:49 +00:00
|
|
|
|
"""
|
2018-06-24 14:57:49 +00:00
|
|
|
|
All the events where the device participated, including
|
|
|
|
|
1) events performed directly to the device, 2) events performed
|
|
|
|
|
to a component, and 3) events performed to a parent device.
|
|
|
|
|
|
|
|
|
|
Events are returned by ascending creation time.
|
2018-06-10 16:47:49 +00:00
|
|
|
|
"""
|
2018-11-09 10:22:13 +00:00
|
|
|
|
return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
@property
|
|
|
|
|
def problems(self):
|
|
|
|
|
"""Current events with severity.Warning or higher.
|
|
|
|
|
|
|
|
|
|
There can be up to 3 events: current Snapshot,
|
|
|
|
|
current Physical event, current Trading event.
|
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
from ereuse_devicehub.resources.event.models import Snapshot
|
|
|
|
|
events = set()
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
events.add(self.last_event_of(Snapshot))
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
events.add(self.last_event_of(*states.Physical.events()))
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
events.add(self.last_event_of(*states.Trading.events()))
|
|
|
|
|
return self._warning_events(events)
|
2018-04-30 17:58:19 +00:00
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
@property
|
2018-04-30 17:58:19 +00:00
|
|
|
|
def physical_properties(self) -> Dict[str, object or None]:
|
2018-04-27 17:16:43 +00:00
|
|
|
|
"""
|
|
|
|
|
Fields that describe the physical properties of a device.
|
|
|
|
|
|
|
|
|
|
:return A generator where each value is a tuple with tho fields:
|
|
|
|
|
- Column.
|
|
|
|
|
- Actual value of the column or None.
|
|
|
|
|
"""
|
|
|
|
|
# todo ensure to remove materialized values when start using them
|
|
|
|
|
# todo or self.__table__.columns if inspect fails
|
2018-04-30 17:58:19 +00:00
|
|
|
|
return {c.key: getattr(self, c.key, None)
|
2018-04-27 17:16:43 +00:00
|
|
|
|
for c in inspect(self.__class__).attrs
|
2018-04-30 17:58:19 +00:00
|
|
|
|
if isinstance(c, ColumnProperty)
|
|
|
|
|
and not getattr(c, 'foreign_keys', None)
|
2018-10-23 13:37:37 +00:00
|
|
|
|
and c.key not in self._NON_PHYSICAL_PROPS}
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-10-05 15:13:23 +00:00
|
|
|
|
@property
|
|
|
|
|
def url(self) -> urlutils.URL:
|
|
|
|
|
"""The URL where to GET this device."""
|
|
|
|
|
return urlutils.URL(url_for_resource(Device, item_id=self.id))
|
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
@property
|
|
|
|
|
def rate(self):
|
2018-10-14 18:10:52 +00:00
|
|
|
|
"""The last AggregateRate of the device."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
from ereuse_devicehub.resources.event.models import AggregateRate
|
|
|
|
|
return self.last_event_of(AggregateRate)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def price(self):
|
2018-10-14 18:10:52 +00:00
|
|
|
|
"""The actual Price of the device, or None if no price has
|
|
|
|
|
ever been set."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
from ereuse_devicehub.resources.event.models import Price
|
|
|
|
|
return self.last_event_of(Price)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def trading(self):
|
2018-10-14 18:10:52 +00:00
|
|
|
|
"""The actual trading state, or None if no Trade event has
|
|
|
|
|
ever been performed to this device."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
event = self.last_event_of(*states.Trading.events())
|
|
|
|
|
return states.Trading(event.__class__)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def physical(self):
|
2018-10-14 18:10:52 +00:00
|
|
|
|
"""The actual physical state, None otherwise."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
event = self.last_event_of(*states.Physical.events())
|
|
|
|
|
return states.Physical(event.__class__)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def physical_possessor(self):
|
|
|
|
|
"""The actual physical possessor or None.
|
|
|
|
|
|
|
|
|
|
The physical possessor is the Agent that has physically
|
|
|
|
|
the device. It differs from legal owners, usufructuarees
|
|
|
|
|
or reserves in that the physical possessor does not have
|
|
|
|
|
a legal relation per se with the device, but it is the one
|
|
|
|
|
that has it physically. As an example, a transporter could
|
|
|
|
|
be a physical possessor of a device although it does not
|
|
|
|
|
own it legally.
|
2018-11-12 17:15:24 +00:00
|
|
|
|
|
|
|
|
|
Note that there can only be one physical possessor per device,
|
|
|
|
|
and :class:`ereuse_devicehub.resources.event.models.Receive`
|
|
|
|
|
changes it.
|
2018-10-13 12:53:46 +00:00
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.event.models import Receive
|
|
|
|
|
with suppress(LookupError):
|
|
|
|
|
event = self.last_event_of(Receive)
|
|
|
|
|
return event.agent
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
@property
|
|
|
|
|
def working(self):
|
|
|
|
|
"""A list of the current tests with warning or errors. A
|
|
|
|
|
device is working if the list is empty.
|
|
|
|
|
|
|
|
|
|
This property returns, for the last test performed of each type,
|
2018-11-12 17:15:24 +00:00
|
|
|
|
the one with the worst severity of them, or `None` if no
|
2018-11-09 10:22:13 +00:00
|
|
|
|
test has been executed.
|
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.event.models import Test
|
|
|
|
|
current_tests = unique_everseen((e for e in reversed(self.events) if isinstance(e, Test)),
|
|
|
|
|
key=attrgetter('type')) # last test of each type
|
|
|
|
|
return self._warning_events(current_tests)
|
|
|
|
|
|
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 == 'Device':
|
2018-04-10 15:06:39 +00:00
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
def last_event_of(self, *types):
|
|
|
|
|
"""Gets the last event of the given types.
|
|
|
|
|
|
|
|
|
|
:raise LookupError: Device has not an event of the given type.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return next(e for e in reversed(self.events) if isinstance(e, types))
|
|
|
|
|
except StopIteration:
|
|
|
|
|
raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
def _warning_events(self, events):
|
|
|
|
|
return sorted((ev for ev in events if ev.severity >= Severity.Warning),
|
|
|
|
|
key=self.EVENT_SORT_KEY)
|
|
|
|
|
|
2018-04-30 17:58:19 +00:00
|
|
|
|
def __lt__(self, other):
|
|
|
|
|
return self.id < other.id
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return '{0.t} {0.id}: model {0.model}, S/N {0.serial_number}'.format(self)
|
|
|
|
|
|
|
|
|
|
def __format__(self, format_spec):
|
|
|
|
|
if not format_spec:
|
|
|
|
|
return super().__format__(format_spec)
|
|
|
|
|
v = ''
|
|
|
|
|
if 't' in format_spec:
|
|
|
|
|
v += '{0.t} {0.model}'.format(self)
|
|
|
|
|
if 's' in format_spec:
|
2018-10-16 14:30:10 +00:00
|
|
|
|
v += '({0.manufacturer})'.format(self)
|
|
|
|
|
if self.serial_number:
|
|
|
|
|
v += ' S/N ' + self.serial_number.upper()
|
2018-10-03 12:51:22 +00:00
|
|
|
|
return v
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class DisplayMixin:
|
2018-10-23 13:37:37 +00:00
|
|
|
|
"""
|
|
|
|
|
Aspect ratio can be computed as in
|
|
|
|
|
https://github.com/mirukan/whratio/blob/master/whratio/ratio.py and
|
|
|
|
|
could be a future property.
|
|
|
|
|
"""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150))
|
|
|
|
|
size.comment = """
|
|
|
|
|
The size of the monitor in inches.
|
|
|
|
|
"""
|
|
|
|
|
technology = Column(DBEnum(DisplayTech))
|
|
|
|
|
technology.comment = """
|
|
|
|
|
The technology the monitor uses to display the image.
|
|
|
|
|
"""
|
|
|
|
|
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000))
|
|
|
|
|
resolution_width.comment = """
|
|
|
|
|
The maximum horizontal resolution the monitor can natively support
|
|
|
|
|
in pixels.
|
|
|
|
|
"""
|
|
|
|
|
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000))
|
|
|
|
|
resolution_height.comment = """
|
|
|
|
|
The maximum vertical resolution the monitor can natively support
|
|
|
|
|
in pixels.
|
|
|
|
|
"""
|
2018-10-23 13:37:37 +00:00
|
|
|
|
refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000))
|
|
|
|
|
contrast_ratio = Column(SmallInteger, check_range('contrast_ratio', 100, 100000))
|
|
|
|
|
touchable = Column(Boolean, nullable=False, default=False)
|
|
|
|
|
touchable.comment = """Whether it is a touchscreen."""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
def __format__(self, format_spec: str) -> str:
|
|
|
|
|
v = ''
|
|
|
|
|
if 't' in format_spec:
|
|
|
|
|
v += '{0.t} {0.model}'.format(self)
|
|
|
|
|
if 's' in format_spec:
|
|
|
|
|
v += '({0.manufacturer}) S/N {0.serial_number} – {0.size}in {0.technology}'
|
|
|
|
|
return v
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
class Computer(Device):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2018-06-26 13:36:21 +00:00
|
|
|
|
chassis = Column(DBEnum(ComputerChassis), nullable=False)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-10-18 08:09:10 +00:00
|
|
|
|
def __init__(self, chassis, **kwargs) -> None:
|
|
|
|
|
chassis = ComputerChassis(chassis)
|
|
|
|
|
super().__init__(chassis=chassis, **kwargs)
|
|
|
|
|
|
2018-06-16 10:41:12 +00:00
|
|
|
|
@property
|
|
|
|
|
def events(self) -> list:
|
2018-11-09 10:22:13 +00:00
|
|
|
|
return sorted(chain(super().events, self.events_parent), key=self.EVENT_SORT_KEY)
|
2018-06-16 10:41:12 +00:00
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
@property
|
|
|
|
|
def ram_size(self) -> int:
|
|
|
|
|
"""The total of RAM memory the computer has."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule))
|
2018-10-03 12:51:22 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def data_storage_size(self) -> int:
|
|
|
|
|
"""The total of data storage the computer has."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage))
|
2018-10-03 12:51:22 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def processor_model(self) -> str:
|
|
|
|
|
"""The model of one of the processors of the computer."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
return next((p.model for p in self.components if isinstance(p, Processor)), None)
|
2018-10-03 12:51:22 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def graphic_card_model(self) -> str:
|
|
|
|
|
"""The model of one of the graphic cards of the computer."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
return next((p.model for p in self.components if isinstance(p, GraphicCard)), None)
|
2018-10-03 12:51:22 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def network_speeds(self) -> List[int]:
|
2018-10-13 12:53:46 +00:00
|
|
|
|
"""Returns two values representing the speeds of the network
|
|
|
|
|
adapters of the device.
|
|
|
|
|
|
|
|
|
|
1. The max Ethernet speed of the computer, 0 if ethernet
|
|
|
|
|
adaptor exists but its speed is unknown, None if no eth
|
|
|
|
|
adaptor exists.
|
|
|
|
|
2. The max WiFi speed of the computer, 0 if computer has
|
|
|
|
|
WiFi but its speed is unknown, None if no WiFi adaptor
|
|
|
|
|
exists.
|
2018-10-03 12:51:22 +00:00
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
speeds = [None, None]
|
2018-10-03 12:51:22 +00:00
|
|
|
|
for net in (c for c in self.components if isinstance(c, NetworkAdapter)):
|
2018-10-13 12:53:46 +00:00
|
|
|
|
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0)
|
2018-10-03 12:51:22 +00:00
|
|
|
|
return speeds
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
@property
|
|
|
|
|
def privacy(self):
|
|
|
|
|
"""Returns the privacy of all DataStorage components when
|
|
|
|
|
it is None.
|
|
|
|
|
"""
|
|
|
|
|
return set(
|
|
|
|
|
privacy for privacy in
|
|
|
|
|
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
|
|
|
|
|
if privacy
|
|
|
|
|
)
|
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
def __format__(self, format_spec):
|
|
|
|
|
if not format_spec:
|
|
|
|
|
return super().__format__(format_spec)
|
|
|
|
|
v = ''
|
|
|
|
|
if 't' in format_spec:
|
|
|
|
|
v += '{0.chassis} {0.model}'.format(self)
|
|
|
|
|
elif 's' in format_spec:
|
2018-10-16 14:30:10 +00:00
|
|
|
|
v += '({0.manufacturer})'.format(self)
|
|
|
|
|
if self.serial_number:
|
|
|
|
|
v += ' S/N ' + self.serial_number.upper()
|
2018-10-03 12:51:22 +00:00
|
|
|
|
return v
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
class Desktop(Computer):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Laptop(Computer):
|
2018-10-23 13:37:37 +00:00
|
|
|
|
layout = Column(DBEnum(Layouts))
|
|
|
|
|
layout.comment = """Layout of a built-in keyboard of the computer,
|
|
|
|
|
if any."""
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class Server(Computer):
|
2018-04-10 15:06:39 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class Monitor(DisplayMixin, Device):
|
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ComputerMonitor(Monitor):
|
2018-04-10 15:06:39 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class TelevisionSet(Monitor):
|
2018-04-10 15:06:39 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
class Projector(Monitor):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class Mobile(Device):
|
2018-06-20 21:18:15 +00:00
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2018-06-26 13:36:21 +00:00
|
|
|
|
imei = Column(BigInteger)
|
|
|
|
|
imei.comment = """
|
|
|
|
|
The International Mobile Equipment Identity of the smartphone
|
|
|
|
|
as an integer.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
meid = Column(Unicode)
|
|
|
|
|
meid.comment = """
|
|
|
|
|
The Mobile Equipment Identifier as a hexadecimal string.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
@validates('imei')
|
|
|
|
|
def validate_imei(self, _, value: int):
|
2018-10-04 08:59:31 +00:00
|
|
|
|
if not imei.is_valid(str(value)):
|
2018-06-26 13:36:21 +00:00
|
|
|
|
raise ValidationError('{} is not a valid imei.'.format(value))
|
2018-11-17 18:22:41 +00:00
|
|
|
|
return value
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
|
|
|
|
@validates('meid')
|
|
|
|
|
def validate_meid(self, _, value: str):
|
|
|
|
|
if not meid.is_valid(value):
|
|
|
|
|
raise ValidationError('{} is not a valid meid.'.format(value))
|
2018-11-17 18:22:41 +00:00
|
|
|
|
return value
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Smartphone(Mobile):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Tablet(Mobile):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Cellphone(Mobile):
|
|
|
|
|
pass
|
|
|
|
|
|
2018-06-20 21:18:15 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
class Component(Device):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-11-04 22:00:51 +00:00
|
|
|
|
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
parent = relationship(Computer,
|
2018-05-13 13:13:12 +00:00
|
|
|
|
backref=backref('components',
|
|
|
|
|
lazy=True,
|
2018-11-11 20:52:55 +00:00
|
|
|
|
cascade=CASCADE_DEL,
|
2018-05-30 10:49:40 +00:00
|
|
|
|
order_by=lambda: Component.id,
|
|
|
|
|
collection_class=OrderedSet),
|
2018-06-15 13:31:03 +00:00
|
|
|
|
primaryjoin=parent_id == Computer.id)
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
|
|
|
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
|
|
|
|
"""
|
|
|
|
|
Gets a component that:
|
|
|
|
|
- has the same parent.
|
|
|
|
|
- Doesn't generate HID.
|
|
|
|
|
- Has same physical properties.
|
|
|
|
|
:param parent:
|
|
|
|
|
:param blacklist: A set of components to not to consider
|
|
|
|
|
when looking for similar ones.
|
|
|
|
|
"""
|
|
|
|
|
assert self.hid is None, 'Don\'t use this method with a component that has HID'
|
|
|
|
|
component = self.__class__.query \
|
|
|
|
|
.filter_by(parent=parent, hid=None, **self.physical_properties) \
|
|
|
|
|
.filter(~Component.id.in_(blacklist)) \
|
|
|
|
|
.first()
|
|
|
|
|
if not component:
|
|
|
|
|
raise ResourceNotFound(self.type)
|
|
|
|
|
return component
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
@property
|
|
|
|
|
def events(self) -> list:
|
2018-11-09 10:22:13 +00:00
|
|
|
|
return sorted(chain(super().events, self.events_components), key=self.EVENT_SORT_KEY)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class JoinedComponentTableMixin:
|
|
|
|
|
@declared_attr
|
|
|
|
|
def id(cls):
|
|
|
|
|
return Column(BigInteger, ForeignKey(Component.id), primary_key=True)
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
class GraphicCard(JoinedComponentTableMixin, Component):
|
2018-06-10 16:47:49 +00:00
|
|
|
|
memory = Column(SmallInteger, check_range('memory', min=1, max=10000))
|
2018-06-26 13:36:21 +00:00
|
|
|
|
memory.comment = """
|
|
|
|
|
The amount of memory of the Graphic Card in MB.
|
|
|
|
|
"""
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
class DataStorage(JoinedComponentTableMixin, Component):
|
|
|
|
|
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
|
2018-06-26 13:36:21 +00:00
|
|
|
|
size.comment = """
|
|
|
|
|
The size of the data-storage in MB.
|
|
|
|
|
"""
|
2018-06-12 14:50:05 +00:00
|
|
|
|
interface = Column(DBEnum(DataStorageInterface))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
@property
|
|
|
|
|
def privacy(self):
|
|
|
|
|
"""Returns the privacy compliance state of the data storage."""
|
|
|
|
|
from ereuse_devicehub.resources.event.models import EraseBasic
|
2018-11-09 10:22:13 +00:00
|
|
|
|
try:
|
|
|
|
|
ev = self.last_event_of(EraseBasic)
|
|
|
|
|
except LookupError:
|
|
|
|
|
ev = None
|
|
|
|
|
return ev
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
def __format__(self, format_spec):
|
|
|
|
|
v = super().__format__(format_spec)
|
|
|
|
|
if 's' in format_spec:
|
|
|
|
|
v += ' – {} GB'.format(self.size // 1000)
|
|
|
|
|
return v
|
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
|
|
|
|
|
class HardDrive(DataStorage):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SolidStateDrive(DataStorage):
|
|
|
|
|
pass
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
class Motherboard(JoinedComponentTableMixin, Component):
|
2018-07-02 10:52:54 +00:00
|
|
|
|
slots = Column(SmallInteger, check_range('slots', min=0))
|
2018-06-26 13:36:21 +00:00
|
|
|
|
slots.comment = """
|
|
|
|
|
PCI slots the motherboard has.
|
|
|
|
|
"""
|
2018-07-02 10:52:54 +00:00
|
|
|
|
usb = Column(SmallInteger, check_range('usb', min=0))
|
|
|
|
|
firewire = Column(SmallInteger, check_range('firewire', min=0))
|
|
|
|
|
serial = Column(SmallInteger, check_range('serial', min=0))
|
|
|
|
|
pcmcia = Column(SmallInteger, check_range('pcmcia', min=0))
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:35:13 +00:00
|
|
|
|
class NetworkMixin:
|
2018-06-10 16:47:49 +00:00
|
|
|
|
speed = Column(SmallInteger, check_range('speed', min=10, max=10000))
|
2018-06-26 13:35:13 +00:00
|
|
|
|
speed.comment = """
|
|
|
|
|
The maximum speed this network adapter can handle, in mbps.
|
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
wireless = Column(Boolean, nullable=False, default=False)
|
2018-07-19 19:25:06 +00:00
|
|
|
|
wireless.comment = """
|
|
|
|
|
Whether it is a wireless interface.
|
|
|
|
|
"""
|
2018-06-26 13:35:13 +00:00
|
|
|
|
|
2018-10-03 12:51:22 +00:00
|
|
|
|
def __format__(self, format_spec):
|
|
|
|
|
v = super().__format__(format_spec)
|
|
|
|
|
if 's' in format_spec:
|
|
|
|
|
v += ' – {} Mbps'.format(self.speed)
|
|
|
|
|
return v
|
|
|
|
|
|
2018-06-26 13:35:13 +00:00
|
|
|
|
|
|
|
|
|
class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
|
|
|
|
|
pass
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
class Processor(JoinedComponentTableMixin, Component):
|
2018-05-11 16:58:48 +00:00
|
|
|
|
speed = Column(Float, check_range('speed', 0.1, 15))
|
|
|
|
|
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
2018-07-19 19:25:06 +00:00
|
|
|
|
threads = Column(SmallInteger, check_range('threads', 1, 20))
|
2018-05-11 16:58:48 +00:00
|
|
|
|
address = Column(SmallInteger, check_range('address', 8, 256))
|
|
|
|
|
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
class RamModule(JoinedComponentTableMixin, Component):
|
2018-04-27 17:16:43 +00:00
|
|
|
|
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
2018-07-19 19:25:06 +00:00
|
|
|
|
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
2018-06-12 14:50:05 +00:00
|
|
|
|
interface = Column(DBEnum(RamInterface))
|
|
|
|
|
format = Column(DBEnum(RamFormat))
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
|
|
|
|
|
2018-07-02 10:52:54 +00:00
|
|
|
|
class SoundCard(JoinedComponentTableMixin, Component):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
class Display(JoinedComponentTableMixin, DisplayMixin, Component):
|
|
|
|
|
"""
|
|
|
|
|
The display of a device. This is used in all devices that have
|
|
|
|
|
displays but that it is not their main treat, like laptops,
|
|
|
|
|
mobiles, smart-watches, and so on; excluding then ComputerMonitor
|
|
|
|
|
and Television Set.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
2018-09-30 17:40:28 +00:00
|
|
|
|
|
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
class ComputerAccessory(Device):
|
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SAI(ComputerAccessory):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Keyboard(ComputerAccessory):
|
|
|
|
|
layout = Column(DBEnum(Layouts)) # If we want to do it not null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mouse(ComputerAccessory):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryCardReader(ComputerAccessory):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Networking(NetworkMixin, Device):
|
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Router(Networking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Switch(Networking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Hub(Networking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WirelessAccessPoint(Networking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Printer(Device):
|
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
|
|
|
|
wireless = Column(Boolean, nullable=False, default=False)
|
|
|
|
|
wireless.comment = """Whether it is a wireless printer."""
|
|
|
|
|
scanning = Column(Boolean, nullable=False, default=False)
|
|
|
|
|
scanning.comment = """Whether the printer has scanning capabilities."""
|
|
|
|
|
technology = Column(DBEnum(PrinterTechnology))
|
|
|
|
|
technology.comment = """Technology used to print."""
|
|
|
|
|
monochrome = Column(Boolean, nullable=False, default=True)
|
|
|
|
|
monochrome.comment = """Whether the printer is only monochrome."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LabelPrinter(Printer):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Sound(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Microphone(Sound):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Video(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VideoScaler(Video):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Videoconference(Video):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-11-12 10:59:49 +00:00
|
|
|
|
class Cooking(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mixer(Cooking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-09-30 17:40:28 +00:00
|
|
|
|
class Manufacturer(db.Model):
|
|
|
|
|
__table_args__ = {'schema': 'common'}
|
2018-10-05 12:35:51 +00:00
|
|
|
|
CSV_DELIMITER = csv.get_dialect('excel').delimiter
|
2018-09-30 17:40:28 +00:00
|
|
|
|
|
2018-10-08 15:32:45 +00:00
|
|
|
|
name = db.Column(CIText(),
|
|
|
|
|
primary_key=True,
|
|
|
|
|
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
|
|
|
|
index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin'))
|
2018-09-30 17:40:28 +00:00
|
|
|
|
url = db.Column(URL(), unique=True)
|
|
|
|
|
logo = db.Column(URL())
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2018-10-05 12:35:51 +00:00
|
|
|
|
def add_all_to_session(cls, session: db.Session):
|
2018-09-30 17:40:28 +00:00
|
|
|
|
"""Adds all manufacturers to session."""
|
2018-10-05 12:35:51 +00:00
|
|
|
|
cursor = session.connection().connection.cursor()
|
|
|
|
|
#: Dialect used to write the CSV
|
|
|
|
|
|
|
|
|
|
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
|
|
|
|
|
cursor.copy_expert(
|
|
|
|
|
'COPY common.manufacturer FROM STDIN (FORMAT csv)',
|
|
|
|
|
f
|
|
|
|
|
)
|