Merge pull request #242 from eReuse/feature/add-column-type-in-devices-list

add icons types of devices in list of devices
This commit is contained in:
cayop 2022-04-28 18:19:09 +02:00 committed by GitHub
commit a5bc694d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 235 additions and 94 deletions

View File

@ -1,20 +1,30 @@
import pathlib
import copy import copy
import pathlib
import time import time
from flask import g
from contextlib import suppress from contextlib import suppress
from fractions import Fraction from fractions import Fraction
from itertools import chain from itertools import chain
from operator import attrgetter from operator import attrgetter
from typing import Dict, List, Set from typing import Dict, List, Set
from flask_sqlalchemy import event
from boltons import urlutils from boltons import urlutils
from citext import CIText from citext import CIText
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
from flask import g
from flask_sqlalchemy import event
from more_itertools import unique_everseen from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ from sqlalchemy import BigInteger, Boolean, Column
Sequence, SmallInteger, Unicode, inspect, text from sqlalchemy import Enum as DBEnum
from sqlalchemy import (
Float,
ForeignKey,
Integer,
Sequence,
SmallInteger,
Unicode,
inspect,
text,
)
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
@ -22,19 +32,41 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType from sqlalchemy_utils import ColorType
from stdnum import imei, meid from stdnum import imei, meid
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \ from teal.db import (
check_lower, check_range, IntEnum CASCADE_DEL,
POLYMORPHIC_ID,
POLYMORPHIC_ON,
URL,
IntEnum,
ResourceNotFound,
check_lower,
check_range,
)
from teal.enums import Layouts from teal.enums import Layouts
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
from teal.resource import url_for_resource from teal.resource import url_for_resource
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.utils import hashcode
from ereuse_devicehub.resources.enums import BatteryTechnology, CameraFacing, ComputerChassis, \
DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity, TransferState
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing, listener_reset_field_updated_in_actual_time
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.device.metrics import Metrics from ereuse_devicehub.resources.device.metrics import Metrics
from ereuse_devicehub.resources.enums import (
BatteryTechnology,
CameraFacing,
ComputerChassis,
DataStorageInterface,
DisplayTech,
PrinterTechnology,
RamFormat,
RamInterface,
Severity,
TransferState,
)
from ereuse_devicehub.resources.models import (
STR_SM_SIZE,
Thing,
listener_reset_field_updated_in_actual_time,
)
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.utils import hashcode
def create_code(context): def create_code(context):
@ -58,17 +90,21 @@ class Device(Thing):
Devices can contain ``Components``, which are just a type of device Devices can contain ``Components``, which are just a type of device
(it is a recursive relationship). (it is a recursive relationship).
""" """
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """The identifier of the device for this database. Used only id.comment = """The identifier of the device for this database. Used only
internally for software; users should not use this. internally for software; users should not use this.
""" """
type = Column(Unicode(STR_SM_SIZE), nullable=False) type = Column(Unicode(STR_SM_SIZE), nullable=False)
hid = Column(Unicode(), check_lower('hid'), unique=False) hid = Column(Unicode(), check_lower('hid'), unique=False)
hid.comment = """The Hardware ID (HID) is the ID traceability hid.comment = (
"""The Hardware ID (HID) is the ID traceability
systems use to ID a device globally. This field is auto-generated systems use to ID a device globally. This field is auto-generated
from Devicehub using literal identifiers from the device, from Devicehub using literal identifiers from the device,
so it can re-generated *offline*. so it can re-generated *offline*.
""" + HID_CONVERSION_DOC """
+ HID_CONVERSION_DOC
)
model = Column(Unicode(), check_lower('model')) model = Column(Unicode(), check_lower('model'))
model.comment = """The model of the device in lower case. model.comment = """The model of the device in lower case.
@ -118,14 +154,18 @@ class Device(Thing):
image = db.Column(db.URL) image = db.Column(db.URL)
image.comment = "An image of the device." image.comment = "An image of the device."
owner_id = db.Column(UUID(as_uuid=True), owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id), db.ForeignKey(User.id),
nullable=False, nullable=False,
default=lambda: g.user.id) default=lambda: g.user.id,
)
owner = db.relationship(User, primaryjoin=owner_id == User.id) owner = db.relationship(User, primaryjoin=owner_id == User.id)
allocated = db.Column(Boolean, default=False) allocated = db.Column(Boolean, default=False)
allocated.comment = "device is allocated or not." allocated.comment = "device is allocated or not."
devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code) devicehub_id = db.Column(
db.CIText(), nullable=True, unique=True, default=create_code
)
devicehub_id.comment = "device have a unique code." devicehub_id.comment = "device have a unique code."
active = db.Column(Boolean, default=True) active = db.Column(Boolean, default=True)
@ -152,12 +192,12 @@ class Device(Thing):
'image', 'image',
'allocated', 'allocated',
'devicehub_id', 'devicehub_id',
'active' 'active',
} }
__table_args__ = ( __table_args__ = (
db.Index('device_id', id, postgresql_using='hash'), db.Index('device_id', id, postgresql_using='hash'),
db.Index('type_index', type, postgresql_using='hash') db.Index('type_index', type, postgresql_using='hash'),
) )
def __init__(self, **kw) -> None: def __init__(self, **kw) -> None:
@ -187,7 +227,9 @@ class Device(Thing):
for ac in actions_one: for ac in actions_one:
ac.real_created = ac.created ac.real_created = ac.created
return sorted(chain(actions_multiple, actions_one), key=lambda x: x.real_created) return sorted(
chain(actions_multiple, actions_one), key=lambda x: x.real_created
)
@property @property
def problems(self): def problems(self):
@ -196,8 +238,9 @@ class Device(Thing):
There can be up to 3 actions: current Snapshot, There can be up to 3 actions: current Snapshot,
current Physical action, current Trading action. current Physical action, current Trading action.
""" """
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.action.models import Snapshot from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.device import states
actions = set() actions = set()
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
actions.add(self.last_action_of(Snapshot)) actions.add(self.last_action_of(Snapshot))
@ -217,11 +260,13 @@ class Device(Thing):
""" """
# todo ensure to remove materialized values when start using them # todo ensure to remove materialized values when start using them
# todo or self.__table__.columns if inspect fails # todo or self.__table__.columns if inspect fails
return {c.key: getattr(self, c.key, None) return {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty) if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None) and not getattr(c, 'foreign_keys', None)
and c.key not in self._NON_PHYSICAL_PROPS} and c.key not in self._NON_PHYSICAL_PROPS
}
@property @property
def public_properties(self) -> Dict[str, object or None]: def public_properties(self) -> Dict[str, object or None]:
@ -234,11 +279,13 @@ class Device(Thing):
""" """
non_public = ['amount', 'transfer_state', 'receiver_id'] non_public = ['amount', 'transfer_state', 'receiver_id']
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
return {c.key: getattr(self, c.key, None) return {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty) if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None) and not getattr(c, 'foreign_keys', None)
and c.key not in hide_properties} and c.key not in hide_properties
}
@property @property
def public_actions(self) -> List[object]: def public_actions(self) -> List[object]:
@ -260,6 +307,7 @@ class Device(Thing):
"""The last Rate of the device.""" """The last Rate of the device."""
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
from ereuse_devicehub.resources.action.models import Rate from ereuse_devicehub.resources.action.models import Rate
return self.last_action_of(Rate) return self.last_action_of(Rate)
@property @property
@ -268,12 +316,14 @@ class Device(Thing):
ever been set.""" ever been set."""
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
from ereuse_devicehub.resources.action.models import Price from ereuse_devicehub.resources.action.models import Price
return self.last_action_of(Price) return self.last_action_of(Price)
@property @property
def last_action_trading(self): def last_action_trading(self):
"""which is the last action trading""" """which is the last action trading"""
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
return self.last_action_of(*states.Trading.actions()) return self.last_action_of(*states.Trading.actions())
@ -287,6 +337,7 @@ class Device(Thing):
- Management - Management
""" """
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
return self.last_action_of(*states.Status.actions()) return self.last_action_of(*states.Status.actions())
@ -300,6 +351,7 @@ class Device(Thing):
- Management - Management
""" """
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
status_actions = [ac.t for ac in states.Status.actions()] status_actions = [ac.t for ac in states.Status.actions()]
history = [] history = []
for ac in self.actions: for ac in self.actions:
@ -329,13 +381,15 @@ class Device(Thing):
if not hasattr(lot, 'trade'): if not hasattr(lot, 'trade'):
return return
Status = {0: 'Trade', Status = {
0: 'Trade',
1: 'Confirm', 1: 'Confirm',
2: 'NeedConfirmation', 2: 'NeedConfirmation',
3: 'TradeConfirmed', 3: 'TradeConfirmed',
4: 'Revoke', 4: 'Revoke',
5: 'NeedConfirmRevoke', 5: 'NeedConfirmRevoke',
6: 'RevokeConfirmed'} 6: 'RevokeConfirmed',
}
trade = lot.trade trade = lot.trade
user_from = trade.user_from user_from = trade.user_from
@ -408,6 +462,7 @@ class Device(Thing):
"""If the actual trading state is an revoke action, this property show """If the actual trading state is an revoke action, this property show
the id of that revoke""" the id of that revoke"""
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Trading.actions()) action = self.last_action_of(*states.Trading.actions())
if action.type == 'Revoke': if action.type == 'Revoke':
@ -417,6 +472,7 @@ class Device(Thing):
def physical(self): def physical(self):
"""The actual physical state, None otherwise.""" """The actual physical state, None otherwise."""
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Physical.actions()) action = self.last_action_of(*states.Physical.actions())
return states.Physical(action.__class__) return states.Physical(action.__class__)
@ -425,6 +481,7 @@ class Device(Thing):
def traking(self): def traking(self):
"""The actual traking state, None otherwise.""" """The actual traking state, None otherwise."""
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Traking.actions()) action = self.last_action_of(*states.Traking.actions())
return states.Traking(action.__class__) return states.Traking(action.__class__)
@ -433,6 +490,7 @@ class Device(Thing):
def usage(self): def usage(self):
"""The actual usage state, None otherwise.""" """The actual usage state, None otherwise."""
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Usage.actions()) action = self.last_action_of(*states.Usage.actions())
return states.Usage(action.__class__) return states.Usage(action.__class__)
@ -470,8 +528,11 @@ class Device(Thing):
test has been executed. test has been executed.
""" """
from ereuse_devicehub.resources.action.models import Test from ereuse_devicehub.resources.action.models import Test
current_tests = unique_everseen((e for e in reversed(self.actions) if isinstance(e, Test)),
key=attrgetter('type')) # last test of each type current_tests = unique_everseen(
(e for e in reversed(self.actions) if isinstance(e, Test)),
key=attrgetter('type'),
) # last test of each type
return self._warning_actions(current_tests) return self._warning_actions(current_tests)
@property @property
@ -496,7 +557,9 @@ class Device(Thing):
def set_hid(self): def set_hid(self):
with suppress(TypeError): with suppress(TypeError):
self.hid = Naming.hid(self.type, self.manufacturer, self.model, self.serial_number) self.hid = Naming.hid(
self.type, self.manufacturer, self.model, self.serial_number
)
def last_action_of(self, *types): def last_action_of(self, *types):
"""Gets the last action of the given types. """Gets the last action of the given types.
@ -509,7 +572,9 @@ class Device(Thing):
actions.sort(key=lambda x: x.created) actions.sort(key=lambda x: x.created)
return next(e for e in reversed(actions) if isinstance(e, types)) return next(e for e in reversed(actions) if isinstance(e, types))
except StopIteration: except StopIteration:
raise LookupError('{!r} does not contain actions of types {}.'.format(self, types)) raise LookupError(
'{!r} does not contain actions of types {}.'.format(self, types)
)
def which_user_put_this_device_in_trace(self): def which_user_put_this_device_in_trace(self):
"""which is the user than put this device in this trade""" """which is the user than put this device in this trade"""
@ -546,6 +611,32 @@ class Device(Thing):
metrics = Metrics(device=self) metrics = Metrics(device=self)
return metrics.get_metrics() return metrics.get_metrics()
def get_type_logo(self):
# This is used for see one logo of type of device in the frontend
types = {
"Desktop": "bi bi-file-post-fill",
"Laptop": "bi bi-laptop",
"Server": "bi bi-server",
"Processor": "bi bi-cpu",
"RamModule": "bi bi-list",
"Motherboard": "bi bi-cpu-fill",
"NetworkAdapter": "bi bi-hdd-network",
"GraphicCard": "bi bi-brush",
"SoundCard": "bi bi-volume-up-fill",
"Monitor": "bi bi-display",
"Display": "bi bi-display",
"ComputerMonitor": "bi bi-display",
"TelevisionSet": "bi bi-easel",
"TV": "bi bi-easel",
"Projector": "bi bi-camera-video",
"Tablet": "bi bi-tablet-landscape",
"Smartphone": "bi bi-phone",
"Cellphone": "bi bi-telephone",
"HardDrive": "bi bi-hdd-stack",
"SolidStateDrive": "bi bi-hdd",
}
return types.get(self.type, '')
def __lt__(self, other): def __lt__(self, other):
return self.id < other.id return self.id < other.id
@ -571,19 +662,24 @@ class Device(Thing):
class DisplayMixin: class DisplayMixin:
"""Base class for the Display Component and the Monitor Device.""" """Base class for the Display Component and the Monitor Device."""
size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True)
size = Column(
Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True
)
size.comment = """The size of the monitor in inches.""" size.comment = """The size of the monitor in inches."""
technology = Column(DBEnum(DisplayTech)) technology = Column(DBEnum(DisplayTech))
technology.comment = """The technology the monitor uses to display technology.comment = """The technology the monitor uses to display
the image. the image.
""" """
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000), resolution_width = Column(
nullable=True) SmallInteger, check_range('resolution_width', 10, 20000), nullable=True
)
resolution_width.comment = """The maximum horizontal resolution the resolution_width.comment = """The maximum horizontal resolution the
monitor can natively support in pixels. monitor can natively support in pixels.
""" """
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000), resolution_height = Column(
nullable=True) SmallInteger, check_range('resolution_height', 10, 20000), nullable=True
)
resolution_height.comment = """The maximum vertical resolution the resolution_height.comment = """The maximum vertical resolution the
monitor can natively support in pixels. monitor can natively support in pixels.
""" """
@ -622,8 +718,12 @@ class DisplayMixin:
def __str__(self) -> str: def __str__(self) -> str:
if self.size: if self.size:
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self) return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(self) self
)
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(
self
)
def __format__(self, format_spec: str) -> str: def __format__(self, format_spec: str) -> str:
v = '' v = ''
@ -645,6 +745,7 @@ class Computer(Device):
Computer is broadly extended by ``Desktop``, ``Laptop``, and Computer is broadly extended by ``Desktop``, ``Laptop``, and
``Server``. The property ``chassis`` defines it more granularly. ``Server``. The property ``chassis`` defines it more granularly.
""" """
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
chassis = Column(DBEnum(ComputerChassis), nullable=True) chassis = Column(DBEnum(ComputerChassis), nullable=True)
chassis.comment = """The physical form of the computer. chassis.comment = """The physical form of the computer.
@ -652,16 +753,18 @@ class Computer(Device):
It is a subset of the Linux definition of DMI / DMI decode. It is a subset of the Linux definition of DMI / DMI decode.
""" """
amount = Column(Integer, check_range('amount', min=0, max=100), default=0) amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
owner_id = db.Column(UUID(as_uuid=True), owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id), db.ForeignKey(User.id),
nullable=False, nullable=False,
default=lambda: g.user.id) default=lambda: g.user.id,
)
# author = db.relationship(User, primaryjoin=owner_id == User.id) # author = db.relationship(User, primaryjoin=owner_id == User.id)
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False) transfer_state = db.Column(
IntEnum(TransferState), default=TransferState.Initial, nullable=False
)
transfer_state.comment = TransferState.__doc__ transfer_state.comment = TransferState.__doc__
receiver_id = db.Column(UUID(as_uuid=True), receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
db.ForeignKey(User.id),
nullable=True)
receiver = db.relationship(User, primaryjoin=receiver_id == User.id) receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
@ -684,22 +787,30 @@ class Computer(Device):
@property @property
def ram_size(self) -> int: def ram_size(self) -> int:
"""The total of RAM memory the computer has.""" """The total of RAM memory the computer has."""
return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule)) return sum(
ram.size or 0 for ram in self.components if isinstance(ram, RamModule)
)
@property @property
def data_storage_size(self) -> int: def data_storage_size(self) -> int:
"""The total of data storage the computer has.""" """The total of data storage the computer has."""
return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)) return sum(
ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)
)
@property @property
def processor_model(self) -> str: def processor_model(self) -> str:
"""The model of one of the processors of the computer.""" """The model of one of the processors of the computer."""
return next((p.model for p in self.components if isinstance(p, Processor)), None) return next(
(p.model for p in self.components if isinstance(p, Processor)), None
)
@property @property
def graphic_card_model(self) -> str: def graphic_card_model(self) -> str:
"""The model of one of the graphic cards of the computer.""" """The model of one of the graphic cards of the computer."""
return next((p.model for p in self.components if isinstance(p, GraphicCard)), None) return next(
(p.model for p in self.components if isinstance(p, GraphicCard)), None
)
@property @property
def network_speeds(self) -> List[int]: def network_speeds(self) -> List[int]:
@ -724,16 +835,18 @@ class Computer(Device):
it is not None. it is not None.
""" """
return set( return set(
privacy for privacy in privacy
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)) for privacy in (
hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)
)
if privacy if privacy
) )
@property @property
def external_document_erasure(self): def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure. """Returns the external ``DataStorage`` proof of erasure."""
"""
from ereuse_devicehub.resources.action.models import DataWipe from ereuse_devicehub.resources.action.models import DataWipe
urls = set() urls = set()
try: try:
ev = self.last_action_of(DataWipe) ev = self.last_action_of(DataWipe)
@ -756,8 +869,11 @@ class Computer(Device):
if not self.hid: if not self.hid:
return return
components = self.components if components_snap is None else components_snap components = self.components if components_snap is None else components_snap
macs_network = [c.serial_number for c in components macs_network = [
if c.type == 'NetworkAdapter' and c.serial_number is not None] c.serial_number
for c in components
if c.type == 'NetworkAdapter' and c.serial_number is not None
]
macs_network.sort() macs_network.sort()
mac = macs_network[0] if macs_network else '' mac = macs_network[0] if macs_network else ''
if not mac or mac in self.hid: if not mac or mac in self.hid:
@ -823,9 +939,13 @@ class Mobile(Device):
""" """
ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000)) ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
ram_size.comment = """The total of RAM of the device in MB.""" ram_size.comment = """The total of RAM of the device in MB."""
data_storage_size = db.Column(db.Integer, check_range('data_storage_size', 0, 10 ** 8)) data_storage_size = db.Column(
db.Integer, check_range('data_storage_size', 0, 10**8)
)
data_storage_size.comment = """The total of data storage of the device in MB""" data_storage_size.comment = """The total of data storage of the device in MB"""
display_size = db.Column(db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)) display_size = db.Column(
db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)
)
display_size.comment = """The total size of the device screen""" display_size.comment = """The total size of the device screen"""
@validates('imei') @validates('imei')
@ -855,21 +975,24 @@ class Cellphone(Mobile):
class Component(Device): class Component(Device):
"""A device that can be inside another device.""" """A device that can be inside another device."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
parent_id = Column(BigInteger, ForeignKey(Computer.id)) parent_id = Column(BigInteger, ForeignKey(Computer.id))
parent = relationship(Computer, parent = relationship(
backref=backref('components', Computer,
backref=backref(
'components',
lazy=True, lazy=True,
cascade=CASCADE_DEL, cascade=CASCADE_DEL,
order_by=lambda: Component.id, order_by=lambda: Component.id,
collection_class=OrderedSet), collection_class=OrderedSet,
primaryjoin=parent_id == Computer.id) ),
primaryjoin=parent_id == Computer.id,
__table_args__ = (
db.Index('parent_index', parent_id, postgresql_using='hash'),
) )
__table_args__ = (db.Index('parent_index', parent_id, postgresql_using='hash'),)
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component': def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
"""Gets a component that: """Gets a component that:
@ -881,11 +1004,16 @@ class Component(Device):
when looking for similar ones. when looking for similar ones.
""" """
assert self.hid is None, 'Don\'t use this method with a component that has HID' assert self.hid is None, 'Don\'t use this method with a component that has HID'
component = self.__class__.query \ component = (
.filter_by(parent=parent, hid=None, owner_id=self.owner_id, self.__class__.query.filter_by(
**self.physical_properties) \ parent=parent,
.filter(~Component.id.in_(blacklist)) \ hid=None,
owner_id=self.owner_id,
**self.physical_properties,
)
.filter(~Component.id.in_(blacklist))
.first() .first()
)
if not component: if not component:
raise ResourceNotFound(self.type) raise ResourceNotFound(self.type)
return component return component
@ -908,6 +1036,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
class DataStorage(JoinedComponentTableMixin, Component): class DataStorage(JoinedComponentTableMixin, Component):
"""A device that stores information.""" """A device that stores information."""
size = Column(Integer, check_range('size', min=1, max=10**8)) size = Column(Integer, check_range('size', min=1, max=10**8))
size.comment = """The size of the data-storage in MB.""" size.comment = """The size of the data-storage in MB."""
interface = Column(DBEnum(DataStorageInterface)) interface = Column(DBEnum(DataStorageInterface))
@ -919,6 +1048,7 @@ class DataStorage(JoinedComponentTableMixin, Component):
This is, the last erasure performed to the data storage. This is, the last erasure performed to the data storage.
""" """
from ereuse_devicehub.resources.action.models import EraseBasic from ereuse_devicehub.resources.action.models import EraseBasic
try: try:
ev = self.last_action_of(EraseBasic) ev = self.last_action_of(EraseBasic)
except LookupError: except LookupError:
@ -933,9 +1063,9 @@ class DataStorage(JoinedComponentTableMixin, Component):
@property @property
def external_document_erasure(self): def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure. """Returns the external ``DataStorage`` proof of erasure."""
"""
from ereuse_devicehub.resources.action.models import DataWipe from ereuse_devicehub.resources.action.models import DataWipe
try: try:
ev = self.last_action_of(DataWipe) ev = self.last_action_of(DataWipe)
return ev.document.url.to_text() return ev.document.url.to_text()
@ -985,6 +1115,7 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
class Processor(JoinedComponentTableMixin, Component): class Processor(JoinedComponentTableMixin, Component):
"""The CPU.""" """The CPU."""
speed = Column(Float, check_range('speed', 0.1, 15)) speed = Column(Float, check_range('speed', 0.1, 15))
speed.comment = """The regular CPU speed.""" speed.comment = """The regular CPU speed."""
cores = Column(SmallInteger, check_range('cores', 1, 10)) cores = Column(SmallInteger, check_range('cores', 1, 10))
@ -999,6 +1130,7 @@ class Processor(JoinedComponentTableMixin, Component):
class RamModule(JoinedComponentTableMixin, Component): class RamModule(JoinedComponentTableMixin, Component):
"""A stick of RAM.""" """A stick of RAM."""
size = Column(SmallInteger, check_range('size', min=128, max=17000)) size = Column(SmallInteger, check_range('size', min=128, max=17000))
size.comment = """The capacity of the RAM stick.""" size.comment = """The capacity of the RAM stick."""
speed = Column(SmallInteger, check_range('speed', min=100, max=10000)) speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
@ -1016,6 +1148,7 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component):
mobiles, smart-watches, and so on; excluding ``ComputerMonitor`` mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
and ``TelevisionSet``. and ``TelevisionSet``.
""" """
pass pass
@ -1033,12 +1166,14 @@ class Battery(JoinedComponentTableMixin, Component):
def capacity(self) -> float: def capacity(self) -> float:
"""The quantity of""" """The quantity of"""
from ereuse_devicehub.resources.action.models import MeasureBattery from ereuse_devicehub.resources.action.models import MeasureBattery
real_size = self.last_action_of(MeasureBattery).size real_size = self.last_action_of(MeasureBattery).size
return real_size / self.size if real_size and self.size else None return real_size / self.size if real_size and self.size else None
class Camera(Component): class Camera(Component):
"""The camera of a device.""" """The camera of a device."""
focal_length = db.Column(db.SmallInteger) focal_length = db.Column(db.SmallInteger)
video_height = db.Column(db.SmallInteger) video_height = db.Column(db.SmallInteger)
video_width = db.Column(db.Integer) video_width = db.Column(db.Integer)
@ -1051,6 +1186,7 @@ class Camera(Component):
class ComputerAccessory(Device): class ComputerAccessory(Device):
"""Computer peripherals and similar accessories.""" """Computer peripherals and similar accessories."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
pass pass
@ -1073,6 +1209,7 @@ class MemoryCardReader(ComputerAccessory):
class Networking(NetworkMixin, Device): class Networking(NetworkMixin, Device):
"""Routers, switches, hubs...""" """Routers, switches, hubs..."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
@ -1118,6 +1255,7 @@ class Microphone(Sound):
class Video(Device): class Video(Device):
"""Devices related to video treatment.""" """Devices related to video treatment."""
pass pass
@ -1131,6 +1269,7 @@ class Videoconference(Video):
class Cooking(Device): class Cooking(Device):
"""Cooking devices.""" """Cooking devices."""
pass pass
@ -1182,6 +1321,7 @@ class Manufacturer(db.Model):
Ideally users should use the names from this list when submitting Ideally users should use the names from this list when submitting
devices. devices.
""" """
name = db.Column(CIText(), primary_key=True) name = db.Column(CIText(), primary_key=True)
name.comment = """The normalized name of the manufacturer.""" name.comment = """The normalized name of the manufacturer."""
url = db.Column(URL(), unique=True) url = db.Column(URL(), unique=True)
@ -1192,7 +1332,7 @@ class Manufacturer(db.Model):
__table_args__ = ( __table_args__ = (
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/ # from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'), db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
{'schema': 'common'} {'schema': 'common'},
) )
@classmethod @classmethod
@ -1202,10 +1342,7 @@ class Manufacturer(db.Model):
#: Dialect used to write the CSV #: Dialect used to write the CSV
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f: with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
cursor.copy_expert( cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
'COPY common.manufacturer FROM STDIN (FORMAT csv)',
f
)
listener_reset_field_updated_in_actual_time(Device) listener_reset_field_updated_in_actual_time(Device)
@ -1217,6 +1354,7 @@ def create_code_tag(mapper, connection, device):
this tag is the same of devicehub_id. this tag is the same of devicehub_id.
""" """
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
if isinstance(device, Computer): if isinstance(device, Computer):
tag = Tag(device_id=device.id, id=device.devicehub_id) tag = Tag(device_id=device.id, id=device.devicehub_id)
db.session.add(tag) db.session.add(tag)

View File

@ -336,6 +336,9 @@
/> />
</td> </td>
<td> <td>
{% if dev.get_type_logo() %}
<i class="{{ dev.get_type_logo() }}" title="{{ dev.type }}"></i>
{% endif %}
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}"> <a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.verbose_name }} {{ dev.verbose_name }}
</a> </a>