2021-01-13 17:11:41 +00:00
|
|
|
|
import copy
|
2022-11-16 10:51:31 +00:00
|
|
|
|
import hashlib
|
2023-01-11 16:17:50 +00:00
|
|
|
|
import json
|
|
|
|
|
import os
|
2022-04-28 15:46:12 +00:00
|
|
|
|
import pathlib
|
2023-01-11 16:17:50 +00:00
|
|
|
|
import uuid
|
2018-04-27 17:16:43 +00:00
|
|
|
|
from contextlib import suppress
|
2019-05-03 12:31:49 +00:00
|
|
|
|
from fractions import Fraction
|
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
|
2022-12-20 09:17:42 +00:00
|
|
|
|
from ereuse_utils.naming import HID_CONVERSION_DOC
|
|
|
|
|
from flask import current_app as app
|
2022-05-05 10:00:02 +00:00
|
|
|
|
from flask import g, request
|
2018-11-09 10:22:13 +00:00
|
|
|
|
from more_itertools import unique_everseen
|
2022-04-28 15:46:12 +00:00
|
|
|
|
from sqlalchemy import BigInteger, Boolean, Column
|
|
|
|
|
from sqlalchemy import Enum as DBEnum
|
|
|
|
|
from sqlalchemy import (
|
|
|
|
|
Float,
|
|
|
|
|
ForeignKey,
|
|
|
|
|
Integer,
|
|
|
|
|
Sequence,
|
|
|
|
|
SmallInteger,
|
|
|
|
|
Unicode,
|
|
|
|
|
inspect,
|
|
|
|
|
text,
|
|
|
|
|
)
|
2020-08-17 14:45:18 +00:00
|
|
|
|
from sqlalchemy.dialects.postgresql import UUID
|
2018-04-10 15:06:39 +00:00
|
|
|
|
from sqlalchemy.ext.declarative import declared_attr
|
2019-05-03 12:31:49 +00:00
|
|
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
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
|
2022-04-28 15:46:12 +00:00
|
|
|
|
from teal.db import (
|
|
|
|
|
CASCADE_DEL,
|
|
|
|
|
POLYMORPHIC_ID,
|
|
|
|
|
POLYMORPHIC_ON,
|
|
|
|
|
URL,
|
|
|
|
|
IntEnum,
|
|
|
|
|
ResourceNotFound,
|
|
|
|
|
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
|
2021-10-14 10:56:33 +00:00
|
|
|
|
from ereuse_devicehub.resources.device.metrics import Metrics
|
2022-04-28 15:46:12 +00:00
|
|
|
|
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
|
2018-07-02 10:52:54 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2021-03-04 12:27:29 +00:00
|
|
|
|
def create_code(context):
|
2021-10-22 17:26:27 +00:00
|
|
|
|
_id = Device.query.order_by(Device.id.desc()).first() or 3
|
|
|
|
|
if not _id == 3:
|
2021-03-04 12:27:29 +00:00
|
|
|
|
_id = _id.id + 1
|
2021-03-08 12:06:07 +00:00
|
|
|
|
return hashcode.encode(_id)
|
2021-03-04 12:27:29 +00:00
|
|
|
|
|
2020-10-30 20:08:55 +00:00
|
|
|
|
|
2022-10-05 07:17:27 +00:00
|
|
|
|
def create_phid(context, count=1):
|
|
|
|
|
phid = str(Placeholder.query.filter(Placeholder.owner == g.user).count() + count)
|
|
|
|
|
if (
|
|
|
|
|
Placeholder.query.filter(Placeholder.owner == g.user)
|
|
|
|
|
.filter(Placeholder.phid == phid)
|
|
|
|
|
.count()
|
|
|
|
|
):
|
|
|
|
|
return create_phid(context, count=count + 1)
|
|
|
|
|
return phid
|
2022-07-04 09:26:24 +00:00
|
|
|
|
|
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
class Device(Thing):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Base class for any type of physical object that can be identified.
|
|
|
|
|
|
|
|
|
|
Device partly extends `Schema's IndividualProduct <https
|
|
|
|
|
://schema.org/IndividualProduct>`_, adapting it to our
|
|
|
|
|
use case.
|
|
|
|
|
|
|
|
|
|
A device requires an identification method, ideally a serial number,
|
|
|
|
|
although it can be identified only with tags too. More ideally
|
|
|
|
|
both methods are used.
|
|
|
|
|
|
|
|
|
|
Devices can contain ``Components``, which are just a type of device
|
|
|
|
|
(it is a recursive relationship).
|
2018-06-24 14:57:49 +00:00
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
id.comment = """The identifier of the device for this database. Used only
|
|
|
|
|
internally for software; users should not use this.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2019-02-07 12:47:42 +00:00
|
|
|
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
2020-03-25 15:47:57 +00:00
|
|
|
|
hid = Column(Unicode(), check_lower('hid'), unique=False)
|
2022-04-28 15:46:12 +00:00
|
|
|
|
hid.comment = (
|
|
|
|
|
"""The Hardware ID (HID) is the ID traceability
|
2019-06-19 11:35:26 +00:00
|
|
|
|
systems use to ID a device globally. This field is auto-generated
|
|
|
|
|
from Devicehub using literal identifiers from the device,
|
2020-04-01 17:11:14 +00:00
|
|
|
|
so it can re-generated *offline*.
|
2022-04-28 15:46:12 +00:00
|
|
|
|
"""
|
|
|
|
|
+ HID_CONVERSION_DOC
|
|
|
|
|
)
|
2020-03-25 15:47:57 +00:00
|
|
|
|
model = Column(Unicode(), check_lower('model'))
|
2019-05-03 12:31:49 +00:00
|
|
|
|
model.comment = """The model of the device in lower case.
|
2020-04-01 17:11:14 +00:00
|
|
|
|
|
2019-05-03 12:31:49 +00:00
|
|
|
|
The model is the unambiguous, as technical as possible, denomination
|
2020-04-01 17:11:14 +00:00
|
|
|
|
for the product. This field, among others, is used to identify
|
2019-05-03 12:31:49 +00:00
|
|
|
|
the product.
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""
|
2018-09-30 10:29:33 +00:00
|
|
|
|
manufacturer = Column(Unicode(), check_lower('manufacturer'))
|
2019-05-03 12:31:49 +00:00
|
|
|
|
manufacturer.comment = """The normalized name of the manufacturer,
|
2019-02-03 16:12:53 +00:00
|
|
|
|
in lower case.
|
2020-04-01 17:11:14 +00:00
|
|
|
|
|
2019-02-03 16:12:53 +00:00
|
|
|
|
Although as of now Devicehub does not enforce normalization,
|
|
|
|
|
users can choose a list of normalized manufacturer names
|
|
|
|
|
from the own ``/manufacturers`` REST endpoint.
|
|
|
|
|
"""
|
2018-09-30 10:29:33 +00:00
|
|
|
|
serial_number = Column(Unicode(), check_lower('serial_number'))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
serial_number.comment = """The serial number of the device in lower case."""
|
2022-09-13 16:15:50 +00:00
|
|
|
|
part_number = Column(Unicode(), check_lower('part_number'))
|
|
|
|
|
part_number.comment = """The part number of the device in lower case."""
|
2019-05-03 12:31:49 +00:00
|
|
|
|
brand = db.Column(CIText())
|
|
|
|
|
brand.comment = """A naming for consumers. This field can represent
|
2020-04-01 17:11:14 +00:00
|
|
|
|
several models, so it can be ambiguous, and it is not used to
|
2019-05-03 12:31:49 +00:00
|
|
|
|
identify the product.
|
|
|
|
|
"""
|
|
|
|
|
generation = db.Column(db.SmallInteger, check_range('generation', 0))
|
|
|
|
|
generation.comment = """The generation of the device."""
|
2019-07-01 09:30:48 +00:00
|
|
|
|
version = db.Column(db.CIText())
|
2019-07-07 19:36:09 +00:00
|
|
|
|
version.comment = """The version code of this device, like v1 or A001."""
|
|
|
|
|
weight = Column(Float(decimal_return_scale=4), check_range('weight', 0.1, 5))
|
|
|
|
|
weight.comment = """The weight of the device in Kg."""
|
|
|
|
|
width = Column(Float(decimal_return_scale=4), check_range('width', 0.1, 5))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
width.comment = """The width of the device in meters."""
|
2019-07-07 19:36:09 +00:00
|
|
|
|
height = Column(Float(decimal_return_scale=4), check_range('height', 0.1, 5))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
height.comment = """The height of the device in meters."""
|
2019-07-07 19:36:09 +00:00
|
|
|
|
depth = Column(Float(decimal_return_scale=4), check_range('depth', 0.1, 5))
|
2019-06-19 11:35:26 +00:00
|
|
|
|
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."""
|
2019-05-03 12:31:49 +00:00
|
|
|
|
production_date = Column(db.DateTime)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
production_date.comment = """The date of production of the device.
|
2019-12-09 16:24:24 +00:00
|
|
|
|
This is timezone naive, as Workbench cannot report this data with timezone information.
|
2019-05-03 12:31:49 +00:00
|
|
|
|
"""
|
2019-07-01 09:30:48 +00:00
|
|
|
|
variant = Column(db.CIText())
|
2019-05-03 12:31:49 +00:00
|
|
|
|
variant.comment = """A variant or sub-model of the device."""
|
2019-07-01 09:30:48 +00:00
|
|
|
|
sku = db.Column(db.CIText())
|
2020-04-01 17:11:14 +00:00
|
|
|
|
sku.comment = """The Stock Keeping Unit (SKU), i.e. a
|
2019-06-29 14:26:14 +00:00
|
|
|
|
merchant-specific identifier for a product or service.
|
|
|
|
|
"""
|
2019-07-07 19:36:09 +00:00
|
|
|
|
image = db.Column(db.URL)
|
|
|
|
|
image.comment = "An image of the device."
|
2018-10-23 13:37:37 +00:00
|
|
|
|
|
2022-04-28 15:46:12 +00:00
|
|
|
|
owner_id = db.Column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey(User.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.id,
|
|
|
|
|
)
|
2020-10-30 20:08:55 +00:00
|
|
|
|
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
2020-11-22 17:47:58 +00:00
|
|
|
|
allocated = db.Column(Boolean, default=False)
|
2020-12-01 14:55:21 +00:00
|
|
|
|
allocated.comment = "device is allocated or not."
|
2022-04-28 15:46:12 +00:00
|
|
|
|
devicehub_id = db.Column(
|
|
|
|
|
db.CIText(), nullable=True, unique=True, default=create_code
|
|
|
|
|
)
|
2021-03-08 18:09:12 +00:00
|
|
|
|
devicehub_id.comment = "device have a unique code."
|
2022-09-07 12:04:09 +00:00
|
|
|
|
dhid_bk = db.Column(db.CIText(), nullable=True, unique=False)
|
|
|
|
|
phid_bk = db.Column(db.CIText(), nullable=True, unique=False)
|
2021-10-05 10:17:07 +00:00
|
|
|
|
active = db.Column(Boolean, default=True)
|
2022-11-16 10:51:31 +00:00
|
|
|
|
family = db.Column(db.CIText())
|
2022-12-13 13:29:52 +00:00
|
|
|
|
chid = db.Column(db.CIText())
|
2020-11-22 17:47:58 +00:00
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
_NON_PHYSICAL_PROPS = {
|
|
|
|
|
'id',
|
|
|
|
|
'type',
|
|
|
|
|
'created',
|
|
|
|
|
'updated',
|
|
|
|
|
'parent_id',
|
2020-11-03 19:26:03 +00:00
|
|
|
|
'owner_id',
|
2018-10-23 13:37:37 +00:00
|
|
|
|
'hid',
|
|
|
|
|
'production_date',
|
2018-11-17 19:21:11 +00:00
|
|
|
|
'color', # these are only user-input thus volatile
|
|
|
|
|
'width',
|
|
|
|
|
'height',
|
|
|
|
|
'depth',
|
2019-05-03 12:31:49 +00:00
|
|
|
|
'weight',
|
|
|
|
|
'brand',
|
|
|
|
|
'generation',
|
|
|
|
|
'production_date',
|
2019-06-29 14:26:14 +00:00
|
|
|
|
'variant',
|
|
|
|
|
'version',
|
2022-11-16 10:51:31 +00:00
|
|
|
|
'family',
|
2019-07-07 19:36:09 +00:00
|
|
|
|
'sku',
|
2020-11-22 17:47:58 +00:00
|
|
|
|
'image',
|
2021-03-03 19:05:48 +00:00
|
|
|
|
'allocated',
|
2021-10-05 10:17:07 +00:00
|
|
|
|
'devicehub_id',
|
2022-06-23 15:06:40 +00:00
|
|
|
|
'system_uuid',
|
2022-04-28 15:46:12 +00:00
|
|
|
|
'active',
|
2022-09-07 14:23:31 +00:00
|
|
|
|
'phid_bk',
|
|
|
|
|
'dhid_bk',
|
2022-12-13 13:29:52 +00:00
|
|
|
|
'chid',
|
2022-12-19 19:03:34 +00:00
|
|
|
|
'user_trusts',
|
|
|
|
|
'chassis',
|
|
|
|
|
'transfer_state',
|
|
|
|
|
'receiver_id',
|
2018-10-23 13:37:37 +00:00
|
|
|
|
}
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
__table_args__ = (
|
|
|
|
|
db.Index('device_id', id, postgresql_using='hash'),
|
2022-04-28 15:46:12 +00:00
|
|
|
|
db.Index('type_index', type, postgresql_using='hash'),
|
2019-02-07 12:47:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
def __init__(self, **kw) -> None:
|
|
|
|
|
super().__init__(**kw)
|
2020-11-10 19:45:03 +00:00
|
|
|
|
self.set_hid()
|
2018-11-09 10:22:13 +00:00
|
|
|
|
|
2022-02-02 12:27:53 +00:00
|
|
|
|
@property
|
|
|
|
|
def reverse_actions(self) -> list:
|
|
|
|
|
return reversed(self.actions)
|
|
|
|
|
|
2022-07-29 15:02:27 +00:00
|
|
|
|
@property
|
|
|
|
|
def manual_actions(self) -> list:
|
|
|
|
|
mactions = [
|
|
|
|
|
'ActionDevice',
|
|
|
|
|
'Allocate',
|
|
|
|
|
'DataWipe',
|
|
|
|
|
'Deallocate',
|
|
|
|
|
'Management',
|
|
|
|
|
'Prepare',
|
|
|
|
|
'Ready',
|
|
|
|
|
'Recycling',
|
|
|
|
|
'Refurbish',
|
|
|
|
|
'ToPrepare',
|
|
|
|
|
'ToRepair',
|
|
|
|
|
'Use',
|
|
|
|
|
]
|
|
|
|
|
return [a for a in self.actions if a in mactions]
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
@property
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def actions(self) -> list:
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""All the actions where the device participated, including:
|
2019-02-03 16:12:53 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
1. Actions performed directly to the device.
|
|
|
|
|
2. Actions performed to a component.
|
|
|
|
|
3. Actions performed to a parent device.
|
2018-06-24 14:57:49 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
Actions are returned by descending ``created`` time.
|
2018-06-10 16:47:49 +00:00
|
|
|
|
"""
|
2021-11-04 10:58:15 +00:00
|
|
|
|
actions_multiple = copy.copy(self.actions_multiple)
|
|
|
|
|
actions_one = copy.copy(self.actions_one)
|
2022-12-22 17:39:59 +00:00
|
|
|
|
actions = []
|
2021-11-04 10:58:15 +00:00
|
|
|
|
|
|
|
|
|
for ac in actions_multiple:
|
|
|
|
|
ac.real_created = ac.actions_device[0].created
|
2022-12-22 17:39:59 +00:00
|
|
|
|
actions.append(ac)
|
2021-11-04 10:58:15 +00:00
|
|
|
|
|
|
|
|
|
for ac in actions_one:
|
|
|
|
|
ac.real_created = ac.created
|
2023-01-16 14:26:58 +00:00
|
|
|
|
actions.append(ac)
|
2021-11-04 10:58:15 +00:00
|
|
|
|
|
2022-12-22 17:39:59 +00:00
|
|
|
|
return sorted(actions, key=lambda x: x.real_created)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
@property
|
|
|
|
|
def problems(self):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
"""Current actions with severity.Warning or higher.
|
2018-11-09 10:22:13 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
There can be up to 3 actions: current Snapshot,
|
|
|
|
|
current Physical action, current Trading action.
|
2018-11-09 10:22:13 +00:00
|
|
|
|
"""
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import Snapshot
|
2022-04-28 15:46:12 +00:00
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions = set()
|
2018-11-09 10:22:13 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions.add(self.last_action_of(Snapshot))
|
2018-11-09 10:22:13 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions.add(self.last_action_of(*states.Physical.actions()))
|
2018-11-09 10:22:13 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions.add(self.last_action_of(*states.Trading.actions()))
|
|
|
|
|
return self._warning_actions(actions)
|
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]:
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Fields that describe the physical properties of a device.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
:return A dictionary:
|
2018-04-27 17:16:43 +00:00
|
|
|
|
- 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
|
2022-04-28 15:46:12 +00:00
|
|
|
|
return {
|
|
|
|
|
c.key: getattr(self, c.key, None)
|
|
|
|
|
for c in inspect(self.__class__).attrs
|
|
|
|
|
if isinstance(c, ColumnProperty)
|
|
|
|
|
and not getattr(c, 'foreign_keys', None)
|
|
|
|
|
and c.key not in self._NON_PHYSICAL_PROPS
|
|
|
|
|
}
|
2021-10-14 10:56:33 +00:00
|
|
|
|
|
2021-02-11 19:00:57 +00:00
|
|
|
|
@property
|
|
|
|
|
def public_properties(self) -> Dict[str, object or None]:
|
2021-10-14 10:56:33 +00:00
|
|
|
|
"""Fields that describe the properties of a device than next show
|
2021-02-11 19:00:57 +00:00
|
|
|
|
in the public page.
|
|
|
|
|
|
|
|
|
|
:return A dictionary:
|
|
|
|
|
- Column.
|
|
|
|
|
- Actual value of the column or None.
|
|
|
|
|
"""
|
|
|
|
|
non_public = ['amount', 'transfer_state', 'receiver_id']
|
|
|
|
|
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
|
2022-04-28 15:46:12 +00:00
|
|
|
|
return {
|
|
|
|
|
c.key: getattr(self, c.key, None)
|
|
|
|
|
for c in inspect(self.__class__).attrs
|
|
|
|
|
if isinstance(c, ColumnProperty)
|
|
|
|
|
and not getattr(c, 'foreign_keys', None)
|
|
|
|
|
and c.key not in hide_properties
|
|
|
|
|
}
|
2021-02-11 19:00:57 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def public_actions(self) -> List[object]:
|
|
|
|
|
"""Actions than we want show in public page as traceability log section
|
|
|
|
|
:return a list of actions:
|
|
|
|
|
"""
|
2021-02-12 09:29:23 +00:00
|
|
|
|
hide_actions = ['Price', 'EreusePrice']
|
2021-10-22 12:19:42 +00:00
|
|
|
|
actions = [ac for ac in self.actions if ac.t not in hide_actions]
|
2021-02-11 19:00:57 +00:00
|
|
|
|
actions.reverse()
|
|
|
|
|
return actions
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2022-05-05 10:00:02 +00:00
|
|
|
|
@property
|
|
|
|
|
def public_link(self) -> str:
|
|
|
|
|
host_url = request.host_url.strip('/')
|
|
|
|
|
return "{}{}".format(host_url, self.url.to_text())
|
|
|
|
|
|
2018-10-05 15:13:23 +00:00
|
|
|
|
@property
|
|
|
|
|
def url(self) -> urlutils.URL:
|
|
|
|
|
"""The URL where to GET this device."""
|
2022-08-31 15:27:17 +00:00
|
|
|
|
return urlutils.URL(url_for_resource(Device, item_id=self.dhid))
|
2018-10-05 15:13:23 +00:00
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
@property
|
|
|
|
|
def rate(self):
|
2019-04-23 19:27:31 +00:00
|
|
|
|
"""The last Rate of the device."""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import Rate
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return self.last_action_of(Rate)
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
|
|
|
|
@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):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import Price
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return self.last_action_of(Price)
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
2021-06-04 17:27:01 +00:00
|
|
|
|
@property
|
|
|
|
|
def last_action_trading(self):
|
|
|
|
|
"""which is the last action trading"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2021-06-04 17:27:01 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
return self.last_action_of(*states.Trading.actions())
|
|
|
|
|
|
2022-05-13 10:17:14 +00:00
|
|
|
|
@property
|
|
|
|
|
def allocated_status(self):
|
2022-05-13 15:17:57 +00:00
|
|
|
|
"""Show the actual status of device.
|
|
|
|
|
The status depend of one of this 3 actions:
|
|
|
|
|
- Allocate
|
|
|
|
|
- Deallocate
|
|
|
|
|
- InUse (Live register)
|
2022-05-13 10:17:14 +00:00
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
return self.last_action_of(*states.Usage.actions())
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def physical_status(self):
|
|
|
|
|
"""Show the actual status of device for this owner.
|
|
|
|
|
The status depend of one of this 4 actions:
|
2022-05-13 15:17:57 +00:00
|
|
|
|
- ToPrepare
|
|
|
|
|
- Prepare
|
|
|
|
|
- DataWipe
|
|
|
|
|
- ToRepair
|
|
|
|
|
- Ready
|
2022-05-13 10:17:14 +00:00
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
|
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
return self.last_action_of(*states.Physical.actions())
|
|
|
|
|
|
2021-09-28 11:05:58 +00:00
|
|
|
|
@property
|
|
|
|
|
def status(self):
|
|
|
|
|
"""Show the actual status of device for this owner.
|
|
|
|
|
The status depend of one of this 4 actions:
|
|
|
|
|
- Use
|
|
|
|
|
- Refurbish
|
|
|
|
|
- Recycling
|
|
|
|
|
- Management
|
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2021-09-28 11:05:58 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
return self.last_action_of(*states.Status.actions())
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def history_status(self):
|
|
|
|
|
"""Show the history of the status actions of the device.
|
|
|
|
|
The status depend of one of this 4 actions:
|
|
|
|
|
- Use
|
|
|
|
|
- Refurbish
|
|
|
|
|
- Recycling
|
|
|
|
|
- Management
|
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2021-09-28 11:05:58 +00:00
|
|
|
|
status_actions = [ac.t for ac in states.Status.actions()]
|
|
|
|
|
history = []
|
|
|
|
|
for ac in self.actions:
|
2021-10-22 12:19:42 +00:00
|
|
|
|
if ac.t not in status_actions:
|
2021-09-28 11:05:58 +00:00
|
|
|
|
continue
|
|
|
|
|
if not history:
|
|
|
|
|
history.append(ac)
|
|
|
|
|
continue
|
|
|
|
|
if ac.rol_user == history[-1].rol_user:
|
|
|
|
|
# get only the last action consecutive for the same user
|
|
|
|
|
history = history[:-1] + [ac]
|
2021-09-28 13:44:25 +00:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
history.append(ac)
|
2021-09-28 11:05:58 +00:00
|
|
|
|
|
|
|
|
|
return history
|
|
|
|
|
|
2022-06-08 07:26:09 +00:00
|
|
|
|
@property
|
|
|
|
|
def sid(self):
|
2022-07-21 11:56:09 +00:00
|
|
|
|
actions = []
|
|
|
|
|
if self.placeholder and self.placeholder.binding:
|
|
|
|
|
actions = [
|
|
|
|
|
x
|
|
|
|
|
for x in self.placeholder.binding.actions
|
|
|
|
|
if x.t == 'Snapshot' and x.sid
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
actions = [x for x in self.actions if x.t == 'Snapshot' and x.sid]
|
|
|
|
|
|
2022-06-08 07:26:09 +00:00
|
|
|
|
if actions:
|
2022-06-08 09:59:22 +00:00
|
|
|
|
return actions[0].sid
|
2022-06-08 07:26:09 +00:00
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
@property
|
2021-11-10 17:59:44 +00:00
|
|
|
|
def tradings(self):
|
2021-11-12 09:00:43 +00:00
|
|
|
|
return {str(x.id): self.trading(x.lot) for x in self.actions if x.t == 'Trade'}
|
2021-11-10 17:59:44 +00:00
|
|
|
|
|
2022-05-26 15:55:52 +00:00
|
|
|
|
def trading(self, lot, simple=None): # noqa: C901
|
2021-06-03 17:05:57 +00:00
|
|
|
|
"""The trading state, or None if no Trade action has
|
2021-11-10 17:59:44 +00:00
|
|
|
|
ever been performed to this device. This extract the posibilities for to do.
|
2021-11-12 09:00:43 +00:00
|
|
|
|
This method is performed for show in the web.
|
|
|
|
|
If you need to do one simple and generic response you can put simple=True for that."""
|
2021-11-10 17:59:44 +00:00
|
|
|
|
if not hasattr(lot, 'trade'):
|
|
|
|
|
return
|
|
|
|
|
|
2022-04-28 15:46:12 +00:00
|
|
|
|
Status = {
|
|
|
|
|
0: 'Trade',
|
|
|
|
|
1: 'Confirm',
|
|
|
|
|
2: 'NeedConfirmation',
|
|
|
|
|
3: 'TradeConfirmed',
|
|
|
|
|
4: 'Revoke',
|
|
|
|
|
5: 'NeedConfirmRevoke',
|
|
|
|
|
6: 'RevokeConfirmed',
|
|
|
|
|
}
|
2021-11-10 17:59:44 +00:00
|
|
|
|
|
2021-11-11 21:08:28 +00:00
|
|
|
|
trade = lot.trade
|
|
|
|
|
user_from = trade.user_from
|
|
|
|
|
user_to = trade.user_to
|
|
|
|
|
status = 0
|
|
|
|
|
last_user = None
|
|
|
|
|
|
|
|
|
|
if not hasattr(trade, 'acceptances'):
|
|
|
|
|
return Status[status]
|
|
|
|
|
|
|
|
|
|
for ac in self.actions:
|
|
|
|
|
if ac.t not in ['Confirm', 'Revoke']:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ac.user not in [user_from, user_to]:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ac.t == 'Confirm' and ac.action == trade:
|
|
|
|
|
if status in [0, 6]:
|
2021-11-12 09:00:43 +00:00
|
|
|
|
if simple:
|
|
|
|
|
status = 2
|
|
|
|
|
continue
|
2021-11-11 21:08:28 +00:00
|
|
|
|
status = 1
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
if ac.user == user_from and user_to == g.user:
|
|
|
|
|
status = 2
|
|
|
|
|
if ac.user == user_to and user_from == g.user:
|
|
|
|
|
status = 2
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if status in [1, 2]:
|
|
|
|
|
if last_user != ac.user:
|
|
|
|
|
status = 3
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if status in [4, 5]:
|
|
|
|
|
status = 3
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ac.t == 'Revoke' and ac.action == trade:
|
|
|
|
|
if status == 3:
|
2021-11-12 09:00:43 +00:00
|
|
|
|
if simple:
|
|
|
|
|
status = 5
|
|
|
|
|
continue
|
2021-11-11 21:08:28 +00:00
|
|
|
|
status = 4
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
if ac.user == user_from and user_to == g.user:
|
|
|
|
|
status = 5
|
|
|
|
|
if ac.user == user_to and user_from == g.user:
|
|
|
|
|
status = 5
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if status in [4, 5]:
|
|
|
|
|
if last_user != ac.user:
|
|
|
|
|
status = 6
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if status in [1, 2]:
|
|
|
|
|
status = 6
|
|
|
|
|
last_user = ac.user
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
return Status[status]
|
2021-06-03 17:05:57 +00:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def revoke(self):
|
|
|
|
|
"""If the actual trading state is an revoke action, this property show
|
|
|
|
|
the id of that revoke"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
action = self.last_action_of(*states.Trading.actions())
|
2021-06-03 17:05:57 +00:00
|
|
|
|
if action.type == 'Revoke':
|
|
|
|
|
return action.id
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
|
|
|
|
@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
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
2019-05-11 14:27:22 +00:00
|
|
|
|
action = self.last_action_of(*states.Physical.actions())
|
|
|
|
|
return states.Physical(action.__class__)
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
2020-11-19 14:25:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def traking(self):
|
|
|
|
|
"""The actual traking state, None otherwise."""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2020-11-19 14:25:56 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
action = self.last_action_of(*states.Traking.actions())
|
|
|
|
|
return states.Traking(action.__class__)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def usage(self):
|
|
|
|
|
"""The actual usage state, None otherwise."""
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2020-11-19 14:25:56 +00:00
|
|
|
|
with suppress(LookupError, ValueError):
|
|
|
|
|
action = self.last_action_of(*states.Usage.actions())
|
|
|
|
|
return states.Usage(action.__class__)
|
|
|
|
|
|
2018-10-13 12:53:46 +00:00
|
|
|
|
@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,
|
2019-05-11 14:27:22 +00:00
|
|
|
|
and :class:`ereuse_devicehub.resources.action.models.Receive`
|
2018-11-12 17:15:24 +00:00
|
|
|
|
changes it.
|
2018-10-13 12:53:46 +00:00
|
|
|
|
"""
|
2020-11-23 10:43:30 +00:00
|
|
|
|
pass
|
|
|
|
|
# TODO @cayop uncomment this lines for link the possessor with the device
|
|
|
|
|
# from ereuse_devicehub.resources.action.models import Receive
|
|
|
|
|
# with suppress(LookupError):
|
2021-10-22 12:19:42 +00:00
|
|
|
|
# action = self.last_action_of(Receive)
|
|
|
|
|
# return action.agent_to
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
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,
|
2019-02-03 16:12:53 +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.
|
|
|
|
|
"""
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import Test
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
|
|
|
|
current_tests = unique_everseen(
|
|
|
|
|
(e for e in reversed(self.actions) if isinstance(e, Test)),
|
|
|
|
|
key=attrgetter('type'),
|
|
|
|
|
) # last test of each type
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return self._warning_actions(current_tests)
|
2018-11-09 10:22:13 +00:00
|
|
|
|
|
2022-03-04 12:31:33 +00:00
|
|
|
|
@property
|
|
|
|
|
def verbose_name(self):
|
|
|
|
|
type = self.type or ''
|
|
|
|
|
manufacturer = self.manufacturer or ''
|
|
|
|
|
model = self.model or ''
|
|
|
|
|
return f'{type} {manufacturer} {model}'
|
|
|
|
|
|
2022-08-31 15:27:17 +00:00
|
|
|
|
@property
|
|
|
|
|
def dhid(self):
|
|
|
|
|
if self.placeholder:
|
|
|
|
|
return self.placeholder.device.devicehub_id
|
|
|
|
|
if self.binding:
|
|
|
|
|
return self.binding.device.devicehub_id
|
|
|
|
|
return self.devicehub_id
|
|
|
|
|
|
2022-11-22 10:59:31 +00:00
|
|
|
|
@property
|
|
|
|
|
def my_partner(self):
|
|
|
|
|
if self.placeholder and self.placeholder.binding:
|
|
|
|
|
return self.placeholder.binding
|
|
|
|
|
if self.binding:
|
|
|
|
|
return self.binding.device
|
|
|
|
|
return self
|
|
|
|
|
|
2022-09-20 15:25:09 +00:00
|
|
|
|
@property
|
|
|
|
|
def get_updated(self):
|
|
|
|
|
if self.placeholder and self.placeholder.binding:
|
|
|
|
|
return max([self.updated, self.placeholder.binding.updated])
|
|
|
|
|
if self.binding:
|
|
|
|
|
return max([self.updated, self.binding.device.updated])
|
|
|
|
|
return self.updated
|
|
|
|
|
|
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}
|
|
|
|
|
if cls.t == 'Device':
|
2018-04-10 15:06:39 +00:00
|
|
|
|
args[POLYMORPHIC_ON] = cls.type
|
|
|
|
|
return args
|
|
|
|
|
|
2022-10-06 16:47:32 +00:00
|
|
|
|
def get_lots_for_template(self):
|
2023-03-09 17:38:54 +00:00
|
|
|
|
if self.binding:
|
|
|
|
|
return self.binding.device.get_lots_for_template()
|
|
|
|
|
|
|
|
|
|
if not self.lots and hasattr(self, 'parent') and self.parent:
|
|
|
|
|
return self.parent.get_lots_for_template()
|
|
|
|
|
|
2022-10-06 16:47:32 +00:00
|
|
|
|
lots = []
|
|
|
|
|
for lot in self.lots:
|
|
|
|
|
if lot.is_incoming:
|
|
|
|
|
name = "IN - " + lot.name
|
|
|
|
|
lots.append(name)
|
|
|
|
|
if lot.is_outgoing:
|
|
|
|
|
name = "OUT - " + lot.name
|
|
|
|
|
lots.append(name)
|
|
|
|
|
if lot.is_temporary:
|
|
|
|
|
name = "TEMP - " + lot.name
|
|
|
|
|
lots.append(name)
|
|
|
|
|
lots.sort()
|
|
|
|
|
return lots
|
|
|
|
|
|
2022-07-21 11:56:09 +00:00
|
|
|
|
def phid(self):
|
|
|
|
|
if self.placeholder:
|
|
|
|
|
return self.placeholder.phid
|
|
|
|
|
if self.binding:
|
|
|
|
|
return self.binding.phid
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
def list_tags(self):
|
|
|
|
|
return ', '.join([t.id for t in self.tags])
|
|
|
|
|
|
2022-07-05 16:09:47 +00:00
|
|
|
|
def appearance(self):
|
|
|
|
|
actions = copy.copy(self.actions)
|
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
with suppress(LookupError, ValueError, StopIteration):
|
|
|
|
|
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
|
|
|
|
|
return action.appearance_range
|
|
|
|
|
|
|
|
|
|
def functionality(self):
|
|
|
|
|
actions = copy.copy(self.actions)
|
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
with suppress(LookupError, ValueError, StopIteration):
|
|
|
|
|
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
|
|
|
|
|
return action.functionality_range
|
|
|
|
|
|
|
|
|
|
def set_appearance(self, value):
|
|
|
|
|
actions = copy.copy(self.actions)
|
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
with suppress(LookupError, ValueError, StopIteration):
|
|
|
|
|
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
|
|
|
|
|
action.appearance_range = value
|
|
|
|
|
|
|
|
|
|
def set_functionality(self, value):
|
|
|
|
|
actions = copy.copy(self.actions)
|
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
with suppress(LookupError, ValueError, StopIteration):
|
|
|
|
|
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
|
|
|
|
|
action.functionality_range = value
|
|
|
|
|
|
2022-08-05 14:42:07 +00:00
|
|
|
|
def is_abstract(self):
|
2022-08-09 08:49:56 +00:00
|
|
|
|
if self.placeholder:
|
|
|
|
|
if self.placeholder.is_abstract:
|
2022-09-15 09:51:27 +00:00
|
|
|
|
return 'Snapshot'
|
2022-08-09 08:49:56 +00:00
|
|
|
|
if self.placeholder.binding:
|
|
|
|
|
return 'Twin'
|
2022-09-15 09:51:27 +00:00
|
|
|
|
return 'Placeholder'
|
2022-08-09 08:49:56 +00:00
|
|
|
|
if self.binding:
|
|
|
|
|
if self.binding.is_abstract:
|
2022-09-15 09:51:27 +00:00
|
|
|
|
return 'Snapshot'
|
2022-08-05 14:42:07 +00:00
|
|
|
|
return 'Twin'
|
2022-08-09 08:49:56 +00:00
|
|
|
|
|
|
|
|
|
return ''
|
2022-08-05 14:42:07 +00:00
|
|
|
|
|
2022-05-13 15:17:57 +00:00
|
|
|
|
def is_status(self, action):
|
|
|
|
|
from ereuse_devicehub.resources.device import states
|
|
|
|
|
|
|
|
|
|
if action.type in states.Usage.__members__:
|
|
|
|
|
return "Allocate State: "
|
|
|
|
|
|
|
|
|
|
if action.type in states.Status.__members__:
|
|
|
|
|
return "Lifecycle State: "
|
|
|
|
|
|
|
|
|
|
if action.type in states.Physical.__members__:
|
|
|
|
|
return "Physical State: "
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
2023-01-26 10:35:08 +00:00
|
|
|
|
def get_exist_untrusted_device(self):
|
|
|
|
|
if isinstance(self, Computer):
|
|
|
|
|
if not self.system_uuid:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
Computer.query.filter_by(
|
|
|
|
|
hid=self.hid,
|
|
|
|
|
user_trusts=False,
|
|
|
|
|
owner_id=g.user.id,
|
|
|
|
|
active=True,
|
|
|
|
|
placeholder=None,
|
|
|
|
|
).first()
|
|
|
|
|
or False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
2022-12-12 13:10:17 +00:00
|
|
|
|
def get_from_db(self):
|
2022-12-20 10:06:41 +00:00
|
|
|
|
if 'property_hid' in app.blueprints.keys():
|
|
|
|
|
try:
|
|
|
|
|
from modules.device.utils import get_from_db
|
2022-12-13 13:29:52 +00:00
|
|
|
|
|
2022-12-20 10:06:41 +00:00
|
|
|
|
return get_from_db(self)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2022-12-13 13:29:52 +00:00
|
|
|
|
|
2022-12-12 13:10:17 +00:00
|
|
|
|
if not self.hid:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
return Device.query.filter_by(
|
|
|
|
|
hid=self.hid,
|
|
|
|
|
owner_id=g.user.id,
|
|
|
|
|
active=True,
|
|
|
|
|
placeholder=None,
|
|
|
|
|
).first()
|
|
|
|
|
|
|
|
|
|
def set_hid(self):
|
2022-12-20 10:06:41 +00:00
|
|
|
|
if 'property_hid' in app.blueprints.keys():
|
2022-12-20 09:17:42 +00:00
|
|
|
|
try:
|
|
|
|
|
from modules.device.utils import set_hid
|
|
|
|
|
|
|
|
|
|
self.hid = set_hid(self)
|
|
|
|
|
self.set_chid()
|
|
|
|
|
return
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2022-12-13 13:29:52 +00:00
|
|
|
|
|
2022-12-19 19:03:34 +00:00
|
|
|
|
self.hid = "{}-{}-{}-{}".format(
|
|
|
|
|
self._clean_string(self.type),
|
|
|
|
|
self._clean_string(self.manufacturer),
|
|
|
|
|
self._clean_string(self.model),
|
|
|
|
|
self._clean_string(self.serial_number),
|
|
|
|
|
).lower()
|
2022-12-13 19:41:15 +00:00
|
|
|
|
self.set_chid()
|
2022-11-16 12:47:50 +00:00
|
|
|
|
|
2022-12-19 19:03:34 +00:00
|
|
|
|
def _clean_string(self, s):
|
|
|
|
|
if not s:
|
|
|
|
|
return ''
|
|
|
|
|
return s.replace(' ', '_')
|
|
|
|
|
|
2022-12-13 13:29:52 +00:00
|
|
|
|
def set_chid(self):
|
|
|
|
|
if self.hid:
|
|
|
|
|
self.chid = hashlib.sha3_256(self.hid.encode()).hexdigest()
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def last_action_of(self, *types):
|
|
|
|
|
"""Gets the last action of the given types.
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
:raise LookupError: Device has not an action of the given type.
|
2018-10-13 12:53:46 +00:00
|
|
|
|
"""
|
|
|
|
|
try:
|
2019-05-03 12:31:49 +00:00
|
|
|
|
# noinspection PyTypeHints
|
2021-06-07 08:50:08 +00:00
|
|
|
|
actions = copy.copy(self.actions)
|
2020-11-27 17:10:51 +00:00
|
|
|
|
actions.sort(key=lambda x: x.created)
|
|
|
|
|
return next(e for e in reversed(actions) if isinstance(e, types))
|
2018-10-13 12:53:46 +00:00
|
|
|
|
except StopIteration:
|
2022-04-28 15:46:12 +00:00
|
|
|
|
raise LookupError(
|
|
|
|
|
'{!r} does not contain actions of types {}.'.format(self, types)
|
|
|
|
|
)
|
2018-10-13 12:53:46 +00:00
|
|
|
|
|
2021-06-07 08:50:08 +00:00
|
|
|
|
def which_user_put_this_device_in_trace(self):
|
|
|
|
|
"""which is the user than put this device in this trade"""
|
|
|
|
|
actions = copy.copy(self.actions)
|
|
|
|
|
actions.reverse()
|
|
|
|
|
# search the automatic Confirm
|
|
|
|
|
for ac in actions:
|
|
|
|
|
if ac.type == 'Trade':
|
2021-11-11 16:06:26 +00:00
|
|
|
|
action_device = [x for x in ac.actions_device if x.device == self][0]
|
2021-11-10 17:59:44 +00:00
|
|
|
|
if action_device.author:
|
|
|
|
|
return action_device.author
|
|
|
|
|
|
|
|
|
|
return ac.author
|
2021-06-07 08:50:08 +00:00
|
|
|
|
|
|
|
|
|
def change_owner(self, new_user):
|
|
|
|
|
"""util for change the owner one device"""
|
2023-01-11 09:18:22 +00:00
|
|
|
|
if not new_user:
|
|
|
|
|
return
|
2021-06-07 08:50:08 +00:00
|
|
|
|
self.owner = new_user
|
|
|
|
|
if hasattr(self, 'components'):
|
|
|
|
|
for c in self.components:
|
|
|
|
|
c.owner = new_user
|
|
|
|
|
|
|
|
|
|
def reset_owner(self):
|
|
|
|
|
"""Change the owner with the user put the device into the trade"""
|
|
|
|
|
user = self.which_user_put_this_device_in_trace()
|
|
|
|
|
self.change_owner(user)
|
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def _warning_actions(self, actions):
|
|
|
|
|
return sorted(ev for ev in actions if ev.severity >= Severity.Warning)
|
2018-11-09 10:22:13 +00:00
|
|
|
|
|
2021-01-13 17:11:41 +00:00
|
|
|
|
def get_metrics(self):
|
|
|
|
|
"""
|
|
|
|
|
This method get a list of values for calculate a metrics from a spreadsheet
|
|
|
|
|
"""
|
2021-10-15 13:04:58 +00:00
|
|
|
|
metrics = Metrics(device=self)
|
2021-10-14 10:56:33 +00:00
|
|
|
|
return metrics.get_metrics()
|
2021-01-13 17:11:41 +00:00
|
|
|
|
|
2022-04-28 15:46:12 +00:00
|
|
|
|
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",
|
2022-04-28 15:56:00 +00:00
|
|
|
|
"Display": "bi bi-display",
|
|
|
|
|
"ComputerMonitor": "bi bi-display",
|
|
|
|
|
"TelevisionSet": "bi bi-easel",
|
2022-04-28 15:46:12 +00:00
|
|
|
|
"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, '')
|
|
|
|
|
|
2022-12-23 17:33:49 +00:00
|
|
|
|
def unreliable(self):
|
2022-12-22 11:01:31 +00:00
|
|
|
|
self.user_trusts = False
|
2022-12-23 17:33:49 +00:00
|
|
|
|
i = 0
|
|
|
|
|
snapshot1 = None
|
2023-01-11 16:17:50 +00:00
|
|
|
|
snapshots = {}
|
2022-12-23 17:33:49 +00:00
|
|
|
|
|
|
|
|
|
for ac in self.actions:
|
|
|
|
|
if ac.type == 'Snapshot':
|
|
|
|
|
if i == 0:
|
|
|
|
|
snapshot1 = ac
|
|
|
|
|
if i > 0:
|
2023-01-11 16:17:50 +00:00
|
|
|
|
snapshots[ac] = self.get_snapshot_file(ac)
|
2022-12-23 17:33:49 +00:00
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
if not snapshot1:
|
|
|
|
|
return
|
|
|
|
|
|
2023-01-26 09:46:34 +00:00
|
|
|
|
self.create_new_device(snapshots.values(), user_trusts=self.user_trusts)
|
2023-01-13 16:42:42 +00:00
|
|
|
|
self.remove_snapshot(snapshots.keys())
|
2022-12-23 17:33:49 +00:00
|
|
|
|
|
2022-12-22 11:01:31 +00:00
|
|
|
|
return
|
|
|
|
|
|
2023-01-11 16:17:50 +00:00
|
|
|
|
def get_snapshot_file(self, action):
|
|
|
|
|
uuid = action.uuid
|
|
|
|
|
user = g.user.email
|
|
|
|
|
name_file = f"*_{user}_{uuid}.json"
|
|
|
|
|
tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
|
|
|
|
path_dir_base = os.path.join(tmp_snapshots, user)
|
|
|
|
|
|
|
|
|
|
for _file in pathlib.Path(path_dir_base).glob(name_file):
|
|
|
|
|
with open(_file) as file_snapshot:
|
|
|
|
|
snapshot = file_snapshot.read()
|
|
|
|
|
return json.loads(snapshot)
|
|
|
|
|
|
2023-01-26 09:46:34 +00:00
|
|
|
|
def create_new_device(self, snapshots, user_trusts=True):
|
2023-01-11 16:17:50 +00:00
|
|
|
|
from ereuse_devicehub.inventory.forms import UploadSnapshotForm
|
|
|
|
|
|
|
|
|
|
new_snapshots = []
|
|
|
|
|
for snapshot in snapshots:
|
|
|
|
|
snapshot['uuid'] = str(uuid.uuid4())
|
|
|
|
|
filename = "{}.json".format(snapshot['uuid'])
|
|
|
|
|
new_snapshots.append((filename, snapshot))
|
|
|
|
|
|
|
|
|
|
form = UploadSnapshotForm()
|
|
|
|
|
form.result = {}
|
|
|
|
|
form.snapshots = new_snapshots
|
2023-01-13 16:42:42 +00:00
|
|
|
|
form.create_new_devices = True
|
2023-01-26 09:46:34 +00:00
|
|
|
|
form.save(commit=False, user_trusts=user_trusts)
|
2023-01-11 16:17:50 +00:00
|
|
|
|
|
2023-01-13 16:42:42 +00:00
|
|
|
|
def remove_snapshot(self, snapshots):
|
|
|
|
|
from ereuse_devicehub.parser.models import SnapshotsLog
|
|
|
|
|
|
|
|
|
|
for ac in snapshots:
|
|
|
|
|
for slog in SnapshotsLog.query.filter_by(snapshot=ac):
|
|
|
|
|
slog.snapshot_id = None
|
|
|
|
|
slog.snapshot_uuid = None
|
|
|
|
|
db.session.delete(ac)
|
|
|
|
|
|
|
|
|
|
def remove_devices(self, devices):
|
|
|
|
|
from ereuse_devicehub.parser.models import SnapshotsLog
|
|
|
|
|
|
|
|
|
|
for dev in devices:
|
|
|
|
|
for ac in dev.actions:
|
|
|
|
|
if ac.type != 'Snapshot':
|
|
|
|
|
continue
|
|
|
|
|
for slog in SnapshotsLog.query.filter_by(snapshot=ac):
|
|
|
|
|
slog.snapshot_id = None
|
|
|
|
|
slog.snapshot_uuid = None
|
|
|
|
|
|
|
|
|
|
for c in dev.components:
|
|
|
|
|
c.parent_id = None
|
|
|
|
|
|
|
|
|
|
for tag in dev.tags:
|
|
|
|
|
tag.device_id = None
|
|
|
|
|
|
|
|
|
|
placeholder = dev.binding or dev.placeholder
|
|
|
|
|
if placeholder:
|
|
|
|
|
db.session.delete(placeholder.binding)
|
|
|
|
|
db.session.delete(placeholder.device)
|
|
|
|
|
db.session.delete(placeholder)
|
|
|
|
|
|
2022-12-23 17:33:49 +00:00
|
|
|
|
def reliable(self):
|
|
|
|
|
computers = Computer.query.filter_by(
|
|
|
|
|
hid=self.hid,
|
|
|
|
|
owner_id=g.user.id,
|
|
|
|
|
active=True,
|
|
|
|
|
placeholder=None,
|
|
|
|
|
).order_by(Device.created.asc())
|
|
|
|
|
|
|
|
|
|
i = 0
|
|
|
|
|
computer1 = None
|
2023-01-13 16:42:42 +00:00
|
|
|
|
computers_to_remove = []
|
2022-12-23 17:33:49 +00:00
|
|
|
|
for d in computers:
|
|
|
|
|
if i == 0:
|
|
|
|
|
d.user_trusts = True
|
|
|
|
|
computer1 = d
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
2023-01-13 16:42:42 +00:00
|
|
|
|
computers_to_remove.append(d)
|
2022-12-23 17:33:49 +00:00
|
|
|
|
|
2023-01-13 16:42:42 +00:00
|
|
|
|
self.remove_devices(computers_to_remove)
|
2022-12-23 17:33:49 +00:00
|
|
|
|
if not computer1:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
snapshot1 = None
|
|
|
|
|
for ac in computer1.actions_one:
|
|
|
|
|
if ac.type == 'Snapshot':
|
|
|
|
|
snapshot1 = ac
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not snapshot1:
|
|
|
|
|
return
|
|
|
|
|
|
2022-12-22 11:01:31 +00:00
|
|
|
|
return
|
|
|
|
|
|
2023-03-13 11:24:57 +00:00
|
|
|
|
def get_last_incoming_lot(self):
|
|
|
|
|
lots = list(self.lots)
|
2023-03-10 19:13:54 +00:00
|
|
|
|
if hasattr(self, "orphan") and self.orphan:
|
2023-03-13 11:24:57 +00:00
|
|
|
|
lots = list(self.lots)
|
2023-03-10 19:13:54 +00:00
|
|
|
|
if self.binding:
|
2023-03-13 11:24:57 +00:00
|
|
|
|
lots = list(self.binding.device.lots)
|
2023-03-10 17:34:09 +00:00
|
|
|
|
|
2023-03-13 11:24:57 +00:00
|
|
|
|
elif hasattr(self, "parent") and self.parent:
|
|
|
|
|
lots = list(self.parent.lots)
|
2023-03-10 19:13:54 +00:00
|
|
|
|
if self.parent.binding:
|
2023-03-13 11:24:57 +00:00
|
|
|
|
lots = list(self.parent.binding.device.lots)
|
2023-03-10 17:34:09 +00:00
|
|
|
|
|
2023-03-13 11:24:57 +00:00
|
|
|
|
lots = sorted(lots, key=lambda x: x.created)
|
|
|
|
|
lots.reverse()
|
|
|
|
|
for lot in lots:
|
|
|
|
|
if lot.is_incoming:
|
|
|
|
|
return lot
|
|
|
|
|
return None
|
2023-03-10 17:34:09 +00:00
|
|
|
|
|
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:
|
2019-07-07 19:36:09 +00:00
|
|
|
|
superclass = self.__class__.mro()[1]
|
|
|
|
|
if not isinstance(self, Device) and superclass != Device:
|
|
|
|
|
assert issubclass(superclass, Thing)
|
|
|
|
|
v += superclass.__name__ + ' '
|
|
|
|
|
v += '{0.manufacturer}'.format(self)
|
2018-10-16 14:30:10 +00:00
|
|
|
|
if self.serial_number:
|
2019-07-07 19:36:09 +00:00
|
|
|
|
v += ' ' + 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:
|
2019-05-03 12:31:49 +00:00
|
|
|
|
"""Base class for the Display Component and the Monitor Device."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
|
|
|
|
size = Column(
|
|
|
|
|
Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True
|
|
|
|
|
)
|
2019-06-19 11:35:26 +00:00
|
|
|
|
size.comment = """The size of the monitor in inches."""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
technology = Column(DBEnum(DisplayTech))
|
2020-04-01 17:11:14 +00:00
|
|
|
|
technology.comment = """The technology the monitor uses to display
|
2019-06-19 11:35:26 +00:00
|
|
|
|
the image.
|
2018-06-26 13:36:21 +00:00
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
resolution_width = Column(
|
|
|
|
|
SmallInteger, check_range('resolution_width', 10, 20000), nullable=True
|
|
|
|
|
)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
resolution_width.comment = """The maximum horizontal resolution the
|
2019-06-19 11:35:26 +00:00
|
|
|
|
monitor can natively support in pixels.
|
2018-06-26 13:36:21 +00:00
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
resolution_height = Column(
|
|
|
|
|
SmallInteger, check_range('resolution_height', 10, 20000), nullable=True
|
|
|
|
|
)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
resolution_height.comment = """The maximum vertical resolution the
|
2019-06-19 11:35:26 +00:00
|
|
|
|
monitor can natively support in pixels.
|
2018-06-26 13:36:21 +00:00
|
|
|
|
"""
|
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))
|
2019-05-03 12:31:49 +00:00
|
|
|
|
touchable = Column(Boolean)
|
2018-10-23 13:37:37 +00:00
|
|
|
|
touchable.comment = """Whether it is a touchscreen."""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
2019-05-03 12:31:49 +00:00
|
|
|
|
@hybrid_property
|
|
|
|
|
def aspect_ratio(self):
|
|
|
|
|
"""The aspect ratio of the display, as a fraction: ``X/Y``.
|
|
|
|
|
|
|
|
|
|
Regular values are ``4/3``, ``5/4``, ``16/9``, ``21/9``,
|
|
|
|
|
``14/10``, ``19/10``, ``16/10``.
|
|
|
|
|
"""
|
2020-12-17 15:03:10 +00:00
|
|
|
|
if self.resolution_height and self.resolution_width:
|
|
|
|
|
return Fraction(self.resolution_width, self.resolution_height)
|
2020-12-17 15:52:42 +00:00
|
|
|
|
return 0
|
2019-05-03 12:31:49 +00:00
|
|
|
|
|
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
|
|
|
@aspect_ratio.expression
|
|
|
|
|
def aspect_ratio(cls):
|
|
|
|
|
# The aspect ratio to use as SQL in the DB
|
|
|
|
|
# This allows comparing resolutions
|
2020-12-17 15:52:42 +00:00
|
|
|
|
return db.func.round(cls.resolution_width / cls.resolution_height, 2)
|
2019-05-03 12:31:49 +00:00
|
|
|
|
|
|
|
|
|
@hybrid_property
|
|
|
|
|
def widescreen(self):
|
|
|
|
|
"""Whether the monitor is considered to be widescreen.
|
|
|
|
|
|
|
|
|
|
Widescreen monitors are those having a higher aspect ratio
|
|
|
|
|
greater than 4/3.
|
|
|
|
|
"""
|
|
|
|
|
# We add a tiny extra to 4/3 to avoid precision errors
|
|
|
|
|
return self.aspect_ratio > 4.001 / 3
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2020-12-17 15:03:10 +00:00
|
|
|
|
if self.size:
|
2022-04-28 15:46:12 +00:00
|
|
|
|
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
|
|
|
|
|
self
|
|
|
|
|
)
|
|
|
|
|
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(
|
|
|
|
|
self
|
|
|
|
|
)
|
2019-05-03 12:31:49 +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:
|
2019-05-03 12:31:49 +00:00
|
|
|
|
v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
|
2020-12-17 15:03:10 +00:00
|
|
|
|
if self.size:
|
|
|
|
|
v += '– {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
|
|
|
|
|
else:
|
|
|
|
|
v += '– 0in ({0.aspect_ratio}) {0.technology}'.format(self)
|
2018-10-03 12:51:22 +00:00
|
|
|
|
return v
|
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
|
2022-06-23 13:51:01 +00:00
|
|
|
|
class Placeholder(Thing):
|
|
|
|
|
id = Column(BigInteger, Sequence('placeholder_seq'), primary_key=True)
|
2022-10-05 10:22:40 +00:00
|
|
|
|
phid = Column(Unicode(), nullable=False, default=create_phid)
|
2022-06-28 15:38:05 +00:00
|
|
|
|
pallet = Column(Unicode(), nullable=True)
|
2022-06-23 13:51:01 +00:00
|
|
|
|
pallet.comment = "used for identification where from where is this placeholders"
|
|
|
|
|
info = db.Column(CIText())
|
2022-08-09 08:49:56 +00:00
|
|
|
|
components = Column(CIText())
|
2022-06-23 13:51:01 +00:00
|
|
|
|
info.comment = "more info of placeholders"
|
2022-07-28 15:48:14 +00:00
|
|
|
|
is_abstract = db.Column(Boolean, default=False)
|
2022-06-23 13:51:01 +00:00
|
|
|
|
id_device_supplier = db.Column(CIText())
|
|
|
|
|
id_device_supplier.comment = (
|
|
|
|
|
"Identification used for one supplier of one placeholders"
|
|
|
|
|
)
|
2022-10-05 07:17:27 +00:00
|
|
|
|
id_device_internal = db.Column(CIText())
|
|
|
|
|
id_device_internal.comment = "Identification used internaly for the user"
|
2022-10-13 16:21:44 +00:00
|
|
|
|
kangaroo = db.Column(Boolean, default=False, nullable=True)
|
2022-06-23 13:51:01 +00:00
|
|
|
|
|
2022-06-28 15:38:05 +00:00
|
|
|
|
device_id = db.Column(
|
2022-06-23 13:51:01 +00:00
|
|
|
|
BigInteger,
|
|
|
|
|
db.ForeignKey(Device.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
)
|
2022-06-28 15:38:05 +00:00
|
|
|
|
device = db.relationship(
|
2022-06-23 13:51:01 +00:00
|
|
|
|
Device,
|
2022-08-05 14:42:07 +00:00
|
|
|
|
backref=backref(
|
|
|
|
|
'placeholder', lazy=True, cascade="all, delete-orphan", uselist=False
|
|
|
|
|
),
|
2022-06-28 15:38:05 +00:00
|
|
|
|
primaryjoin=device_id == Device.id,
|
2022-06-23 13:51:01 +00:00
|
|
|
|
)
|
2022-06-28 15:38:05 +00:00
|
|
|
|
device_id.comment = "datas of the placeholder"
|
2022-06-23 13:51:01 +00:00
|
|
|
|
|
2022-07-11 13:36:45 +00:00
|
|
|
|
binding_id = db.Column(
|
|
|
|
|
BigInteger,
|
|
|
|
|
db.ForeignKey(Device.id),
|
|
|
|
|
nullable=True,
|
|
|
|
|
)
|
|
|
|
|
binding = db.relationship(
|
|
|
|
|
Device,
|
|
|
|
|
backref=backref('binding', lazy=True, uselist=False),
|
2022-07-12 09:23:55 +00:00
|
|
|
|
primaryjoin=binding_id == Device.id,
|
2022-07-11 13:36:45 +00:00
|
|
|
|
)
|
|
|
|
|
binding_id.comment = "binding placeholder with workbench device"
|
2022-07-28 15:48:14 +00:00
|
|
|
|
owner_id = db.Column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey(User.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.id,
|
|
|
|
|
)
|
|
|
|
|
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
2022-07-11 13:36:45 +00:00
|
|
|
|
|
2022-08-11 08:21:45 +00:00
|
|
|
|
@property
|
|
|
|
|
def actions(self):
|
|
|
|
|
actions = list(self.device.actions) or []
|
|
|
|
|
|
|
|
|
|
if self.binding:
|
|
|
|
|
actions.extend(list(self.binding.actions))
|
|
|
|
|
|
|
|
|
|
actions = sorted(actions, key=lambda x: x.created)
|
|
|
|
|
actions.reverse()
|
|
|
|
|
return actions
|
|
|
|
|
|
2022-08-11 12:50:47 +00:00
|
|
|
|
@property
|
|
|
|
|
def status(self):
|
|
|
|
|
if self.is_abstract:
|
2022-09-15 09:51:27 +00:00
|
|
|
|
return 'Snapshot'
|
2022-08-11 12:50:47 +00:00
|
|
|
|
if self.binding:
|
|
|
|
|
return 'Twin'
|
2022-09-15 09:51:27 +00:00
|
|
|
|
return 'Placeholder'
|
2022-08-11 12:50:47 +00:00
|
|
|
|
|
2022-06-23 13:51:01 +00:00
|
|
|
|
|
2018-04-10 15:06:39 +00:00
|
|
|
|
class Computer(Device):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""A chassis with components inside that can be processed
|
|
|
|
|
automatically with Workbench Computer.
|
|
|
|
|
|
|
|
|
|
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
|
|
|
|
``Server``. The property ``chassis`` defines it more granularly.
|
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-06-10 16:47:49 +00:00
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
2020-12-16 10:57:51 +00:00
|
|
|
|
chassis = Column(DBEnum(ComputerChassis), nullable=True)
|
2019-02-03 16:12:53 +00:00
|
|
|
|
chassis.comment = """The physical form of the computer.
|
2019-12-10 23:35:17 +00:00
|
|
|
|
|
2019-02-03 16:12:53 +00:00
|
|
|
|
It is a subset of the Linux definition of DMI / DMI decode.
|
|
|
|
|
"""
|
2021-02-05 12:21:20 +00:00
|
|
|
|
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
2022-04-28 15:46:12 +00:00
|
|
|
|
owner_id = db.Column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
db.ForeignKey(User.id),
|
|
|
|
|
nullable=False,
|
|
|
|
|
default=lambda: g.user.id,
|
|
|
|
|
)
|
2021-06-24 17:09:12 +00:00
|
|
|
|
# author = db.relationship(User, primaryjoin=owner_id == User.id)
|
2022-04-28 15:46:12 +00:00
|
|
|
|
transfer_state = db.Column(
|
|
|
|
|
IntEnum(TransferState), default=TransferState.Initial, nullable=False
|
|
|
|
|
)
|
2019-12-17 22:57:55 +00:00
|
|
|
|
transfer_state.comment = TransferState.__doc__
|
2022-04-28 15:46:12 +00:00
|
|
|
|
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
2022-06-15 10:17:16 +00:00
|
|
|
|
system_uuid = db.Column(UUID(as_uuid=True), nullable=True)
|
2022-12-13 13:29:52 +00:00
|
|
|
|
user_trusts = db.Column(Boolean(), default=True)
|
2018-04-10 15:06:39 +00:00
|
|
|
|
|
2020-12-17 09:45:01 +00:00
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
|
if args:
|
|
|
|
|
chassis = ComputerChassis(args[0])
|
|
|
|
|
super().__init__(chassis=chassis, **kwargs)
|
|
|
|
|
else:
|
|
|
|
|
super().__init__(*args, **kwargs)
|
2018-10-18 08:09:10 +00:00
|
|
|
|
|
2018-06-16 10:41:12 +00:00
|
|
|
|
@property
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def actions(self) -> list:
|
2021-11-04 12:27:25 +00:00
|
|
|
|
actions = copy.copy(super().actions)
|
|
|
|
|
actions_parent = copy.copy(self.actions_parent)
|
|
|
|
|
for ac in actions_parent:
|
|
|
|
|
ac.real_created = ac.created
|
|
|
|
|
|
|
|
|
|
return sorted(chain(actions, actions_parent), key=lambda x: x.real_created)
|
|
|
|
|
# return sorted(chain(super().actions, self.actions_parent))
|
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."""
|
2022-04-28 15:46:12 +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."""
|
2022-04-28 15:46:12 +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."""
|
2022-04-28 15:46:12 +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."""
|
2022-04-28 15:46:12 +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
|
2019-05-08 17:12:05 +00:00
|
|
|
|
adaptor exists.
|
2018-10-13 12:53:46 +00:00
|
|
|
|
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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Returns the privacy of all ``DataStorage`` components when
|
2018-11-21 13:26:56 +00:00
|
|
|
|
it is not None.
|
2018-11-09 10:22:13 +00:00
|
|
|
|
"""
|
2022-09-02 14:40:35 +00:00
|
|
|
|
components = self.components
|
|
|
|
|
if self.placeholder and self.placeholder.binding:
|
|
|
|
|
components = self.placeholder.binding.components
|
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
return set(
|
2022-04-28 15:46:12 +00:00
|
|
|
|
privacy
|
|
|
|
|
for privacy in (
|
2022-09-02 14:40:35 +00:00
|
|
|
|
hdd.privacy for hdd in components if isinstance(hdd, DataStorage)
|
2022-04-28 15:46:12 +00:00
|
|
|
|
)
|
2018-11-09 10:22:13 +00:00
|
|
|
|
if privacy
|
|
|
|
|
)
|
|
|
|
|
|
2021-07-28 13:59:11 +00:00
|
|
|
|
@property
|
|
|
|
|
def external_document_erasure(self):
|
2022-04-28 15:46:12 +00:00
|
|
|
|
"""Returns the external ``DataStorage`` proof of erasure."""
|
2021-07-29 10:45:43 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import DataWipe
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2021-07-28 13:59:11 +00:00
|
|
|
|
urls = set()
|
|
|
|
|
try:
|
2021-07-29 10:45:43 +00:00
|
|
|
|
ev = self.last_action_of(DataWipe)
|
2021-07-28 13:59:11 +00:00
|
|
|
|
urls.add(ev.document.url.to_text())
|
|
|
|
|
except LookupError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
for comp in self.components:
|
|
|
|
|
if isinstance(comp, DataStorage):
|
|
|
|
|
doc = comp.external_document_erasure
|
|
|
|
|
if doc:
|
|
|
|
|
urls.add(doc)
|
|
|
|
|
return urls
|
|
|
|
|
|
2020-11-12 19:57:33 +00:00
|
|
|
|
def add_mac_to_hid(self, components_snap=None):
|
2021-10-14 10:56:33 +00:00
|
|
|
|
"""Returns the Naming.hid with the first mac of network adapter,
|
2020-11-10 19:45:03 +00:00
|
|
|
|
following an alphabetical order.
|
|
|
|
|
"""
|
|
|
|
|
self.set_hid()
|
|
|
|
|
if not self.hid:
|
|
|
|
|
return
|
2020-11-13 10:05:00 +00:00
|
|
|
|
components = self.components if components_snap is None else components_snap
|
2022-04-28 15:46:12 +00:00
|
|
|
|
macs_network = [
|
|
|
|
|
c.serial_number
|
|
|
|
|
for c in components
|
|
|
|
|
if c.type == 'NetworkAdapter' and c.serial_number is not None
|
|
|
|
|
]
|
2020-11-10 19:45:03 +00:00
|
|
|
|
macs_network.sort()
|
2020-11-12 19:57:33 +00:00
|
|
|
|
mac = macs_network[0] if macs_network else ''
|
|
|
|
|
if not mac or mac in self.hid:
|
|
|
|
|
return
|
|
|
|
|
mac = f"-{mac}"
|
2020-11-13 10:05:00 +00:00
|
|
|
|
self.hid += mac
|
2020-11-10 19:45:03 +00:00
|
|
|
|
|
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,
|
2019-06-19 11:35:26 +00:00
|
|
|
|
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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""A mobile device consisting of smartphones, tablets, and cellphones."""
|
|
|
|
|
|
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)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
imei.comment = """The International Mobile Equipment Identity of
|
2019-05-03 12:31:49 +00:00
|
|
|
|
the smartphone as an integer.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2018-06-26 13:36:21 +00:00
|
|
|
|
meid = Column(Unicode)
|
2020-04-01 17:11:14 +00:00
|
|
|
|
meid.comment = """The Mobile Equipment Identifier as a hexadecimal
|
2019-05-03 12:31:49 +00:00
|
|
|
|
string.
|
2018-06-20 21:18:15 +00:00
|
|
|
|
"""
|
2020-02-18 12:50:48 +00:00
|
|
|
|
ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
|
2019-05-03 12:31:49 +00:00
|
|
|
|
ram_size.comment = """The total of RAM of the device in MB."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
data_storage_size = db.Column(
|
|
|
|
|
db.Integer, check_range('data_storage_size', 0, 10**8)
|
|
|
|
|
)
|
2020-02-18 12:50:48 +00:00
|
|
|
|
data_storage_size.comment = """The total of data storage of the device in MB"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
display_size = db.Column(
|
|
|
|
|
db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)
|
|
|
|
|
)
|
2020-03-03 11:03:09 +00:00
|
|
|
|
display_size.comment = """The total size of the device screen"""
|
2018-06-20 21:18:15 +00:00
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
@validates('imei')
|
|
|
|
|
def validate_imei(self, _, value: int):
|
2022-07-28 16:34:34 +00:00
|
|
|
|
if value and 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):
|
2022-07-28 16:34:34 +00:00
|
|
|
|
if value and not meid.is_valid(value):
|
2018-06-26 13:36:21 +00:00
|
|
|
|
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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""A device that can be inside another device."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
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
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
2022-04-28 15:46:12 +00:00
|
|
|
|
parent = relationship(
|
|
|
|
|
Computer,
|
|
|
|
|
backref=backref(
|
|
|
|
|
'components',
|
|
|
|
|
lazy=True,
|
|
|
|
|
cascade=CASCADE_DEL,
|
|
|
|
|
order_by=lambda: Component.id,
|
|
|
|
|
collection_class=OrderedSet,
|
|
|
|
|
),
|
|
|
|
|
primaryjoin=parent_id == Computer.id,
|
2019-02-07 12:47:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
2022-04-28 15:46:12 +00:00
|
|
|
|
__table_args__ = (db.Index('parent_index', parent_id, postgresql_using='hash'),)
|
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Gets a component that:
|
|
|
|
|
|
|
|
|
|
* has the same parent.
|
|
|
|
|
* Doesn't generate HID.
|
|
|
|
|
* Has same physical properties.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
: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'
|
2022-04-28 15:46:12 +00:00
|
|
|
|
component = (
|
|
|
|
|
self.__class__.query.filter_by(
|
|
|
|
|
parent=parent,
|
|
|
|
|
hid=None,
|
|
|
|
|
owner_id=self.owner_id,
|
|
|
|
|
**self.physical_properties,
|
|
|
|
|
)
|
|
|
|
|
.filter(~Component.id.in_(blacklist))
|
2018-04-27 17:16:43 +00:00
|
|
|
|
.first()
|
2022-04-28 15:46:12 +00:00
|
|
|
|
)
|
2018-04-27 17:16:43 +00:00
|
|
|
|
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
|
2019-05-11 14:27:22 +00:00
|
|
|
|
def actions(self) -> list:
|
|
|
|
|
return sorted(chain(super().actions, self.actions_components))
|
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))
|
2019-06-19 11:35:26 +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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""A device that stores information."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
|
|
|
|
size = Column(Integer, check_range('size', min=1, max=10**8))
|
2019-06-19 11:35:26 +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):
|
2018-12-30 11:43:29 +00:00
|
|
|
|
"""Returns the privacy compliance state of the data storage.
|
|
|
|
|
|
|
|
|
|
This is, the last erasure performed to the data storage.
|
|
|
|
|
"""
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import EraseBasic
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-11-09 10:22:13 +00:00
|
|
|
|
try:
|
2019-05-11 14:27:22 +00:00
|
|
|
|
ev = self.last_action_of(EraseBasic)
|
2018-11-09 10:22:13 +00:00
|
|
|
|
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:
|
2018-11-21 13:26:56 +00:00
|
|
|
|
v += ' – {} GB'.format(self.size // 1000 if self.size else '?')
|
2018-10-03 12:51:22 +00:00
|
|
|
|
return v
|
|
|
|
|
|
2021-07-28 13:59:11 +00:00
|
|
|
|
@property
|
|
|
|
|
def external_document_erasure(self):
|
2022-04-28 15:46:12 +00:00
|
|
|
|
"""Returns the external ``DataStorage`` proof of erasure."""
|
2021-07-29 10:45:43 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import DataWipe
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2021-07-28 13:59:11 +00:00
|
|
|
|
try:
|
2021-07-29 10:45:43 +00:00
|
|
|
|
ev = self.last_action_of(DataWipe)
|
2021-07-28 13:59:11 +00:00
|
|
|
|
return ev.document.url.to_text()
|
|
|
|
|
except LookupError:
|
|
|
|
|
return None
|
|
|
|
|
|
2022-11-21 12:24:56 +00:00
|
|
|
|
@property
|
|
|
|
|
def orphan(self):
|
|
|
|
|
if not self.parent:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if self.parent.placeholder and self.parent.placeholder.kangaroo:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if self.parent.binding and self.parent.binding.kangaroo:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
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))
|
2019-06-19 11:35:26 +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))
|
2022-03-29 11:34:01 +00:00
|
|
|
|
firewire = Column(SmallInteger, check_range('firewire', min=0))
|
2018-07-02 10:52:54 +00:00
|
|
|
|
serial = Column(SmallInteger, check_range('serial', min=0))
|
|
|
|
|
pcmcia = Column(SmallInteger, check_range('pcmcia', min=0))
|
2019-05-03 12:31:49 +00:00
|
|
|
|
bios_date = Column(db.Date)
|
|
|
|
|
bios_date.comment = """The date of the BIOS version."""
|
2019-06-29 14:26:14 +00:00
|
|
|
|
ram_slots = Column(db.SmallInteger, check_range('ram_slots'))
|
|
|
|
|
ram_max_size = Column(db.Integer, check_range('ram_max_size'))
|
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))
|
2020-04-01 17:11:14 +00:00
|
|
|
|
speed.comment = """The maximum speed this network adapter can handle,
|
2019-06-19 11:35:26 +00:00
|
|
|
|
in mbps.
|
2018-06-26 13:35:13 +00:00
|
|
|
|
"""
|
2018-10-13 12:53:46 +00:00
|
|
|
|
wireless = Column(Boolean, nullable=False, default=False)
|
2019-06-19 11:35:26 +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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""The CPU."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-05-11 16:58:48 +00:00
|
|
|
|
speed = Column(Float, check_range('speed', 0.1, 15))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
speed.comment = """The regular CPU speed."""
|
2018-05-11 16:58:48 +00:00
|
|
|
|
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
cores.comment = """The number of regular cores."""
|
2018-07-19 19:25:06 +00:00
|
|
|
|
threads = Column(SmallInteger, check_range('threads', 1, 20))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
threads.comment = """The number of threads per core."""
|
2018-05-11 16:58:48 +00:00
|
|
|
|
address = Column(SmallInteger, check_range('address', 8, 256))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits."""
|
2019-05-03 12:31:49 +00:00
|
|
|
|
abi = Column(Unicode, check_lower('abi'))
|
|
|
|
|
abi.comment = """The Application Binary Interface of the processor."""
|
2018-05-11 16:58:48 +00:00
|
|
|
|
|
|
|
|
|
|
2018-05-13 13:13:12 +00:00
|
|
|
|
class RamModule(JoinedComponentTableMixin, Component):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""A stick of RAM."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
2019-02-03 16:12:53 +00:00
|
|
|
|
size.comment = """The capacity of the RAM stick."""
|
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):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""The display of a device. This is used in all devices that have
|
2019-02-03 16:12:53 +00:00
|
|
|
|
displays but that it is not their main part, like laptops,
|
|
|
|
|
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
|
|
|
|
|
and ``TelevisionSet``.
|
2018-06-26 13:36:21 +00:00
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-06-26 13:36:21 +00:00
|
|
|
|
pass
|
2018-09-30 17:40:28 +00:00
|
|
|
|
|
|
|
|
|
|
2019-05-03 12:31:49 +00:00
|
|
|
|
class Battery(JoinedComponentTableMixin, Component):
|
|
|
|
|
wireless = db.Column(db.Boolean)
|
|
|
|
|
wireless.comment = """If the battery can be charged wirelessly."""
|
|
|
|
|
technology = db.Column(db.Enum(BatteryTechnology))
|
|
|
|
|
size = db.Column(db.Integer, nullable=False)
|
|
|
|
|
size.comment = """Maximum battery capacity by design, in mAh.
|
2020-04-01 17:11:14 +00:00
|
|
|
|
|
2019-05-03 12:31:49 +00:00
|
|
|
|
Use BatteryTest's "size" to get the actual size of the battery.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def capacity(self) -> float:
|
2022-04-28 15:46:12 +00:00
|
|
|
|
"""The quantity of"""
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import MeasureBattery
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
real_size = self.last_action_of(MeasureBattery).size
|
2019-05-03 12:31:49 +00:00
|
|
|
|
return real_size / self.size if real_size and self.size else None
|
|
|
|
|
|
|
|
|
|
|
2019-07-01 18:39:32 +00:00
|
|
|
|
class Camera(Component):
|
|
|
|
|
"""The camera of a device."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2019-07-01 18:39:32 +00:00
|
|
|
|
focal_length = db.Column(db.SmallInteger)
|
|
|
|
|
video_height = db.Column(db.SmallInteger)
|
|
|
|
|
video_width = db.Column(db.Integer)
|
|
|
|
|
horizontal_view_angle = db.Column(db.Integer)
|
|
|
|
|
facing = db.Column(db.Enum(CameraFacing))
|
|
|
|
|
vertical_view_angle = db.Column(db.SmallInteger)
|
|
|
|
|
video_stabilization = db.Column(db.Boolean)
|
|
|
|
|
flash = db.Column(db.Boolean)
|
|
|
|
|
|
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
class ComputerAccessory(Device):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Computer peripherals and similar accessories."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Routers, switches, hubs..."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
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):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Devices related to video treatment."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-10-23 13:37:37 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VideoScaler(Video):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Videoconference(Video):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-11-12 10:59:49 +00:00
|
|
|
|
class Cooking(Device):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""Cooking devices."""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2018-11-12 10:59:49 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mixer(Cooking):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class DIYAndGardening(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Drill(DIYAndGardening):
|
2019-07-01 18:39:32 +00:00
|
|
|
|
max_drill_bit_size = db.Column(db.SmallInteger)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PackOfScrewdrivers(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class Home(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Dehumidifier(Home):
|
2019-07-01 18:39:32 +00:00
|
|
|
|
size = db.Column(db.SmallInteger)
|
|
|
|
|
size.comment = """The capacity in Liters."""
|
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class Stairs(Home):
|
2019-07-01 18:39:32 +00:00
|
|
|
|
max_allowed_weight = db.Column(db.Integer)
|
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class Recreation(Device):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Bike(Recreation):
|
2019-07-01 18:39:32 +00:00
|
|
|
|
wheel_size = db.Column(db.SmallInteger)
|
|
|
|
|
gears = db.Column(db.SmallInteger)
|
|
|
|
|
|
|
|
|
|
|
2019-07-07 19:36:09 +00:00
|
|
|
|
class Racket(Recreation):
|
2019-07-01 18:39:32 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-09-30 17:40:28 +00:00
|
|
|
|
class Manufacturer(db.Model):
|
2019-02-03 16:12:53 +00:00
|
|
|
|
"""The normalized information about a manufacturer.
|
|
|
|
|
|
|
|
|
|
Ideally users should use the names from this list when submitting
|
|
|
|
|
devices.
|
|
|
|
|
"""
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
name = db.Column(CIText(), primary_key=True)
|
2019-02-03 16:12:53 +00:00
|
|
|
|
name.comment = """The normalized name of the manufacturer."""
|
2018-09-30 17:40:28 +00:00
|
|
|
|
url = db.Column(URL(), unique=True)
|
2019-02-03 16:12:53 +00:00
|
|
|
|
url.comment = """An URL to a page describing the manufacturer."""
|
2018-09-30 17:40:28 +00:00
|
|
|
|
logo = db.Column(URL())
|
2019-02-03 16:12:53 +00:00
|
|
|
|
logo.comment = """An URL pointing to the logo of the manufacturer."""
|
2018-09-30 17:40:28 +00:00
|
|
|
|
|
2019-02-07 12:47:42 +00:00
|
|
|
|
__table_args__ = (
|
|
|
|
|
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
|
|
|
|
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
2022-04-28 15:46:12 +00:00
|
|
|
|
{'schema': 'common'},
|
2019-02-07 12:47:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
2018-09-30 17:40:28 +00:00
|
|
|
|
@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:
|
2022-04-28 15:46:12 +00:00
|
|
|
|
cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
|
2020-10-16 14:25:46 +00:00
|
|
|
|
|
|
|
|
|
|
2020-10-27 20:29:38 +00:00
|
|
|
|
listener_reset_field_updated_in_actual_time(Device)
|
2021-03-03 19:05:48 +00:00
|
|
|
|
|
2021-10-22 17:26:27 +00:00
|
|
|
|
|
2021-10-26 14:35:41 +00:00
|
|
|
|
def create_code_tag(mapper, connection, device):
|
|
|
|
|
"""
|
|
|
|
|
This function create a new tag every time than one device is create.
|
|
|
|
|
this tag is the same of devicehub_id.
|
|
|
|
|
"""
|
|
|
|
|
from ereuse_devicehub.resources.tag.model import Tag
|
2022-04-28 15:46:12 +00:00
|
|
|
|
|
2022-07-15 11:11:04 +00:00
|
|
|
|
if isinstance(device, Computer) and not device.placeholder:
|
2022-03-03 09:43:34 +00:00
|
|
|
|
tag = Tag(device_id=device.id, id=device.devicehub_id)
|
|
|
|
|
db.session.add(tag)
|
2021-10-26 14:21:05 +00:00
|
|
|
|
|
|
|
|
|
|
2022-10-05 10:22:40 +00:00
|
|
|
|
# from flask_sqlalchemy import event
|
2022-07-20 08:33:44 +00:00
|
|
|
|
# event.listen(Device, 'after_insert', create_code_tag, propagate=True)
|
2022-11-29 15:49:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Other(Device):
|
|
|
|
|
"""
|
|
|
|
|
Used for put in there all devices than not have actualy a class
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|