Merge pull request #212 from eReuse/feature/server-side-render-parser-3021

Feature/server side render parser 3021
This commit is contained in:
Santiago L 2022-06-07 11:00:27 +02:00 committed by GitHub
commit 083e763b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 18712 additions and 1036 deletions

View File

View File

@ -0,0 +1,109 @@
import json
from binascii import Error as asciiError
from flask import Blueprint
from flask import current_app as app
from flask import g, jsonify, request
from flask.views import View
from flask.wrappers import Response
from marshmallow.exceptions import ValidationError
from werkzeug.exceptions import Unauthorized
from ereuse_devicehub.auth import Auth
from ereuse_devicehub.db import db
from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite
from ereuse_devicehub.resources.action.views.snapshot import (
SnapshotMixin,
move_json,
save_json,
)
from ereuse_devicehub.resources.enums import Severity
api = Blueprint('api', __name__, url_prefix='/api')
class LoginMixin(View):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.authenticate()
def authenticate(self):
unauthorized = Unauthorized('Provide a suitable token.')
basic_token = request.headers.get('Authorization', " ").split(" ")
if not len(basic_token) == 2:
raise unauthorized
token = basic_token[1]
try:
token = Auth.decode(token)
except asciiError:
raise unauthorized
self.user = Auth().authenticate(token)
g.user = self.user
class InventoryView(LoginMixin, SnapshotMixin):
methods = ['POST']
def dispatch_request(self):
snapshot_json = json.loads(request.data)
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
snapshot_json = self.validate(snapshot_json)
if type(snapshot_json) == Response:
return snapshot_json
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
snapshot = self.build()
db.session.add(snapshot)
snap_log = SnapshotsLog(
description='Ok',
snapshot_uuid=snapshot.uuid,
severity=Severity.Info,
sid=snapshot.sid,
version=str(snapshot.version),
snapshot=snapshot,
)
snap_log.save()
db.session().final_flush()
db.session.commit()
self.response = jsonify(
{
'url': snapshot.device.url.to_text(),
'dhid': snapshot.device.devicehub_id,
'sid': snapshot.sid,
}
)
self.response.status_code = 201
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
return self.response
def validate(self, snapshot_json):
self.schema = Snapshot_lite()
try:
return self.schema.load(snapshot_json)
except ValidationError as err:
txt = "{}".format(err)
uuid = snapshot_json.get('uuid')
sid = snapshot_json.get('sid')
version = snapshot_json.get('version')
error = SnapshotsLog(
description=txt,
snapshot_uuid=uuid,
severity=Severity.Error,
sid=sid,
version=str(version),
)
error.save(commit=True)
# raise err
self.response = jsonify(err)
self.response.status_code = 400
return self.response
api.add_url_rule('/inventory/', view_func=InventoryView.as_view('inventory'))

View File

@ -1,39 +1,49 @@
from distutils.version import StrictVersion from distutils.version import StrictVersion
from itertools import chain from itertools import chain
from typing import Set from typing import Set
from decouple import config
from decouple import config
from teal.auth import TokenAuth from teal.auth import TokenAuth
from teal.config import Config from teal.config import Config
from teal.enums import Currency from teal.enums import Currency
from teal.utils import import_resource from teal.utils import import_resource
from ereuse_devicehub.resources import action, agent, deliverynote, inventory, \ from ereuse_devicehub.resources import (
lot, tag, user action,
agent,
deliverynote,
inventory,
lot,
tag,
user,
)
from ereuse_devicehub.resources.device import definitions from ereuse_devicehub.resources.device import definitions
from ereuse_devicehub.resources.documents import documents from ereuse_devicehub.resources.documents import documents
from ereuse_devicehub.resources.tradedocument import definitions as tradedocument
from ereuse_devicehub.resources.enums import PriceSoftware from ereuse_devicehub.resources.enums import PriceSoftware
from ereuse_devicehub.resources.versions import versions
from ereuse_devicehub.resources.licences import licences from ereuse_devicehub.resources.licences import licences
from ereuse_devicehub.resources.metric import definitions as metric_def from ereuse_devicehub.resources.metric import definitions as metric_def
from ereuse_devicehub.resources.tradedocument import definitions as tradedocument
from ereuse_devicehub.resources.versions import versions
class DevicehubConfig(Config): class DevicehubConfig(Config):
RESOURCE_DEFINITIONS = set(chain(import_resource(definitions), RESOURCE_DEFINITIONS = set(
import_resource(action), chain(
import_resource(user), import_resource(definitions),
import_resource(tag), import_resource(action),
import_resource(agent), import_resource(user),
import_resource(lot), import_resource(tag),
import_resource(deliverynote), import_resource(agent),
import_resource(documents), import_resource(lot),
import_resource(tradedocument), import_resource(deliverynote),
import_resource(inventory), import_resource(documents),
import_resource(versions), import_resource(tradedocument),
import_resource(licences), import_resource(inventory),
import_resource(metric_def), import_resource(versions),
),) import_resource(licences),
import_resource(metric_def),
),
)
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SECRET_KEY = config('SECRET_KEY') SECRET_KEY = config('SECRET_KEY')
DB_USER = config('DB_USER', 'dhub') DB_USER = config('DB_USER', 'dhub')
@ -48,11 +58,12 @@ class DevicehubConfig(Config):
db=DB_DATABASE, db=DB_DATABASE,
) # type: str ) # type: str
SCHEMA = config('SCHEMA', 'dbtest') SCHEMA = config('SCHEMA', 'dbtest')
HOST = config('HOST', 'localhost') HOST = config('HOST', 'localhost')
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
"""The minimum version of ereuse.org workbench that this devicehub """The minimum version of ereuse.org workbench that this devicehub
accepts. we recommend not changing this value. accepts. we recommend not changing this value.
""" """
SCHEMA_WORKBENCH = ["1.0.0"]
TMP_SNAPSHOTS = config('TMP_SNAPSHOTS', '/tmp/snapshots') TMP_SNAPSHOTS = config('TMP_SNAPSHOTS', '/tmp/snapshots')
TMP_LIVES = config('TMP_LIVES', '/tmp/lives') TMP_LIVES = config('TMP_LIVES', '/tmp/lives')
@ -60,11 +71,7 @@ class DevicehubConfig(Config):
"""This var is for save a snapshots in json format when fail something""" """This var is for save a snapshots in json format when fail something"""
API_DOC_CONFIG_TITLE = 'Devicehub' API_DOC_CONFIG_TITLE = 'Devicehub'
API_DOC_CONFIG_VERSION = '0.2' API_DOC_CONFIG_VERSION = '0.2'
API_DOC_CONFIG_COMPONENTS = { API_DOC_CONFIG_COMPONENTS = {'securitySchemes': {'bearerAuth': TokenAuth.API_DOCS}}
'securitySchemes': {
'bearerAuth': TokenAuth.API_DOCS
}
}
API_DOC_CLASS_DISCRIMINATOR = 'type' API_DOC_CLASS_DISCRIMINATOR = 'type'
PRICE_SOFTWARE = PriceSoftware.Ereuse PRICE_SOFTWARE = PriceSoftware.Ereuse

View File

@ -4,8 +4,10 @@ import json
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from boltons.urlutils import URL from boltons.urlutils import URL
from flask import current_app as app
from flask import g, request from flask import g, request
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from marshmallow import ValidationError
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from wtforms import ( from wtforms import (
@ -26,14 +28,20 @@ from wtforms import (
from wtforms.fields import FormField from wtforms.fields import FormField
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import RateComputer, Snapshot, Trade from ereuse_devicehub.inventory.models import DeliveryNote, ReceiverNote, Transfer
from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite
from ereuse_devicehub.resources.action.models import Snapshot, Trade
from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema
from ereuse_devicehub.resources.action.views.snapshot import move_json, save_json from ereuse_devicehub.resources.action.views.snapshot import (
SnapshotMixin,
move_json,
save_json,
)
from ereuse_devicehub.resources.device.models import ( from ereuse_devicehub.resources.device.models import (
SAI, SAI,
Cellphone, Cellphone,
Computer,
ComputerMonitor, ComputerMonitor,
Device, Device,
Keyboard, Keyboard,
@ -44,12 +52,11 @@ from ereuse_devicehub.resources.device.models import (
) )
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.documents.models import DataWipeDocument
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware from ereuse_devicehub.resources.enums import Severity
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user.exceptions import InsufficientPermission
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
DEVICES = { DEVICES = {
@ -76,7 +83,7 @@ DEVICES = {
], ],
} }
COMPUTERS = ['Desktop', 'Laptop', 'Server'] COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"] MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"] MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"]
@ -197,7 +204,7 @@ class LotForm(FlaskForm):
return self.instance return self.instance
class UploadSnapshotForm(FlaskForm): class UploadSnapshotForm(SnapshotMixin, FlaskForm):
snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()]) snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()])
def validate(self, extra_validators=None): def validate(self, extra_validators=None):
@ -237,20 +244,58 @@ class UploadSnapshotForm(FlaskForm):
return True return True
def is_wb_lite_snapshot(self, version: str) -> bool:
is_lite = False
if version in app.config['SCHEMA_WORKBENCH']:
is_lite = True
return is_lite
def save(self, commit=True): def save(self, commit=True):
if any([x == 'Error' for x in self.result.values()]): if any([x == 'Error' for x in self.result.values()]):
return return
# result = []
self.sync = Sync()
schema = SnapshotSchema() schema = SnapshotSchema()
# self.tmp_snapshots = app.config['TMP_SNAPSHOTS'] schema_lite = Snapshot_lite()
# TODO @cayop get correct var config devices = []
self.tmp_snapshots = '/tmp/' self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
for filename, snapshot_json in self.snapshots: for filename, snapshot_json in self.snapshots:
path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email) path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
snapshot_json.pop('debug', None) snapshot_json.pop('debug', None)
snapshot_json = schema.load(snapshot_json) version = snapshot_json.get('schema_api')
uuid = snapshot_json.get('uuid')
sid = snapshot_json.get('sid')
software_version = snapshot_json.get('version')
if self.is_wb_lite_snapshot(version):
self.snapshot_json = schema_lite.load(snapshot_json)
snapshot_json = ParseSnapshotLsHw(self.snapshot_json).snapshot_json
try:
snapshot_json = schema.load(snapshot_json)
except ValidationError as err:
txt = "{}".format(err)
error = SnapshotsLog(
description=txt,
snapshot_uuid=uuid,
severity=Severity.Error,
sid=sid,
version=software_version,
)
error.save(commit=True)
self.result[filename] = 'Error'
continue
response = self.build(snapshot_json) response = self.build(snapshot_json)
db.session.add(response)
devices.append(response.device)
snap_log = SnapshotsLog(
description='Ok',
snapshot_uuid=uuid,
severity=Severity.Info,
sid=sid,
version=software_version,
snapshot=response,
)
snap_log.save()
if hasattr(response, 'type'): if hasattr(response, 'type'):
self.result[filename] = 'Ok' self.result[filename] = 'Ok'
@ -261,69 +306,7 @@ class UploadSnapshotForm(FlaskForm):
if commit: if commit:
db.session.commit() db.session.commit()
return response return self.result, devices
def build(self, snapshot_json): # noqa: C901
# this is a copy adaptated from ereuse_devicehub.resources.action.views.snapshot
device = snapshot_json.pop('device') # type: Computer
components = None
if snapshot_json['software'] == (
SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid
):
components = snapshot_json.pop('components', None)
if isinstance(device, Computer) and device.hid:
device.add_mac_to_hid(components_snap=components)
snapshot = Snapshot(**snapshot_json)
# Remove new actions from devices so they don't interfere with sync
actions_device = set(e for e in device.actions_one)
device.actions_one.clear()
if components:
actions_components = tuple(
set(e for e in c.actions_one) for c in components
)
for component in components:
component.actions_one.clear()
assert not device.actions_one
assert all(not c.actions_one for c in components) if components else True
db_device, remove_actions = self.sync.run(device, components)
del device # Do not use device anymore
snapshot.device = db_device
snapshot.actions |= remove_actions | actions_device # Set actions to snapshot
# commit will change the order of the components by what
# the DB wants. Let's get a copy of the list so we preserve order
ordered_components = OrderedSet(x for x in snapshot.components)
# Add the new actions to the db-existing devices and components
db_device.actions_one |= actions_device
if components:
for component, actions in zip(ordered_components, actions_components):
component.actions_one |= actions
snapshot.actions |= actions
if snapshot.software == SnapshotSoftware.Workbench:
# Check ownership of (non-component) device to from current.user
if db_device.owner_id != g.user.id:
raise InsufficientPermission()
# Compute ratings
try:
rate_computer, price = RateComputer.compute(db_device)
except CannotRate:
pass
else:
snapshot.actions.add(rate_computer)
if price:
snapshot.actions.add(price)
elif snapshot.software == SnapshotSoftware.WorkbenchAndroid:
pass # TODO try except to compute RateMobile
# Check if HID is null and add Severity:Warning to Snapshot
if snapshot.device.hid is None:
snapshot.severity = Severity.Warning
db.session.add(snapshot)
return snapshot
class NewDeviceForm(FlaskForm): class NewDeviceForm(FlaskForm):
@ -559,7 +542,7 @@ class TagDeviceForm(FlaskForm):
db.session.commit() db.session.commit()
class ActionFormMix(FlaskForm): class ActionFormMixin(FlaskForm):
name = StringField( name = StringField(
'Name', 'Name',
[validators.length(max=50)], [validators.length(max=50)],
@ -640,7 +623,7 @@ class ActionFormMix(FlaskForm):
return self.type.data return self.type.data
class NewActionForm(ActionFormMix): class NewActionForm(ActionFormMixin):
def validate(self, extra_validators=None): def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -653,7 +636,7 @@ class NewActionForm(ActionFormMix):
return True return True
class AllocateForm(ActionFormMix): class AllocateForm(ActionFormMixin):
date = HiddenField('') date = HiddenField('')
start_time = DateField('Start time') start_time = DateField('Start time')
end_time = DateField('End time', [validators.Optional()]) end_time = DateField('End time', [validators.Optional()])
@ -854,7 +837,7 @@ class DataWipeDocumentForm(Form):
return self._obj return self._obj
class DataWipeForm(ActionFormMix): class DataWipeForm(ActionFormMixin):
document = FormField(DataWipeDocumentForm) document = FormField(DataWipeDocumentForm)
def save(self): def save(self):
@ -881,7 +864,7 @@ class DataWipeForm(ActionFormMix):
return self.instance return self.instance
class TradeForm(ActionFormMix): class TradeForm(ActionFormMixin):
user_from = StringField( user_from = StringField(
'Supplier', 'Supplier',
[validators.Optional()], [validators.Optional()],
@ -1118,3 +1101,208 @@ class TradeDocumentForm(FlaskForm):
db.session.commit() db.session.commit()
return self._obj return self._obj
class TransferForm(FlaskForm):
code = StringField(
'Code',
[validators.DataRequired()],
render_kw={'class': "form-control"},
description="You need put a code for transfer the external user",
)
description = TextAreaField(
'Description',
[validators.Optional()],
render_kw={'class': "form-control"},
)
type = HiddenField()
def __init__(self, *args, **kwargs):
self._type = kwargs.get('type')
lot_id = kwargs.pop('lot_id', None)
self._tmp_lot = Lot.query.filter(Lot.id == lot_id).one()
super().__init__(*args, **kwargs)
self._obj = None
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not self._tmp_lot:
return False
if self._type and self.type.data not in ['incoming', 'outgoing']:
return False
if self._obj and self.date.data:
if self.date.data > datetime.datetime.now().date():
return False
return is_valid
def save(self, commit=True):
self.set_obj()
db.session.add(self._obj)
if commit:
db.session.commit()
return self._obj
def set_obj(self):
self.newlot = Lot(name=self._tmp_lot.name)
self.newlot.devices = self._tmp_lot.devices
db.session.add(self.newlot)
self._obj = Transfer(lot=self.newlot)
self.populate_obj(self._obj)
if self.type.data == 'incoming':
self._obj.user_to = g.user
elif self.type.data == 'outgoing':
self._obj.user_from = g.user
class EditTransferForm(TransferForm):
date = DateField(
'Date',
[validators.Optional()],
render_kw={'class': "form-control"},
description="""Date when the transfer is closed""",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.type
self._obj = self._tmp_lot.transfer
if not self.data['csrf_token']:
self.code.data = self._obj.code
self.description.data = self._obj.description
self.date.data = self._obj.date
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
date = self.date.data
if date and date > datetime.datetime.now().date():
self.date.errors = ["You have to choose a date before today."]
is_valid = False
return is_valid
def set_obj(self, commit=True):
self.populate_obj(self._obj)
class NotesForm(FlaskForm):
number = StringField(
'Number',
[validators.Optional()],
render_kw={'class': "form-control"},
description="You can put a number for tracer of receiver or delivery",
)
date = DateField(
'Date',
[validators.Optional()],
render_kw={'class': "form-control"},
description="""Date when the transfer was do it""",
)
units = IntegerField(
'Units',
[validators.Optional()],
render_kw={'class': "form-control"},
description="Number of units",
)
weight = IntegerField(
'Weight',
[validators.Optional()],
render_kw={'class': "form-control"},
description="Weight expressed in Kg",
)
def __init__(self, *args, **kwargs):
self.type = kwargs.pop('type', None)
lot_id = kwargs.pop('lot_id', None)
self._tmp_lot = Lot.query.filter(Lot.id == lot_id).one()
self._obj = None
super().__init__(*args, **kwargs)
if self._tmp_lot.transfer:
if self.type == 'Delivery':
self._obj = self._tmp_lot.transfer.delivery_note
if not self._obj:
self._obj = DeliveryNote(transfer_id=self._tmp_lot.transfer.id)
self.date.description = """Date when the delivery was do it."""
self.number.description = (
"""You can put a number for tracer of delivery note."""
)
if self.type == 'Receiver':
self._obj = self._tmp_lot.transfer.receiver_note
if not self._obj:
self._obj = ReceiverNote(transfer_id=self._tmp_lot.transfer.id)
self.date.description = """Date when the receipt was do it."""
self.number.description = (
"""You can put a number for tracer of receiber note."""
)
if self.is_editable():
self.number.render_kw.pop('disabled', None)
self.date.render_kw.pop('disabled', None)
self.units.render_kw.pop('disabled', None)
self.weight.render_kw.pop('disabled', None)
else:
disabled = {'disabled': "disabled"}
self.number.render_kw.update(disabled)
self.date.render_kw.update(disabled)
self.units.render_kw.update(disabled)
self.weight.render_kw.update(disabled)
if self._obj and not self.data['csrf_token']:
self.number.data = self._obj.number
self.date.data = self._obj.date
self.units.data = self._obj.units
self.weight.data = self._obj.weight
def is_editable(self):
if not self._tmp_lot.transfer:
return False
if self._tmp_lot.transfer.closed:
return False
if self._tmp_lot.transfer.code:
return True
if self._tmp_lot.transfer.user_from == g.user and self.type == 'Receiver':
return False
if self._tmp_lot.transfer.user_to == g.user and self.type == 'Delivery':
return False
return True
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
date = self.date.data
if date and date > datetime.datetime.now().date():
self.date.errors = ["You have to choose a date before today."]
is_valid = False
if not self.is_editable():
is_valid = False
return is_valid
def save(self, commit=True):
if self._tmp_lot.transfer.closed:
return self._obj
self.populate_obj(self._obj)
db.session.add(self._obj)
if commit:
db.session.commit()
return self._obj

View File

@ -0,0 +1,82 @@
from uuid import uuid4
from citext import CIText
from sqlalchemy import Column, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import backref, relationship
from teal.db import CASCADE_OWN
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User
class Transfer(Thing):
"""
The transfer is a transfer of possession of devices between
a user and a code (not system user)
"""
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
code = Column(CIText(), default='', nullable=False)
date = Column(db.TIMESTAMP(timezone=True))
description = Column(CIText(), default='', nullable=True)
lot_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey('lot.id', use_alter=True, name='lot_transfer'),
nullable=False,
)
lot = relationship(
'Lot',
backref=backref('transfer', lazy=True, uselist=False, cascade=CASCADE_OWN),
primaryjoin='Transfer.lot_id == Lot.id',
)
user_from_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
user_from = db.relationship(User, primaryjoin=user_from_id == User.id)
user_to_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
user_to = db.relationship(User, primaryjoin=user_to_id == User.id)
@property
def closed(self):
if self.date:
return True
return False
class DeliveryNote(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
number = Column(CIText(), default='', nullable=False)
date = Column(db.TIMESTAMP(timezone=True))
units = Column(Integer, default=0)
weight = Column(Integer, default=0)
transfer_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey('transfer.id'),
nullable=False,
)
transfer = relationship(
'Transfer',
backref=backref('delivery_note', lazy=True, uselist=False, cascade=CASCADE_OWN),
primaryjoin='DeliveryNote.transfer_id == Transfer.id',
)
class ReceiverNote(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
number = Column(CIText(), default='', nullable=False)
date = Column(db.TIMESTAMP(timezone=True))
units = Column(Integer, default=0)
weight = Column(Integer, default=0)
transfer_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey('transfer.id'),
nullable=False,
)
transfer = relationship(
'Transfer',
backref=backref('receiver_note', lazy=True, uselist=False, cascade=CASCADE_OWN),
primaryjoin='ReceiverNote.transfer_id == Transfer.id',
)

View File

@ -16,30 +16,34 @@ from ereuse_devicehub.inventory.forms import (
AdvancedSearchForm, AdvancedSearchForm,
AllocateForm, AllocateForm,
DataWipeForm, DataWipeForm,
EditTransferForm,
FilterForm, FilterForm,
LotForm, LotForm,
NewActionForm, NewActionForm,
NewDeviceForm, NewDeviceForm,
NotesForm,
TagDeviceForm, TagDeviceForm,
TradeDocumentForm, TradeDocumentForm,
TradeForm, TradeForm,
TransferForm,
UploadSnapshotForm, UploadSnapshotForm,
) )
from ereuse_devicehub.labels.forms import PrintLabelsForm from ereuse_devicehub.labels.forms import PrintLabelsForm
from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.views import GenericMixView from ereuse_devicehub.views import GenericMixin
devices = Blueprint('inventory', __name__, url_prefix='/inventory') devices = Blueprint('inventory', __name__, url_prefix='/inventory')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DeviceListMix(GenericMixView): class DeviceListMixin(GenericMixin):
template_name = 'inventory/device_list.html' template_name = 'inventory/device_list.html'
def get_context(self, lot_id, only_unassigned=True): def get_context(self, lot_id, only_unassigned=True):
@ -48,16 +52,16 @@ class DeviceListMix(GenericMixView):
form_filter = FilterForm(lots, lot_id, only_unassigned=only_unassigned) form_filter = FilterForm(lots, lot_id, only_unassigned=only_unassigned)
devices = form_filter.search() devices = form_filter.search()
lot = None lot = None
form_transfer = ''
form_delivery = ''
form_receiver = ''
if lot_id: if lot_id:
lot = lots.filter(Lot.id == lot_id).one() lot = lots.filter(Lot.id == lot_id).one()
form_new_trade = TradeForm( if not lot.is_temporary and lot.transfer:
lot=lot.id, form_transfer = EditTransferForm(lot_id=lot.id)
user_to=g.user.email, form_delivery = NotesForm(lot_id=lot.id, type='Delivery')
user_from=g.user.email, form_receiver = NotesForm(lot_id=lot.id, type='Receiver')
)
else:
form_new_trade = ''
form_new_action = NewActionForm(lot=lot_id) form_new_action = NewActionForm(lot=lot_id)
self.context.update( self.context.update(
@ -67,7 +71,9 @@ class DeviceListMix(GenericMixView):
'form_new_action': form_new_action, 'form_new_action': form_new_action,
'form_new_allocate': AllocateForm(lot=lot_id), 'form_new_allocate': AllocateForm(lot=lot_id),
'form_new_datawipe': DataWipeForm(lot=lot_id), 'form_new_datawipe': DataWipeForm(lot=lot_id),
'form_new_trade': form_new_trade, 'form_transfer': form_transfer,
'form_delivery': form_delivery,
'form_receiver': form_receiver,
'form_filter': form_filter, 'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(), 'form_print_labels': PrintLabelsForm(),
'lot': lot, 'lot': lot,
@ -94,7 +100,7 @@ class DeviceListMix(GenericMixView):
return [] return []
class DeviceListView(DeviceListMix): class DeviceListView(DeviceListMixin):
def dispatch_request(self, lot_id=None): def dispatch_request(self, lot_id=None):
only_unassigned = request.args.get( only_unassigned = request.args.get(
'only_unassigned', default=True, type=strtobool 'only_unassigned', default=True, type=strtobool
@ -103,7 +109,7 @@ class DeviceListView(DeviceListMix):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class AdvancedSearchView(DeviceListMix): class AdvancedSearchView(DeviceListMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
template_name = 'inventory/search.html' template_name = 'inventory/search.html'
title = "Advanced Search" title = "Advanced Search"
@ -116,7 +122,7 @@ class AdvancedSearchView(DeviceListMix):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class DeviceDetailView(GenericMixView): class DeviceDetailView(GenericMixin):
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/device_detail.html' template_name = 'inventory/device_detail.html'
@ -137,7 +143,7 @@ class DeviceDetailView(GenericMixView):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class LotCreateView(GenericMixView): class LotCreateView(GenericMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/lot.html' template_name = 'inventory/lot.html'
@ -160,7 +166,7 @@ class LotCreateView(GenericMixView):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class LotUpdateView(GenericMixView): class LotUpdateView(GenericMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/lot.html' template_name = 'inventory/lot.html'
@ -201,7 +207,7 @@ class LotDeleteView(View):
return flask.redirect(next_url) return flask.redirect(next_url)
class UploadSnapshotView(GenericMixView): class UploadSnapshotView(GenericMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/upload_snapshot.html' template_name = 'inventory/upload_snapshot.html'
@ -217,18 +223,19 @@ class UploadSnapshotView(GenericMixView):
} }
) )
if form.validate_on_submit(): if form.validate_on_submit():
snapshot = form.save(commit=False) snapshot, devices = form.save(commit=False)
if lot_id: if lot_id:
lots = self.context['lots'] lots = self.context['lots']
lot = lots.filter(Lot.id == lot_id).one() lot = lots.filter(Lot.id == lot_id).one()
lot.devices.add(snapshot.device) for dev in devices:
lot.devices.add(dev)
db.session.add(lot) db.session.add(lot)
db.session.commit() db.session.commit()
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class DeviceCreateView(GenericMixView): class DeviceCreateView(GenericMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/device_create.html' template_name = 'inventory/device_create.html'
@ -273,7 +280,7 @@ class TagLinkDeviceView(View):
return flask.redirect(request.referrer) return flask.redirect(request.referrer)
class TagUnlinkDeviceView(GenericMixView): class TagUnlinkDeviceView(GenericMixin):
methods = ['POST', 'GET'] methods = ['POST', 'GET']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/tag_unlink_device.html' template_name = 'inventory/tag_unlink_device.html'
@ -326,7 +333,7 @@ class NewActionView(View):
return url_for('inventory.devicelist') return url_for('inventory.devicelist')
class NewAllocateView(NewActionView, DeviceListMix): class NewAllocateView(DeviceListMixin, NewActionView):
methods = ['POST'] methods = ['POST']
form_class = AllocateForm form_class = AllocateForm
@ -351,7 +358,7 @@ class NewAllocateView(NewActionView, DeviceListMix):
return flask.redirect(next_url) return flask.redirect(next_url)
class NewDataWipeView(NewActionView, DeviceListMix): class NewDataWipeView(DeviceListMixin, NewActionView):
methods = ['POST'] methods = ['POST']
form_class = DataWipeForm form_class = DataWipeForm
@ -372,7 +379,7 @@ class NewDataWipeView(NewActionView, DeviceListMix):
return flask.redirect(next_url) return flask.redirect(next_url)
class NewTradeView(NewActionView, DeviceListMix): class NewTradeView(DeviceListMixin, NewActionView):
methods = ['POST'] methods = ['POST']
form_class = TradeForm form_class = TradeForm
@ -414,6 +421,52 @@ class NewTradeDocumentView(View):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class NewTransferView(GenericMixin):
methods = ['POST', 'GET']
template_name = 'inventory/new_transfer.html'
form_class = TransferForm
title = "Add new transfer"
def dispatch_request(self, lot_id, type_id):
self.form = self.form_class(lot_id=lot_id, type=type_id)
self.get_context()
if self.form.validate_on_submit():
self.form.save()
new_lot_id = lot_id
if self.form.newlot.id:
new_lot_id = "{}".format(self.form.newlot.id)
Lot.query.filter(Lot.id == new_lot_id).one()
messages.success('Transfer created successfully!')
next_url = url_for('inventory.lotdevicelist', lot_id=str(new_lot_id))
return flask.redirect(next_url)
self.context.update({'form': self.form, 'title': self.title})
return flask.render_template(self.template_name, **self.context)
class EditTransferView(GenericMixin):
methods = ['POST']
form_class = EditTransferForm
def dispatch_request(self, lot_id):
self.get_context()
form = self.form_class(request.form, lot_id=lot_id)
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
if form.validate_on_submit():
form.save()
messages.success('Transfer updated successfully!')
return flask.redirect(next_url)
messages.error('Transfer updated error!')
for k, v in form.errors.items():
value = ';'.join(v)
key = form[k].label.text
messages.error('Error {key}: {value}!'.format(key=key, value=value))
return flask.redirect(next_url)
class ExportsView(View): class ExportsView(View):
methods = ['GET'] methods = ['GET']
decorators = [login_required] decorators = [login_required]
@ -525,6 +578,114 @@ class ExportsView(View):
return flask.render_template('inventory/erasure.html', **params) return flask.render_template('inventory/erasure.html', **params)
class SnapshotListView(GenericMixin):
template_name = 'inventory/snapshots_list.html'
def dispatch_request(self):
self.get_context()
self.context['page_title'] = "Snapshots Logs"
self.context['snapshots_log'] = self.get_snapshots_log()
return flask.render_template(self.template_name, **self.context)
def get_snapshots_log(self):
snapshots_log = SnapshotsLog.query.filter(
SnapshotsLog.owner == g.user
).order_by(SnapshotsLog.created.desc())
logs = {}
for snap in snapshots_log:
if snap.snapshot_uuid not in logs:
logs[snap.snapshot_uuid] = {
'sid': snap.sid,
'snapshot_uuid': snap.snapshot_uuid,
'version': snap.version,
'device': snap.get_device(),
'status': snap.get_status(),
'severity': snap.severity,
'created': snap.created,
}
continue
if snap.created > logs[snap.snapshot_uuid]['created']:
logs[snap.snapshot_uuid]['created'] = snap.created
if snap.severity > logs[snap.snapshot_uuid]['severity']:
logs[snap.snapshot_uuid]['severity'] = snap.severity
logs[snap.snapshot_uuid]['status'] = snap.get_status()
result = sorted(logs.values(), key=lambda d: d['created'])
result.reverse()
return result
class SnapshotDetailView(GenericMixin):
template_name = 'inventory/snapshot_detail.html'
def dispatch_request(self, snapshot_uuid):
self.snapshot_uuid = snapshot_uuid
self.get_context()
self.context['page_title'] = "Snapshot Detail"
self.context['snapshots_log'] = self.get_snapshots_log()
self.context['snapshot_uuid'] = snapshot_uuid
self.context['snapshot_sid'] = ''
if self.context['snapshots_log'].count():
self.context['snapshot_sid'] = self.context['snapshots_log'][0].sid
return flask.render_template(self.template_name, **self.context)
def get_snapshots_log(self):
return (
SnapshotsLog.query.filter(SnapshotsLog.owner == g.user)
.filter(SnapshotsLog.snapshot_uuid == self.snapshot_uuid)
.order_by(SnapshotsLog.created.desc())
)
class DeliveryNoteView(GenericMixin):
methods = ['POST']
form_class = NotesForm
def dispatch_request(self, lot_id):
self.get_context()
form = self.form_class(request.form, lot_id=lot_id, type='Delivery')
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
if form.validate_on_submit():
form.save()
messages.success('Delivery Note updated successfully!')
return flask.redirect(next_url)
messages.error('Delivery Note updated error!')
for k, v in form.errors.items():
value = ';'.join(v)
key = form[k].label.text
messages.error('Error {key}: {value}!'.format(key=key, value=value))
return flask.redirect(next_url)
class ReceiverNoteView(GenericMixin):
methods = ['POST']
form_class = NotesForm
def dispatch_request(self, lot_id):
self.get_context()
form = self.form_class(request.form, lot_id=lot_id, type='Receiver')
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
if form.validate_on_submit():
form.save()
messages.success('Receiver Note updated successfully!')
return flask.redirect(next_url)
messages.error('Receiver Note updated error!')
for k, v in form.errors.items():
value = ';'.join(v)
key = form[k].label.text
messages.error('Error {key}: {value}!'.format(key=key, value=value))
return flask.redirect(next_url)
devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add'))
devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add')) devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add'))
devices.add_url_rule( devices.add_url_rule(
@ -574,3 +735,24 @@ devices.add_url_rule(
devices.add_url_rule( devices.add_url_rule(
'/export/<string:export_id>/', view_func=ExportsView.as_view('export') '/export/<string:export_id>/', view_func=ExportsView.as_view('export')
) )
devices.add_url_rule('/snapshots/', view_func=SnapshotListView.as_view('snapshotslist'))
devices.add_url_rule(
'/snapshots/<string:snapshot_uuid>/',
view_func=SnapshotDetailView.as_view('snapshot_detail'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/transfer/<string:type_id>/',
view_func=NewTransferView.as_view('new_transfer'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/transfer/',
view_func=EditTransferView.as_view('edit_transfer'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/deliverynote/',
view_func=DeliveryNoteView.as_view('delivery_note'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/receivernote/',
view_func=ReceiverNoteView.as_view('receiver_note'),
)

View File

@ -0,0 +1,124 @@
"""transfer
Revision ID: 054a3aea9f08
Revises: 926865284103
Create Date: 2022-05-27 11:07:18.245322
"""
from uuid import uuid4
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '054a3aea9f08'
down_revision = '926865284103'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade_datas():
sql = f'select user_from_id, user_to_id, lot_id, code from {get_inv()}.trade where confirm=False'
con = op.get_bind()
sql_phantom = 'select id from common.user where phantom=True'
phantoms = [x[0] for x in con.execute(sql_phantom)]
for ac in con.execute(sql):
id = uuid4()
user_from = ac.user_from_id
user_to = ac.user_to_id
lot = ac.lot_id
code = ac.code
columns = '(id, user_from_id, user_to_id, lot_id, code)'
values = f'(\'{id}\', \'{user_from}\', \'{user_to}\', \'{lot}\', \'{code}\')'
if user_to not in phantoms:
columns = '(id, user_to_id, lot_id, code)'
values = f'(\'{id}\', \'{user_to}\', \'{lot}\', \'{code}\')'
if user_from not in phantoms:
columns = '(id, user_from_id, lot_id, code)'
values = f'(\'{id}\', \'{user_from}\', \'{lot}\', \'{code}\')'
new_transfer = f'insert into {get_inv()}.transfer {columns} values {values}'
op.execute(new_transfer)
def upgrade():
# creating transfer table
op.create_table(
'transfer',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('code', citext.CIText(), nullable=False),
sa.Column(
'description',
citext.CIText(),
nullable=True,
comment='A comment about the action.',
),
sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_to_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('user_from_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id']),
sa.ForeignKeyConstraint(['user_from_id'], ['common.user.id']),
sa.ForeignKeyConstraint(['user_to_id'], ['common.user.id']),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
# creating index
op.create_index(
op.f('ix_transfer_created'),
'transfer',
['created'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
op.f('ix_transfer_updated'),
'transfer',
['updated'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
'ix_transfer_id',
'transfer',
['id'],
unique=False,
postgresql_using='hash',
schema=f'{get_inv()}',
)
upgrade_datas()
def downgrade():
op.drop_index(
op.f('ix_transfer_created'), table_name='transfer', schema=f'{get_inv()}'
)
op.drop_index(
op.f('ix_transfer_updated'), table_name='transfer', schema=f'{get_inv()}'
)
op.drop_index(op.f('ix_transfer_id'), table_name='transfer', schema=f'{get_inv()}')
op.drop_table('transfer', schema=f'{get_inv()}')

View File

@ -0,0 +1,42 @@
"""change firewire
Revision ID: 17288b2a7440
Revises: 8571fb32c912
Create Date: 2022-03-29 11:49:39.270791
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '17288b2a7440'
down_revision = '8571fb32c912'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.add_column(
'computer',
sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True),
schema=f'{get_inv()}',
)
op.add_column(
'snapshot',
sa.Column('wbid', citext.CIText(), nullable=True),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_column('computer', 'uuid', schema=f'{get_inv()}')
op.drop_column('snapshot', 'wbid', schema=f'{get_inv()}')

View File

@ -0,0 +1,56 @@
"""add snapshot errors
Revision ID: 23d9e7ebbd7d
Revises: 17288b2a7440
Create Date: 2022-04-04 19:27:48.675387
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '23d9e7ebbd7d'
down_revision = '17288b2a7440'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'snapshot_errors',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='The last time Devicehub recorded a change for \n this thing.\n ',
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='When Devicehub created this.',
),
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('description', citext.CIText(), nullable=False),
sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('severity', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
op.execute(f"CREATE SEQUENCE {get_inv()}.snapshot_errors_seq START 1;")
def downgrade():
op.drop_table('snapshot_errors', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.snapshot_errors_seq;")

View File

@ -0,0 +1,66 @@
"""change wbid for sid
Revision ID: 6f6771813f2e
Revises: 97bef94f7982
Create Date: 2022-04-25 10:52:11.767569
"""
import citext
import sqlalchemy as sa
from alembic import context, op
# revision identifiers, used by Alembic.
revision = '6f6771813f2e'
down_revision = '97bef94f7982'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade_datas():
con = op.get_bind()
sql = f"select * from {get_inv()}.snapshot;"
snapshots = con.execute(sql)
for snap in snapshots:
wbid = snap.wbid
if wbid:
sql = f"""update {get_inv()}.snapshot set sid='{wbid}'
where wbid='{wbid}';"""
con.execute(sql)
sql = f"select wbid from {get_inv()}.snapshot_errors;"
snapshots = con.execute(sql)
for snap in snapshots:
wbid = snap.wbid
if wbid:
sql = f"""update {get_inv()}.snapshot set sid='{wbid}'
where wbid='{wbid}';"""
con.execute(sql)
def upgrade():
op.add_column(
'snapshot',
sa.Column('sid', citext.CIText(), nullable=True),
schema=f'{get_inv()}',
)
op.add_column(
'snapshot_errors',
sa.Column('sid', citext.CIText(), nullable=True),
schema=f'{get_inv()}',
)
upgrade_datas()
op.drop_column('snapshot', 'wbid', schema=f'{get_inv()}')
op.drop_column('snapshot_errors', 'wbid', schema=f'{get_inv()}')
def downgrade():
op.drop_column('snapshot', 'sid', schema=f'{get_inv()}')
op.drop_column('snapshot_errors', 'sid', schema=f'{get_inv()}')

View File

@ -0,0 +1,102 @@
"""snapshot_log
Revision ID: 926865284103
Revises: 6f6771813f2e
Create Date: 2022-05-17 17:57:46.651106
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '926865284103'
down_revision = '6f6771813f2e'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'snapshots_log',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='The last time Devicehub recorded a change for \n this thing.\n ',
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='When Devicehub created this.',
),
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('description', citext.CIText(), nullable=True),
sa.Column('version', citext.CIText(), nullable=True),
sa.Column('sid', citext.CIText(), nullable=True),
sa.Column('severity', sa.SmallInteger(), nullable=False),
sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('snapshot_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
['snapshot_id'],
[f'{get_inv()}.snapshot.id'],
),
sa.ForeignKeyConstraint(
['owner_id'],
['common.user.id'],
),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
op.execute(f"CREATE SEQUENCE {get_inv()}.snapshots_log_seq START 1;")
op.drop_table('snapshot_errors', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.snapshot_errors_seq;")
def downgrade():
op.drop_table('snapshots_log', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.snapshots_log_seq;")
op.create_table(
'snapshot_errors',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='The last time Devicehub recorded a change for \n this thing.\n ',
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='When Devicehub created this.',
),
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('description', citext.CIText(), nullable=False),
sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('severity', sa.SmallInteger(), nullable=False),
sa.Column('sid', citext.CIText(), nullable=True),
sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
['owner_id'],
['common.user.id'],
),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
op.execute(f"CREATE SEQUENCE {get_inv()}.snapshot_errors_seq START 1;")

View File

@ -0,0 +1,57 @@
"""add wbid user in snapshotErrors
Revision ID: 97bef94f7982
Revises: 23d9e7ebbd7d
Create Date: 2022-04-12 09:27:59.670911
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '97bef94f7982'
down_revision = '23d9e7ebbd7d'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.add_column(
'snapshot_errors',
sa.Column('wbid', citext.CIText(), nullable=True),
schema=f'{get_inv()}',
)
op.add_column(
'snapshot_errors',
sa.Column('owner_id', postgresql.UUID(), nullable=True),
schema=f'{get_inv()}',
)
op.create_foreign_key(
"fk_snapshot_errors_owner_id_user_id",
"snapshot_errors",
"user",
["owner_id"],
["id"],
ondelete="SET NULL",
source_schema=f'{get_inv()}',
referent_schema='common',
)
def downgrade():
op.drop_constraint(
"fk_snapshot_errors_owner_id_user_id",
"snapshot_errors",
type_="foreignkey",
schema=f'{get_inv()}',
)
op.drop_column('snapshot_errors', 'owner_id', schema=f'{get_inv()}')

View File

@ -0,0 +1,158 @@
"""transfer notes
Revision ID: dac62da1621a
Revises: 054a3aea9f08
Create Date: 2022-06-03 12:04:39.486276
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'dac62da1621a'
down_revision = '054a3aea9f08'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
# creating delivery note table
op.create_table(
'delivery_note',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column('number', citext.CIText(), nullable=True),
sa.Column('weight', sa.Integer(), nullable=True),
sa.Column('units', sa.Integer(), nullable=True),
sa.Column('transfer_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['transfer_id'], [f'{get_inv()}.transfer.id']),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
# creating index
op.create_index(
op.f('ix_delivery_note_created'),
'delivery_note',
['created'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
op.f('ix_delivery_note_updated'),
'delivery_note',
['updated'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
'ix_delivery_note_id',
'delivery_note',
['id'],
unique=False,
postgresql_using='hash',
schema=f'{get_inv()}',
)
# creating receiver note table
op.create_table(
'receiver_note',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column('number', citext.CIText(), nullable=True),
sa.Column('weight', sa.Integer(), nullable=True),
sa.Column('units', sa.Integer(), nullable=True),
sa.Column('transfer_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['transfer_id'], [f'{get_inv()}.transfer.id']),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
# creating index
op.create_index(
op.f('ix_receiver_note_created'),
'receiver_note',
['created'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
op.f('ix_receiver_note_updated'),
'receiver_note',
['updated'],
unique=False,
schema=f'{get_inv()}',
)
op.create_index(
'ix_receiver_note_id',
'receiver_note',
['id'],
unique=False,
postgresql_using='hash',
schema=f'{get_inv()}',
)
def downgrade():
op.drop_index(
op.f('ix_delivery_note_created'),
table_name='delivery_note',
schema=f'{get_inv()}',
)
op.drop_index(
op.f('ix_delivery_note_updated'),
table_name='delivery_note',
schema=f'{get_inv()}',
)
op.drop_index(
op.f('ix_delivery_note_id'), table_name='delivery_note', schema=f'{get_inv()}'
)
op.drop_table('delivery_note', schema=f'{get_inv()}')
op.drop_index(
op.f('ix_receiver_note_created'),
table_name='receiver_note',
schema=f'{get_inv()}',
)
op.drop_index(
op.f('ix_receiver_note_updated'),
table_name='receiver_note',
schema=f'{get_inv()}',
)
op.drop_index(
op.f('ix_receiver_note_id'), table_name='receiver_note', schema=f'{get_inv()}'
)
op.drop_table('receiver_note', schema=f'{get_inv()}')

View File

@ -0,0 +1,25 @@
from pathlib import Path
from pint import UnitRegistry
# Sets up the unit handling
unit_registry = Path(__file__).parent / 'unit_registry'
unit = UnitRegistry()
unit.load_definitions(str(unit_registry / 'quantities.txt'))
TB = unit.TB
GB = unit.GB
MB = unit.MB
Mbs = unit.Mbit / unit.s
MBs = unit.MB / unit.s
Hz = unit.Hz
GHz = unit.GHz
MHz = unit.MHz
Inch = unit.inch
mAh = unit.hour * unit.mA
mV = unit.mV
base2 = UnitRegistry()
base2.load_definitions(str(unit_registry / 'base2.quantities.txt'))
GiB = base2.GiB

View File

@ -0,0 +1,473 @@
import logging
import re
from contextlib import suppress
from datetime import datetime
from fractions import Fraction
from math import hypot
from typing import Iterator, List, Optional, Type, TypeVar
import dateutil.parser
from ereuse_utils import getter, text
from ereuse_utils.nested_lookup import (
get_nested_dicts_with_key_containing_value,
get_nested_dicts_with_key_value,
)
from ereuse_devicehub.parser import base2, unit, utils
from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.parser.utils import Dumpeable
from ereuse_devicehub.resources.enums import Severity
logger = logging.getLogger(__name__)
class Device(Dumpeable):
"""
Base class for a computer and each component, containing
its physical characteristics (like serial number) and Devicehub
actions. For Devicehub actions, this class has an interface to execute
:meth:`.benchmarks`.
"""
def __init__(self, *sources) -> None:
"""Gets the device information."""
self.actions = set()
self.type = self.__class__.__name__
super().__init__()
def from_lshw(self, lshw_node: dict):
self.manufacturer = getter.dict(lshw_node, 'vendor', default=None, type=str)
self.model = getter.dict(
lshw_node,
'product',
remove={self.manufacturer} if self.manufacturer else set(),
default=None,
type=str,
)
self.serial_number = getter.dict(lshw_node, 'serial', default=None, type=str)
def __str__(self) -> str:
return ' '.join(x for x in (self.model, self.serial_number) if x)
C = TypeVar('C', bound='Component')
class Component(Device):
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> Iterator[C]:
raise NotImplementedError()
class Processor(Component):
@classmethod
def new(cls, lshw: dict, **kwargs) -> Iterator[C]:
nodes = get_nested_dicts_with_key_value(lshw, 'class', 'processor')
# We want only the physical cpu's, not the logic ones
# In some cases we may get empty cpu nodes, we can detect them because
# all regular cpus have at least a description (Intel Core i5...)
return (
cls(node)
for node in nodes
if 'logical' not in node['id']
and node.get('description', '').lower() != 'co-processor'
and not node.get('disabled')
and 'co-processor' not in node.get('model', '').lower()
and 'co-processor' not in node.get('description', '').lower()
and 'width' in node
)
def __init__(self, node: dict) -> None:
super().__init__(node)
self.from_lshw(node)
self.speed = unit.Quantity(node['size'], node['units']).to('gigahertz').m
self.address = node['width']
try:
self.cores = int(node['configuration']['cores'])
self.threads = int(node['configuration']['threads'])
except KeyError:
self.threads = 1
self.cores = 1
self.serial_number = None # Processors don't have valid SN :-(
self.brand, self.generation = self.processor_brand_generation(self.model)
assert not hasattr(self, 'cores') or 1 <= self.cores <= 16
@staticmethod # noqa: C901
def processor_brand_generation(model: str):
"""Generates the ``brand`` and ``generation`` fields for the given model.
This returns a tuple with:
- The brand as a string or None.
- The generation as an int or None.
Intel desktop processor numbers:
https://www.intel.com/content/www/us/en/processors/processor-numbers.html
Intel server processor numbers:
https://www.intel.com/content/www/us/en/processors/processor-numbers-data-center.html
"""
if 'Duo' in model:
return 'Core2 Duo', None
if 'Quad' in model:
return 'Core2 Quad', None
if 'Atom' in model:
return 'Atom', None
if 'Celeron' in model:
return 'Celeron', None
if 'Pentium' in model:
return 'Pentium', None
if 'Xeon Platinum' in model:
generation = int(re.findall(r'\bPlatinum \d{4}\w', model)[0][10])
return 'Xeon Platinum', generation
if 'Xeon Gold' in model:
generation = int(re.findall(r'\bGold \d{4}\w', model)[0][6])
return 'Xeon Gold', generation
if 'Xeon' in model: # Xeon E5...
generation = 1
results = re.findall(r'\bV\d\b', model) # find V1, V2...
if results:
generation = int(results[0][1])
return 'Xeon', generation
results = re.findall(r'\bi\d-\w+', model) # i3-XXX..., i5-XXX...
if results: # i3, i5...
return 'Core i{}'.format(results[0][1]), int(results[0][3])
results = re.findall(r'\bi\d CPU \w+', model)
if results: # i3 CPU XXX
return 'Core i{}'.format(results[0][1]), 1
results = re.findall(r'\bm\d-\w+', model) # m3-XXXX...
if results:
return 'Core m{}'.format(results[0][1]), None
return None, None
def __str__(self) -> str:
return super().__str__() + (
' ({} generation)'.format(self.generation) if self.generation else ''
)
class RamModule(Component):
@classmethod
def new(cls, lshw, **kwargs) -> Iterator[C]:
# We can get flash memory (BIOS?), system memory and unknown types of memory
memories = get_nested_dicts_with_key_value(lshw, 'class', 'memory')
TYPES = {'ddr', 'sdram', 'sodimm'}
for memory in memories:
physical_ram = any(
t in memory.get('description', '').lower() for t in TYPES
)
not_empty = 'size' in memory
if physical_ram and not_empty:
yield cls(memory)
def __init__(self, node: dict) -> None:
# Node with no size == empty ram slot
super().__init__(node)
self.from_lshw(node)
description = node['description'].upper()
self.format = 'SODIMM' if 'SODIMM' in description else 'DIMM'
self.size = base2.Quantity(node['size'], node['units']).to('MiB').m
# self.size = int(utils.convert_capacity(node['size'], node['units'], 'MB'))
for w in description.split():
if w.startswith('DDR'): # We assume all DDR are SDRAM
self.interface = w
break
elif w.startswith('SDRAM'):
# Fallback. SDRAM is generic denomination for DDR types.
self.interface = w
if 'clock' in node:
self.speed = unit.Quantity(node['clock'], 'Hz').to('MHz').m
assert not hasattr(self, 'speed') or 100.0 <= self.speed <= 1000000000000.0
def __str__(self) -> str:
return '{} {} {}'.format(super().__str__(), self.format, self.size)
class GraphicCard(Component):
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> Iterator[C]:
nodes = get_nested_dicts_with_key_value(lshw, 'class', 'display')
return (cls(n) for n in nodes if n['configuration'].get('driver', None))
def __init__(self, node: dict) -> None:
super().__init__(node)
self.from_lshw(node)
self.memory = self._memory(node['businfo'].split('@')[1])
@staticmethod
def _memory(bus_info):
"""The size of the memory of the gpu."""
return None
def __str__(self) -> str:
return '{} with {}'.format(super().__str__(), self.memory)
class Motherboard(Component):
INTERFACES = 'usb', 'firewire', 'serial', 'pcmcia'
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> C:
node = next(get_nested_dicts_with_key_value(lshw, 'description', 'Motherboard'))
bios_node = next(get_nested_dicts_with_key_value(lshw, 'id', 'firmware'))
# bios_node = '1'
memory_array = next(
getter.indents(hwinfo, 'Physical Memory Array', indent=' '), None
)
return cls(node, bios_node, memory_array)
def __init__(
self, node: dict, bios_node: dict, memory_array: Optional[List[str]]
) -> None:
super().__init__(node)
self.from_lshw(node)
self.usb = self.num_interfaces(node, 'usb')
self.firewire = self.num_interfaces(node, 'firewire')
self.serial = self.num_interfaces(node, 'serial')
self.pcmcia = self.num_interfaces(node, 'pcmcia')
self.slots = int(2)
# run(
# 'dmidecode -t 17 | ' 'grep -o BANK | ' 'wc -l',
# check=True,
# universal_newlines=True,
# shell=True,
# stdout=PIPE,
# ).stdout
self.bios_date = dateutil.parser.parse(bios_node['date']).isoformat()
self.version = bios_node['version']
self.ram_slots = self.ram_max_size = None
if memory_array:
self.ram_slots = getter.kv(memory_array, 'Slots', default=None)
self.ram_max_size = getter.kv(memory_array, 'Max. Size', default=None)
if self.ram_max_size:
self.ram_max_size = next(text.numbers(self.ram_max_size))
@staticmethod
def num_interfaces(node: dict, interface: str) -> int:
interfaces = get_nested_dicts_with_key_containing_value(node, 'id', interface)
if interface == 'usb':
interfaces = (
c
for c in interfaces
if 'usbhost' not in c['id'] and 'usb' not in c['businfo']
)
return len(tuple(interfaces))
def __str__(self) -> str:
return super().__str__()
class NetworkAdapter(Component):
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> Iterator[C]:
nodes = get_nested_dicts_with_key_value(lshw, 'class', 'network')
return (cls(node) for node in nodes)
def __init__(self, node: dict) -> None:
super().__init__(node)
self.from_lshw(node)
self.speed = None
if 'capacity' in node:
self.speed = unit.Quantity(node['capacity'], 'bit/s').to('Mbit/s').m
if 'logicalname' in node: # todo this was taken from 'self'?
# If we don't have logicalname it means we don't have the
# (proprietary) drivers fot that NetworkAdaptor
# which means we can't access at the MAC address
# (note that S/N == MAC) "sudo /sbin/lspci -vv" could bring
# the MAC even if no drivers are installed however more work
# has to be done in ensuring it is reliable, really needed,
# and to parse it
# https://www.redhat.com/archives/redhat-list/2010-October/msg00066.html
# workbench-live includes proprietary firmwares
self.serial_number = self.serial_number or utils.get_hw_addr(
node['logicalname']
)
self.variant = node.get('version', None)
self.wireless = bool(node.get('configuration', {}).get('wireless', False))
def __str__(self) -> str:
return '{} {} {}'.format(
super().__str__(), self.speed, 'wireless' if self.wireless else 'ethernet'
)
class SoundCard(Component):
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> Iterator[C]:
nodes = get_nested_dicts_with_key_value(lshw, 'class', 'multimedia')
return (cls(node) for node in nodes)
def __init__(self, node) -> None:
super().__init__(node)
self.from_lshw(node)
class Display(Component):
TECHS = 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED'
"""Display technologies"""
@classmethod
def new(cls, lshw, hwinfo, **kwargs) -> Iterator[C]:
for node in getter.indents(hwinfo, 'Monitor'):
yield cls(node)
def __init__(self, node: dict) -> None:
super().__init__(node)
self.model = getter.kv(node, 'Model')
self.manufacturer = getter.kv(node, 'Vendor')
self.serial_number = getter.kv(node, 'Serial ID', default=None, type=str)
self.resolution_width, self.resolution_height, refresh_rate = text.numbers(
getter.kv(node, 'Resolution')
)
self.refresh_rate = unit.Quantity(refresh_rate, 'Hz').m
with suppress(StopIteration):
# some monitors can have several resolutions, and the one
# in "Detailed Timings" seems the highest one
timings = next(getter.indents(node, 'Detailed Timings', indent=' '))
self.resolution_width, self.resolution_height = text.numbers(
getter.kv(timings, 'Resolution')
)
x, y = (
unit.convert(v, 'millimeter', 'inch')
for v in text.numbers(getter.kv(node, 'Size'))
)
self.size = hypot(x, y)
self.technology = next((t for t in self.TECHS if t in node[0]), None)
d = '{} {} 0'.format(
getter.kv(node, 'Year of Manufacture'),
getter.kv(node, 'Week of Manufacture'),
)
# We assume it has been produced the first day of such week
self.production_date = datetime.strptime(d, '%Y %W %w').isoformat()
self._aspect_ratio = Fraction(self.resolution_width, self.resolution_height)
def __str__(self) -> str:
return (
'{0} {1.resolution_width}x{1.resolution_height} {1.size} inches {2}'.format(
super().__str__(), self, self._aspect_ratio
)
)
class Computer(Device):
CHASSIS_TYPE = {
'Desktop': {
'desktop',
'low-profile',
'tower',
'docking',
'all-in-one',
'pizzabox',
'mini-tower',
'space-saving',
'lunchbox',
'mini',
'stick',
},
'Laptop': {
'portable',
'laptop',
'convertible',
'tablet',
'detachable',
'notebook',
'handheld',
'sub-notebook',
},
'Server': {'server'},
'Computer': {'_virtual'},
}
"""
A translation dictionary whose keys are Devicehub types and values
are possible chassis values that `dmi <https://ezix.org/src/pkg/
lshw/src/master/src/core/dmi.cc#L632>`_ can offer.
"""
CHASSIS_DH = {
'Tower': {'desktop', 'low-profile', 'tower', 'server'},
'Docking': {'docking'},
'AllInOne': {'all-in-one'},
'Microtower': {'mini-tower', 'space-saving', 'mini'},
'PizzaBox': {'pizzabox'},
'Lunchbox': {'lunchbox'},
'Stick': {'stick'},
'Netbook': {'notebook', 'sub-notebook'},
'Handheld': {'handheld'},
'Laptop': {'portable', 'laptop'},
'Convertible': {'convertible'},
'Detachable': {'detachable'},
'Tablet': {'tablet'},
'Virtual': {'_virtual'},
}
"""
A conversion table from DMI's chassis type value Devicehub
chassis value.
"""
COMPONENTS = list(Component.__subclasses__()) # type: List[Type[Component]]
COMPONENTS.remove(Motherboard)
def __init__(self, node: dict) -> None:
super().__init__(node)
self.from_lshw(node)
chassis = node.get('configuration', {}).get('chassis', '_virtual')
self.type = next(
t for t, values in self.CHASSIS_TYPE.items() if chassis in values
)
self.chassis = next(
t for t, values in self.CHASSIS_DH.items() if chassis in values
)
self.sku = getter.dict(node, ('configuration', 'sku'), default=None, type=str)
self.version = getter.dict(node, 'version', default=None, type=str)
self._ram = None
@classmethod
def run(cls, lshw, hwinfo_raw, uuid=None, sid=None, version=None):
"""
Gets hardware information from the computer and its components,
like serial numbers or model names, and benchmarks them.
This function uses ``LSHW`` as the main source of hardware information,
which is obtained once when it is instantiated.
"""
hwinfo = hwinfo_raw.splitlines()
computer = cls(lshw)
components = []
try:
for Component in cls.COMPONENTS:
if Component == Display and computer.type != 'Laptop':
continue # Only get display info when computer is laptop
components.extend(Component.new(lshw=lshw, hwinfo=hwinfo))
components.append(Motherboard.new(lshw, hwinfo))
computer._ram = sum(
ram.size for ram in components if isinstance(ram, RamModule)
)
except Exception as err:
# if there are any problem with components, save the problem and continue
txt = "Error: Snapshot: {uuid}, sid: {sid}, type_error: {type}, error: {error}".format(
uuid=uuid, sid=sid, type=err.__class__, error=err
)
cls.errors(txt, uuid=uuid, sid=sid, version=version)
return computer, components
@classmethod
def errors(
cls, txt=None, uuid=None, sid=None, version=None, severity=Severity.Error
):
if not txt:
return
logger.error(txt)
error = SnapshotsLog(
description=txt,
snapshot_uuid=uuid,
severity=severity,
sid=sid,
version=version,
)
error.save()
def __str__(self) -> str:
specs = super().__str__()
return '{} with {} MB of RAM.'.format(specs, self._ram)

View File

@ -0,0 +1,45 @@
from citext import CIText
from flask import g
from sqlalchemy import BigInteger, Column, Sequence, SmallInteger
from sqlalchemy.dialects.postgresql import UUID
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.enums import Severity
from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User
class SnapshotsLog(Thing):
"""A Snapshot log."""
id = Column(BigInteger, Sequence('snapshots_log_seq'), primary_key=True)
severity = Column(SmallInteger, default=Severity.Info, nullable=False)
version = Column(CIText(), default='', nullable=True)
description = Column(CIText(), default='', nullable=True)
sid = Column(CIText(), nullable=True)
snapshot_uuid = Column(UUID(as_uuid=True), nullable=True)
snapshot_id = Column(UUID(as_uuid=True), db.ForeignKey(Snapshot.id), nullable=True)
owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id,
)
snapshot = db.relationship(Snapshot, primaryjoin=snapshot_id == Snapshot.id)
owner = db.relationship(User, primaryjoin=owner_id == User.id)
def save(self, commit=False):
db.session.add(self)
if commit:
db.session.commit()
def get_status(self):
return Severity(self.severity)
def get_device(self):
if self.snapshot:
return self.snapshot.device.devicehub_id
return ''

View File

@ -0,0 +1,563 @@
import json
import logging
import uuid
from dmidecode import DMIParse
from flask import request
from marshmallow.exceptions import ValidationError
from ereuse_devicehub.parser import base2
from ereuse_devicehub.parser.computer import Computer
from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.resources.action.schemas import Snapshot
from ereuse_devicehub.resources.enums import DataStorageInterface, Severity
logger = logging.getLogger(__name__)
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.dmidecode_raw = snapshot["data"]["dmidecode"]
self.smart_raw = snapshot["data"]["smart"]
self.hwinfo_raw = snapshot["data"]["hwinfo"]
self.device = {"actions": []}
self.components = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.hwinfo = self.parse_hwinfo()
self.set_basic_datas()
self.set_components()
self.snapshot_json = {
"device": self.device,
"software": "Workbench",
"components": self.components,
"uuid": snapshot['uuid'],
"type": snapshot['type'],
"version": "14.0.0",
"endTime": snapshot["timestamp"],
"elapsed": 1,
"sid": snapshot["sid"],
}
def get_snapshot(self):
return Snapshot().load(self.snapshot_json)
def set_basic_datas(self):
self.device['manufacturer'] = self.dmi.manufacturer()
self.device['model'] = self.dmi.model()
self.device['serialNumber'] = self.dmi.serial_number()
self.device['type'] = self.get_type()
self.device['sku'] = self.get_sku()
self.device['version'] = self.get_version()
self.device['uuid'] = self.get_uuid()
def set_components(self):
self.get_cpu()
self.get_ram()
self.get_mother_board()
self.get_data_storage()
self.get_networks()
def get_cpu(self):
# TODO @cayop generation, brand and address not exist in dmidecode
for cpu in self.dmi.get('Processor'):
self.components.append(
{
"actions": [],
"type": "Processor",
"speed": self.get_cpu_speed(cpu),
"cores": int(cpu.get('Core Count', 1)),
"model": cpu.get('Version'),
"threads": int(cpu.get('Thread Count', 1)),
"manufacturer": cpu.get('Manufacturer'),
"serialNumber": cpu.get('Serial Number'),
"generation": cpu.get('Generation'),
"brand": cpu.get('Brand'),
"address": cpu.get('Address'),
}
)
def get_ram(self):
# TODO @cayop format and model not exist in dmidecode
for ram in self.dmi.get("Memory Device"):
self.components.append(
{
"actions": [],
"type": "RamModule",
"size": self.get_ram_size(ram),
"speed": self.get_ram_speed(ram),
"manufacturer": ram.get("Manufacturer", self.default),
"serialNumber": ram.get("Serial Number", self.default),
"interface": self.get_ram_type(ram),
"format": self.get_ram_format(ram),
"model": ram.get("Part Number", self.default),
}
)
def get_mother_board(self):
# TODO @cayop model, not exist in dmidecode
for moder_board in self.dmi.get("Baseboard"):
self.components.append(
{
"actions": [],
"type": "Motherboard",
"version": moder_board.get("Version"),
"serialNumber": moder_board.get("Serial Number"),
"manufacturer": moder_board.get("Manufacturer"),
"biosDate": self.get_bios_date(),
# "firewire": self.get_firmware(),
"ramMaxSize": self.get_max_ram_size(),
"ramSlots": len(self.dmi.get("Memory Device")),
"slots": self.get_ram_slots(),
"model": moder_board.get("Product Name"), # ??
"pcmcia": self.get_pcmcia_num(), # ??
"serial": self.get_serial_num(), # ??
"usb": self.get_usb_num(),
}
)
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_date(self):
return self.dmi.get("BIOS")[0].get("Release Date", self.default)
def get_firmware(self):
return self.dmi.get("BIOS")[0].get("Firmware Revision", '1')
def get_max_ram_size(self):
size = 0
for slot in self.dmi.get("Physical Memory Array"):
capacity = slot.get("Maximum Capacity", '0').split(" ")[0]
size += int(capacity)
return size
def get_ram_slots(self):
slots = 0
for x in self.dmi.get("Physical Memory Array"):
slots += int(x.get("Number Of Devices", 0))
return slots
def get_ram_size(self, ram):
size = ram.get("Size", "0")
return int(size.split(" ")[0])
def get_ram_speed(self, ram):
size = ram.get("Speed", "0")
return int(size.split(" ")[0])
def get_ram_type(self, ram):
TYPES = {'ddr', 'sdram', 'sodimm'}
for t in TYPES:
if t in ram.get("Type", "DDR"):
return t
def get_ram_format(self, ram):
channel = ram.get("Locator", "DIMM")
return 'SODIMM' if 'SODIMM' in channel else 'DIMM'
def get_cpu_speed(self, cpu):
speed = cpu.get('Max Speed', "0")
return float(speed.split(" ")[0]) / 1024
def get_sku(self):
return self.dmi.get("System")[0].get("SKU Number", self.default)
def get_version(self):
return self.dmi.get("System")[0].get("Version", self.default)
def get_uuid(self):
return self.dmi.get("System")[0].get("UUID", self.default)
def get_chassis(self):
return self.dmi.get("Chassis")[0].get("Type", self.default)
def get_type(self):
chassis_type = self.get_chassis()
return self.translation_to_devicehub(chassis_type)
def translation_to_devicehub(self, original_type):
lower_type = original_type.lower()
CHASSIS_TYPE = {
'Desktop': [
'desktop',
'low-profile',
'tower',
'docking',
'all-in-one',
'pizzabox',
'mini-tower',
'space-saving',
'lunchbox',
'mini',
'stick',
],
'Laptop': [
'portable',
'laptop',
'convertible',
'tablet',
'detachable',
'notebook',
'handheld',
'sub-notebook',
],
'Server': ['server'],
'Computer': ['_virtual'],
}
for k, v in CHASSIS_TYPE.items():
if lower_type in v:
return k
return self.default
def get_data_storage(self):
for sm in self.smart:
model = sm.get('model_name')
manufacturer = None
if len(model.split(" ")) == 2:
manufacturer, model = model.split(" ")
self.components.append(
{
"actions": [],
"type": self.get_data_storage_type(sm),
"model": model,
"manufacturer": manufacturer,
"serialNumber": sm.get('serial_number'),
"size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm),
}
)
def get_data_storage_type(self, x):
# TODO @cayop add more SSDS types
SSDS = ["nvme"]
SSD = 'SolidStateDrive'
HDD = 'HardDrive'
type_dev = x.get('device', {}).get('type')
return SSD if type_dev in SSDS else HDD
def get_data_storage_interface(self, x):
return x.get('device', {}).get('protocol', 'ATA')
def get_data_storage_size(self, x):
type_dev = x.get('device', {}).get('type')
total_capacity = "{type}_total_capacity".format(type=type_dev)
# convert bytes to Mb
return x.get(total_capacity) / 1024**2
def get_networks(self):
hw_class = " Hardware Class: "
mac = " Permanent HW Address: "
model = " Model: "
wireless = "wireless"
for line in self.hwinfo:
iface = {
"variant": "1",
"actions": [],
"speed": 100.0,
"type": "NetworkAdapter",
"wireless": False,
"manufacturer": "Ethernet",
}
for y in line:
if hw_class in y and not y.split(hw_class)[1] == 'network':
break
if mac in y:
iface["serialNumber"] = y.split(mac)[1]
if model in y:
iface["model"] = y.split(model)[1]
if wireless in y:
iface["wireless"] = True
if iface.get("serialNumber"):
self.components.append(iface)
def parse_hwinfo(self):
hw_blocks = self.hwinfo_raw.split("\n\n")
return [x.split("\n") for x in hw_blocks]
def loads(self, x):
if isinstance(x, str):
return json.loads(x)
return x
class ParseSnapshotLsHw:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.uuid = snapshot.get("uuid")
self.sid = snapshot.get("sid")
self.version = str(snapshot.get("version"))
self.dmidecode_raw = snapshot["data"]["dmidecode"]
self.smart = snapshot["data"]["smart"]
self.hwinfo_raw = snapshot["data"]["hwinfo"]
self.lshw = snapshot["data"]["lshw"]
self.device = {"actions": []}
self.components = []
self.components_obj = []
self._errors = []
self.dmi = DMIParse(self.dmidecode_raw)
self.hwinfo = self.parse_hwinfo()
self.set_basic_datas()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": "Workbench",
"components": self.components,
"uuid": snapshot['uuid'],
"version": "14.0.0",
"endTime": snapshot["timestamp"],
"elapsed": 1,
"sid": snapshot["sid"],
}
def get_snapshot(self):
return Snapshot().load(self.snapshot_json)
def parse_hwinfo(self):
hw_blocks = self.hwinfo_raw.split("\n\n")
return [x.split("\n") for x in hw_blocks]
def loads(self, x):
if isinstance(x, str):
return json.loads(x)
return x
def set_basic_datas(self):
try:
pc, self.components_obj = Computer.run(
self.lshw, self.hwinfo_raw, self.uuid, self.sid, self.version
)
pc = pc.dump()
minimum_hid = None in [pc['manufacturer'], pc['model'], pc['serialNumber']]
if minimum_hid and not self.components_obj:
# if no there are hid and any components return 422
raise Exception
except Exception:
msg = """It has not been possible to create the device because we lack data.
You can find more information at: {}""".format(
request.url_root
)
txt = json.dumps({'sid': self.sid, 'message': msg})
raise ValidationError(txt)
self.device = pc
self.device['uuid'] = self.get_uuid()
def set_components(self):
memory = None
for x in self.components_obj:
if x.type == 'RamModule':
memory = 1
if x.type == 'Motherboard':
x.slots = self.get_ram_slots()
self.components.append(x.dump())
if not memory:
self.get_ram()
self.get_data_storage()
def get_ram(self):
for ram in self.dmi.get("Memory Device"):
self.components.append(
{
"actions": [],
"type": "RamModule",
"size": self.get_ram_size(ram),
"speed": self.get_ram_speed(ram),
"manufacturer": ram.get("Manufacturer", self.default),
"serialNumber": ram.get("Serial Number", self.default),
"interface": self.get_ram_type(ram),
"format": self.get_ram_format(ram),
"model": ram.get("Part Number", self.default),
}
)
def get_ram_size(self, ram):
size = ram.get("Size")
if not len(size.split(" ")) == 2:
txt = (
"Error: Snapshot: {uuid}, Sid: {sid} have this ram Size: {size}".format(
uuid=self.uuid, size=size, sid=self.sid
)
)
self.errors(txt, severity=Severity.Warning)
return 128
size, units = size.split(" ")
return base2.Quantity(float(size), units).to('MiB').m
def get_ram_speed(self, ram):
speed = ram.get("Speed", "100")
if not len(speed.split(" ")) == 2:
txt = "Error: Snapshot: {uuid}, Sid: {sid} have this ram Speed: {speed}".format(
uuid=self.uuid, speed=speed, sid=self.sid
)
self.errors(txt, severity=Severity.Warning)
return 100
speed, units = speed.split(" ")
return float(speed)
# TODO @cayop is neccesary change models for accept sizes more high of speed or change to string
# return base2.Quantity(float(speed), units).to('MHz').m
def get_ram_slots(self):
slots = 0
for x in self.dmi.get("Physical Memory Array"):
slots += int(x.get("Number Of Devices", 0))
return slots
def get_ram_type(self, ram):
TYPES = {'ddr', 'sdram', 'sodimm'}
for t in TYPES:
if t in ram.get("Type", "DDR"):
return t
def get_ram_format(self, ram):
channel = ram.get("Locator", "DIMM")
return 'SODIMM' if 'SODIMM' in channel else 'DIMM'
def get_uuid(self):
dmi_uuid = 'undefined'
if self.dmi.get("System"):
dmi_uuid = self.dmi.get("System")[0].get("UUID")
try:
uuid.UUID(dmi_uuid)
except (ValueError, AttributeError) as err:
self.errors("{}".format(err))
txt = "Error: Snapshot: {uuid} sid: {sid} have this uuid: {device}".format(
uuid=self.uuid, device=dmi_uuid, sid=self.sid
)
self.errors(txt, severity=Severity.Warning)
dmi_uuid = None
return dmi_uuid
def get_data_storage(self):
for sm in self.smart:
if sm.get('smartctl', {}).get('exit_status') == 1:
continue
model = sm.get('model_name')
manufacturer = None
if model and len(model.split(" ")) > 1:
mm = model.split(" ")
model = mm[-1]
manufacturer = " ".join(mm[:-1])
self.components.append(
{
"actions": [self.get_test_data_storage(sm)],
"type": self.get_data_storage_type(sm),
"model": model,
"manufacturer": manufacturer,
"serialNumber": sm.get('serial_number'),
"size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm),
}
)
def get_data_storage_type(self, x):
# TODO @cayop add more SSDS types
SSDS = ["nvme"]
SSD = 'SolidStateDrive'
HDD = 'HardDrive'
type_dev = x.get('device', {}).get('type')
trim = x.get('trim', {}).get("supported") in [True, "true"]
return SSD if type_dev in SSDS or trim else HDD
def get_data_storage_interface(self, x):
interface = x.get('device', {}).get('protocol', 'ATA')
try:
DataStorageInterface(interface.upper())
except ValueError as err:
txt = "Sid: {}, interface {} is not in DataStorageInterface Enum".format(
self.sid, interface
)
self.errors("{}".format(err))
self.errors(txt, severity=Severity.Warning)
return "ATA"
def get_data_storage_size(self, x):
total_capacity = x.get('user_capacity', {}).get('bytes')
if not total_capacity:
return 1
# convert bytes to Mb
return total_capacity / 1024**2
def get_test_data_storage(self, smart):
hours = smart.get("power_on_time", {}).get('hours', 0)
action = {
"status": "Completed without error",
"reallocatedSectorCount": smart.get("reallocated_sector_count", 0),
"currentPendingSectorCount": smart.get("current_pending_sector_count", 0),
"assessment": True,
"severity": "Info",
"offlineUncorrectable": smart.get("offline_uncorrectable", 0),
"lifetime": hours,
"powerOnHours": hours,
"type": "TestDataStorage",
"length": "Short",
"elapsed": 0,
"reportedUncorrectableErrors": smart.get(
"reported_uncorrectable_errors", 0
),
"powerCycleCount": smart.get("power_cycle_count", 0),
}
return action
def errors(self, txt=None, severity=Severity.Error):
if not txt:
return self._errors
logger.error(txt)
self._errors.append(txt)
error = SnapshotsLog(
description=txt,
snapshot_uuid=self.uuid,
severity=severity,
sid=self.sid,
version=self.version,
)
error.save()

View File

@ -0,0 +1,36 @@
from flask import current_app as app
from marshmallow import Schema as MarshmallowSchema
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Dict, List, Nested, String
from ereuse_devicehub.resources.schemas import Thing
class Snapshot_lite_data(MarshmallowSchema):
dmidecode = String(required=True)
hwinfo = String(required=True)
smart = List(Dict(), required=True)
lshw = Dict(required=True)
lspci = String(required=True)
class Snapshot_lite(Thing):
uuid = String(required=True)
version = String(required=True)
schema_api = String(required=True)
software = String(required=True)
sid = String(required=True)
type = String(required=True)
timestamp = String(required=True)
data = Nested(Snapshot_lite_data, required=True)
@validates_schema
def validate_workbench_version(self, data: dict):
if data['schema_api'] not in app.config['SCHEMA_WORKBENCH']:
raise ValidationError(
'Min. supported Workbench version is '
'{} but yours is {}.'.format(
app.config['SCHEMA_WORKBENCH'][0], data['version']
),
field_names=['version'],
)

View File

@ -0,0 +1,38 @@
from datetime import datetime, timezone
from typing import List
from ereuse_workbench.computer import Component, Computer, DataStorage
from ereuse_workbench.utils import Dumpeable
class Snapshot(Dumpeable):
"""
Generates the Snapshot report for Devicehub by obtaining the
data from the computer, performing benchmarks and tests...
After instantiating the class, run :meth:`.computer` before any
other method.
"""
def __init__(self, uuid, software, version, lshw, hwinfo):
self.type = 'Snapshot'
self.uuid = uuid
self.software = software
self.version = version
self.lshw = lshw
self.hwinfo = hwinfo
self.endTime = datetime.now(timezone.utc)
self.closed = False
self.elapsed = None
self.device = None # type: Computer
self.components = None # type: List[Component]
self._storages = None
def computer(self):
"""Retrieves information about the computer and components."""
self.device, self.components = Computer.run(self.lshw, self.hwinfo)
self._storages = tuple(c for c in self.components if isinstance(c, DataStorage))
def close(self):
"""Closes the Snapshot"""
self.closed = True

View File

@ -0,0 +1,4 @@
K = KiB = k = kb = KB
M = MiB = m = mb = MB
G = GiB = g = gb = GB
T = TiB = t = tb = TB

View File

@ -0,0 +1,9 @@
HZ = hertz = hz
KHZ = kilohertz = khz
MHZ = megahertz = mhz
GHZ = gigahertz = ghz
B = byte = b = UNIT = unit
KB = kilobyte = kb = K = k
MB = megabyte = mb = M = m
GB = gigabyte = gb = G = g
T = terabyte = tb = T = t

View File

@ -0,0 +1,38 @@
import datetime
import fcntl
import socket
import struct
from contextlib import contextmanager
from enum import Enum
from ereuse_utils import Dumpeable
class Severity(Enum):
Info = 'Info'
Error = 'Error'
def get_hw_addr(ifname):
# http://stackoverflow.com/a/4789267/1538221
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', ifname[:15]))
return ':'.join('%02x' % ord(char) for char in info[18:24])
class Measurable(Dumpeable):
"""A base class that allows measuring execution times."""
def __init__(self) -> None:
super().__init__()
self.elapsed = None
@contextmanager
def measure(self):
init = datetime.datetime.now(datetime.timezone.utc)
yield
self.elapsed = datetime.datetime.now(datetime.timezone.utc) - init
try:
assert self.elapsed.total_seconds() > 0
except AssertionError:
self.elapsed = datetime.timedelta(seconds=0)

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,28 @@
import copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil.tz import tzutc from dateutil.tz import tzutc
from flask import current_app as app, g from flask import current_app as app
from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema, pre_load, post_load from flask import g
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \ from marshmallow import Schema as MarshmallowSchema
TimeDelta, UUID from marshmallow import ValidationError
from marshmallow import fields as f
from marshmallow import post_load, pre_load, validates_schema
from marshmallow.fields import (
UUID,
Boolean,
DateTime,
Decimal,
Float,
Integer,
Nested,
String,
TimeDelta,
)
from marshmallow.validate import Length, OneOf, Range from marshmallow.validate import Length, OneOf, Range
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from teal.enums import Country, Currency, Subdivision from teal.enums import Country, Currency, Subdivision
from teal.marshmallow import EnumField, IP, SanitizedStr, URL, Version from teal.marshmallow import IP, URL, EnumField, SanitizedStr, Version
from teal.resource import Schema from teal.resource import Schema
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
@ -16,24 +30,32 @@ from ereuse_devicehub.resources import enums
from ereuse_devicehub.resources.action import models as m from ereuse_devicehub.resources.action import models as m
from ereuse_devicehub.resources.agent import schemas as s_agent from ereuse_devicehub.resources.agent import schemas as s_agent
from ereuse_devicehub.resources.device import schemas as s_device from ereuse_devicehub.resources.device import schemas as s_device
from ereuse_devicehub.resources.tradedocument import schemas as s_document
from ereuse_devicehub.resources.documents import schemas as s_generic_document from ereuse_devicehub.resources.documents import schemas as s_generic_document
from ereuse_devicehub.resources.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \ from ereuse_devicehub.resources.enums import (
PhysicalErasureMethod, R_POSITIVE, RatingRange, \ R_POSITIVE,
Severity, SnapshotSoftware, TestDataStorageLength AppearanceRange,
BiosAccessRange,
FunctionalityRange,
PhysicalErasureMethod,
RatingRange,
Severity,
SnapshotSoftware,
TestDataStorageLength,
)
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.tradedocument import schemas as s_document
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user import schemas as s_user from ereuse_devicehub.resources.user import schemas as s_user
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
class Action(Thing): class Action(Thing):
__doc__ = m.Action.__doc__ __doc__ = m.Action.__doc__
id = UUID(dump_only=True) id = UUID(dump_only=True)
name = SanitizedStr(default='', name = SanitizedStr(
validate=Length(max=STR_BIG_SIZE), default='', validate=Length(max=STR_BIG_SIZE), description=m.Action.name.comment
description=m.Action.name.comment) )
closed = Boolean(missing=True, description=m.Action.closed.comment) closed = Boolean(missing=True, description=m.Action.closed.comment)
severity = EnumField(Severity, description=m.Action.severity.comment) severity = EnumField(Severity, description=m.Action.severity.comment)
description = SanitizedStr(default='', description=m.Action.description.comment) description = SanitizedStr(default='', description=m.Action.description.comment)
@ -43,16 +65,18 @@ class Action(Thing):
agent = NestedOn(s_agent.Agent, description=m.Action.agent_id.comment) agent = NestedOn(s_agent.Agent, description=m.Action.agent_id.comment)
author = NestedOn(s_user.User, dump_only=True, exclude=('token',)) author = NestedOn(s_user.User, dump_only=True, exclude=('token',))
components = NestedOn(s_device.Component, dump_only=True, many=True) components = NestedOn(s_device.Component, dump_only=True, many=True)
parent = NestedOn(s_device.Computer, dump_only=True, description=m.Action.parent_id.comment) parent = NestedOn(
s_device.Computer, dump_only=True, description=m.Action.parent_id.comment
)
url = URL(dump_only=True, description=m.Action.url.__doc__) url = URL(dump_only=True, description=m.Action.url.__doc__)
@validates_schema @validates_schema
def validate_times(self, data: dict): def validate_times(self, data: dict):
unix_time = datetime.fromisoformat("1970-01-02 00:00:00+00:00") unix_time = datetime.fromisoformat("1970-01-02 00:00:00+00:00")
if 'end_time' in data and data['end_time'] < unix_time: if 'end_time' in data and data['end_time'].replace(tzinfo=tzutc()) < unix_time:
data['end_time'] = unix_time data['end_time'] = unix_time
if 'start_time' in data and data['start_time'] < unix_time: if 'start_time' in data and data['start_time'].replace(tzinfo=tzutc()) < unix_time:
data['start_time'] = unix_time data['start_time'] = unix_time
if data.get('end_time') and data.get('start_time'): if data.get('end_time') and data.get('start_time'):
@ -67,24 +91,27 @@ class ActionWithOneDevice(Action):
class ActionWithMultipleDocuments(Action): class ActionWithMultipleDocuments(Action):
__doc__ = m.ActionWithMultipleTradeDocuments.__doc__ __doc__ = m.ActionWithMultipleTradeDocuments.__doc__
documents = NestedOn(s_document.TradeDocument, documents = NestedOn(
many=True, s_document.TradeDocument,
required=True, # todo test ensuring len(devices) >= 1 many=True,
only_query='id', required=True, # todo test ensuring len(devices) >= 1
collection_class=OrderedSet) only_query='id',
collection_class=OrderedSet,
)
class ActionWithMultipleDevices(Action): class ActionWithMultipleDevices(Action):
__doc__ = m.ActionWithMultipleDevices.__doc__ __doc__ = m.ActionWithMultipleDevices.__doc__
devices = NestedOn(s_device.Device, devices = NestedOn(
many=True, s_device.Device,
required=True, # todo test ensuring len(devices) >= 1 many=True,
only_query='id', required=True, # todo test ensuring len(devices) >= 1
collection_class=OrderedSet) only_query='id',
collection_class=OrderedSet,
)
class ActionWithMultipleDevicesCheckingOwner(ActionWithMultipleDevices): class ActionWithMultipleDevicesCheckingOwner(ActionWithMultipleDevices):
@post_load @post_load
def check_owner_of_device(self, data): def check_owner_of_device(self, data):
for dev in data['devices']: for dev in data['devices']:
@ -102,20 +129,29 @@ class Remove(ActionWithOneDevice):
class Allocate(ActionWithMultipleDevicesCheckingOwner): class Allocate(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Allocate.__doc__ __doc__ = m.Allocate.__doc__
start_time = DateTime(data_key='startTime', required=True, start_time = DateTime(
description=m.Action.start_time.comment) data_key='startTime', required=True, description=m.Action.start_time.comment
end_time = DateTime(data_key='endTime', required=False, )
description=m.Action.end_time.comment) end_time = DateTime(
final_user_code = SanitizedStr(data_key="finalUserCode", data_key='endTime', required=False, description=m.Action.end_time.comment
validate=Length(min=1, max=STR_BIG_SIZE), )
required=False, final_user_code = SanitizedStr(
description='This is a internal code for mainteing the secrets of the \ data_key="finalUserCode",
personal datas of the new holder') validate=Length(min=1, max=STR_BIG_SIZE),
transaction = SanitizedStr(validate=Length(min=1, max=STR_BIG_SIZE), required=False,
required=False, description='This is a internal code for mainteing the secrets of the \
description='The code used from the owner for \ personal datas of the new holder',
relation with external tool.') )
end_users = Integer(data_key='endUsers', validate=[Range(min=1, error="Value must be greater than 0")]) transaction = SanitizedStr(
validate=Length(min=1, max=STR_BIG_SIZE),
required=False,
description='The code used from the owner for \
relation with external tool.',
)
end_users = Integer(
data_key='endUsers',
validate=[Range(min=1, error="Value must be greater than 0")],
)
@validates_schema @validates_schema
def validate_allocate(self, data: dict): def validate_allocate(self, data: dict):
@ -136,12 +172,15 @@ class Allocate(ActionWithMultipleDevicesCheckingOwner):
class Deallocate(ActionWithMultipleDevicesCheckingOwner): class Deallocate(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Deallocate.__doc__ __doc__ = m.Deallocate.__doc__
start_time = DateTime(data_key='startTime', required=True, start_time = DateTime(
description=m.Action.start_time.comment) data_key='startTime', required=True, description=m.Action.start_time.comment
transaction = SanitizedStr(validate=Length(min=1, max=STR_BIG_SIZE), )
required=False, transaction = SanitizedStr(
description='The code used from the owner for \ validate=Length(min=1, max=STR_BIG_SIZE),
relation with external tool.') required=False,
description='The code used from the owner for \
relation with external tool.',
)
@validates_schema @validates_schema
def validate_deallocate(self, data: dict): def validate_deallocate(self, data: dict):
@ -232,7 +271,9 @@ class MeasureBattery(Test):
__doc__ = m.MeasureBattery.__doc__ __doc__ = m.MeasureBattery.__doc__
size = Integer(required=True, description=m.MeasureBattery.size.comment) size = Integer(required=True, description=m.MeasureBattery.size.comment)
voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment) voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment)
cycle_count = Integer(data_key='cycleCount', description=m.MeasureBattery.cycle_count.comment) cycle_count = Integer(
data_key='cycleCount', description=m.MeasureBattery.cycle_count.comment
)
health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment) health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment)
@ -289,28 +330,32 @@ class TestBios(Test):
class VisualTest(Test): class VisualTest(Test):
__doc__ = m.VisualTest.__doc__ __doc__ = m.VisualTest.__doc__
appearance_range = EnumField(AppearanceRange, data_key='appearanceRange') appearance_range = EnumField(AppearanceRange, data_key='appearanceRange')
functionality_range = EnumField(FunctionalityRange, functionality_range = EnumField(FunctionalityRange, data_key='functionalityRange')
data_key='functionalityRange')
labelling = Boolean() labelling = Boolean()
class Rate(ActionWithOneDevice): class Rate(ActionWithOneDevice):
__doc__ = m.Rate.__doc__ __doc__ = m.Rate.__doc__
rating = Integer(validate=Range(*R_POSITIVE), rating = Integer(
dump_only=True, validate=Range(*R_POSITIVE), dump_only=True, description=m.Rate._rating.comment
description=m.Rate._rating.comment) )
version = Version(dump_only=True, version = Version(dump_only=True, description=m.Rate.version.comment)
description=m.Rate.version.comment) appearance = Integer(
appearance = Integer(validate=Range(enums.R_NEGATIVE), validate=Range(enums.R_NEGATIVE),
dump_only=True, dump_only=True,
description=m.Rate._appearance.comment) description=m.Rate._appearance.comment,
functionality = Integer(validate=Range(enums.R_NEGATIVE), )
dump_only=True, functionality = Integer(
description=m.Rate._functionality.comment) validate=Range(enums.R_NEGATIVE),
rating_range = EnumField(RatingRange, dump_only=True,
dump_only=True, description=m.Rate._functionality.comment,
data_key='ratingRange', )
description=m.Rate.rating_range.__doc__) rating_range = EnumField(
RatingRange,
dump_only=True,
data_key='ratingRange',
description=m.Rate.rating_range.__doc__,
)
class RateComputer(Rate): class RateComputer(Rate):
@ -320,19 +365,25 @@ class RateComputer(Rate):
data_storage = Float(dump_only=True, data_key='dataStorage') data_storage = Float(dump_only=True, data_key='dataStorage')
graphic_card = Float(dump_only=True, data_key='graphicCard') graphic_card = Float(dump_only=True, data_key='graphicCard')
data_storage_range = EnumField(RatingRange, dump_only=True, data_key='dataStorageRange') data_storage_range = EnumField(
RatingRange, dump_only=True, data_key='dataStorageRange'
)
ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange') ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange')
processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange') processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange')
graphic_card_range = EnumField(RatingRange, dump_only=True, data_key='graphicCardRange') graphic_card_range = EnumField(
RatingRange, dump_only=True, data_key='graphicCardRange'
)
class Price(ActionWithOneDevice): class Price(ActionWithOneDevice):
__doc__ = m.Price.__doc__ __doc__ = m.Price.__doc__
currency = EnumField(Currency, required=True, description=m.Price.currency.comment) currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
price = Decimal(places=m.Price.SCALE, price = Decimal(
rounding=m.Price.ROUND, places=m.Price.SCALE,
required=True, rounding=m.Price.ROUND,
description=m.Price.price.comment) required=True,
description=m.Price.price.comment,
)
version = Version(dump_only=True, description=m.Price.version.comment) version = Version(dump_only=True, description=m.Price.version.comment)
rating = NestedOn(Rate, dump_only=True, description=m.Price.rating_id.comment) rating = NestedOn(Rate, dump_only=True, description=m.Price.rating_id.comment)
@ -356,9 +407,11 @@ class EreusePrice(Price):
class Install(ActionWithOneDevice): class Install(ActionWithOneDevice):
__doc__ = m.Install.__doc__ __doc__ = m.Install.__doc__
name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE), name = SanitizedStr(
required=True, validate=Length(min=4, max=STR_BIG_SIZE),
description='The name of the OS installed.') required=True,
description='The name of the OS installed.',
)
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256})) address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))
@ -372,18 +425,23 @@ class Snapshot(ActionWithOneDevice):
See docs for more info. See docs for more info.
""" """
uuid = UUID() uuid = UUID()
software = EnumField(SnapshotSoftware, sid = String(required=False)
required=True, software = EnumField(
description='The software that generated this Snapshot.') SnapshotSoftware,
required=True,
description='The software that generated this Snapshot.',
)
version = Version(required=True, description='The version of the software.') version = Version(required=True, description='The version of the software.')
actions = NestedOn(Action, many=True, dump_only=True) actions = NestedOn(Action, many=True, dump_only=True)
elapsed = TimeDelta(precision=TimeDelta.SECONDS) elapsed = TimeDelta(precision=TimeDelta.SECONDS)
components = NestedOn(s_device.Component, components = NestedOn(
many=True, s_device.Component,
description='A list of components that are inside of the device' many=True,
'at the moment of this Snapshot.' description='A list of components that are inside of the device'
'Order is preserved, so the component num 0 when' 'at the moment of this Snapshot.'
'submitting is the component num 0 when returning it back.') 'Order is preserved, so the component num 0 when'
'submitting is the component num 0 when returning it back.',
)
@validates_schema @validates_schema
def validate_workbench_version(self, data: dict): def validate_workbench_version(self, data: dict):
@ -391,16 +449,21 @@ class Snapshot(ActionWithOneDevice):
if data['version'] < app.config['MIN_WORKBENCH']: if data['version'] < app.config['MIN_WORKBENCH']:
raise ValidationError( raise ValidationError(
'Min. supported Workbench version is ' 'Min. supported Workbench version is '
'{} but yours is {}.'.format(app.config['MIN_WORKBENCH'], data['version']), '{} but yours is {}.'.format(
field_names=['version'] app.config['MIN_WORKBENCH'], data['version']
),
field_names=['version'],
) )
@validates_schema @validates_schema
def validate_components_only_workbench(self, data: dict): def validate_components_only_workbench(self, data: dict):
if (data['software'] != SnapshotSoftware.Workbench) and (data['software'] != SnapshotSoftware.WorkbenchAndroid): if (data['software'] != SnapshotSoftware.Workbench) and (
data['software'] != SnapshotSoftware.WorkbenchAndroid
):
if data.get('components', None) is not None: if data.get('components', None) is not None:
raise ValidationError('Only Workbench can add component info', raise ValidationError(
field_names=['components']) 'Only Workbench can add component info', field_names=['components']
)
@validates_schema @validates_schema
def validate_only_workbench_fields(self, data: dict): def validate_only_workbench_fields(self, data: dict):
@ -408,22 +471,32 @@ class Snapshot(ActionWithOneDevice):
# todo test # todo test
if data['software'] == SnapshotSoftware.Workbench: if data['software'] == SnapshotSoftware.Workbench:
if not data.get('uuid', None): if not data.get('uuid', None):
raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid', raise ValidationError(
field_names=['uuid']) 'Snapshots from Workbench and WorkbenchAndroid must have uuid',
field_names=['uuid'],
)
if data.get('elapsed', None) is None: if data.get('elapsed', None) is None:
raise ValidationError('Snapshots from Workbench must have elapsed', raise ValidationError(
field_names=['elapsed']) 'Snapshots from Workbench must have elapsed',
field_names=['elapsed'],
)
elif data['software'] == SnapshotSoftware.WorkbenchAndroid: elif data['software'] == SnapshotSoftware.WorkbenchAndroid:
if not data.get('uuid', None): if not data.get('uuid', None):
raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid', raise ValidationError(
field_names=['uuid']) 'Snapshots from Workbench and WorkbenchAndroid must have uuid',
field_names=['uuid'],
)
else: else:
if data.get('uuid', None): if data.get('uuid', None):
raise ValidationError('Only Snapshots from Workbench or WorkbenchAndroid can have uuid', raise ValidationError(
field_names=['uuid']) 'Only Snapshots from Workbench or WorkbenchAndroid can have uuid',
field_names=['uuid'],
)
if data.get('elapsed', None): if data.get('elapsed', None):
raise ValidationError('Only Snapshots from Workbench can have elapsed', raise ValidationError(
field_names=['elapsed']) 'Only Snapshots from Workbench can have elapsed',
field_names=['elapsed'],
)
class ToRepair(ActionWithMultipleDevicesCheckingOwner): class ToRepair(ActionWithMultipleDevicesCheckingOwner):
@ -440,16 +513,20 @@ class Ready(ActionWithMultipleDevicesCheckingOwner):
class ActionStatus(Action): class ActionStatus(Action):
rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',)) rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
devices = NestedOn(s_device.Device, devices = NestedOn(
many=True, s_device.Device,
required=False, # todo test ensuring len(devices) >= 1 many=True,
only_query='id', required=False, # todo test ensuring len(devices) >= 1
collection_class=OrderedSet) only_query='id',
documents = NestedOn(s_document.TradeDocument, collection_class=OrderedSet,
many=True, )
required=False, # todo test ensuring len(devices) >= 1 documents = NestedOn(
only_query='id', s_document.TradeDocument,
collection_class=OrderedSet) many=True,
required=False, # todo test ensuring len(devices) >= 1
only_query='id',
collection_class=OrderedSet,
)
@pre_load @pre_load
def put_devices(self, data: dict): def put_devices(self, data: dict):
@ -508,20 +585,28 @@ class Live(ActionWithOneDevice):
See docs for more info. See docs for more info.
""" """
uuid = UUID() uuid = UUID()
software = EnumField(SnapshotSoftware, software = EnumField(
required=True, SnapshotSoftware,
description='The software that generated this Snapshot.') required=True,
description='The software that generated this Snapshot.',
)
version = Version(required=True, description='The version of the software.') version = Version(required=True, description='The version of the software.')
final_user_code = SanitizedStr(data_key="finalUserCode", dump_only=True) final_user_code = SanitizedStr(data_key="finalUserCode", dump_only=True)
licence_version = Version(required=True, description='The version of the software.') licence_version = Version(required=True, description='The version of the software.')
components = NestedOn(s_device.Component, components = NestedOn(
many=True, s_device.Component,
description='A list of components that are inside of the device' many=True,
'at the moment of this Snapshot.' description='A list of components that are inside of the device'
'Order is preserved, so the component num 0 when' 'at the moment of this Snapshot.'
'submitting is the component num 0 when returning it back.') 'Order is preserved, so the component num 0 when'
usage_time_allocate = TimeDelta(data_key='usageTimeAllocate', required=False, 'submitting is the component num 0 when returning it back.',
precision=TimeDelta.HOURS, dump_only=True) )
usage_time_allocate = TimeDelta(
data_key='usageTimeAllocate',
required=False,
precision=TimeDelta.HOURS,
dump_only=True,
)
class Organize(ActionWithMultipleDevices): class Organize(ActionWithMultipleDevices):
@ -570,7 +655,7 @@ class Revoke(ActionWithMultipleDevices):
@validates_schema @validates_schema
def validate_documents(self, data): def validate_documents(self, data):
"""Check if there are or no one before confirmation, """Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable This is not checked in the view becouse the list of documents is inmutable
""" """
if not data['devices'] == OrderedSet(): if not data['devices'] == OrderedSet():
@ -610,7 +695,7 @@ class ConfirmDocument(ActionWithMultipleDocuments):
@validates_schema @validates_schema
def validate_documents(self, data): def validate_documents(self, data):
"""If there are one device than have one confirmation, """If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action then remove the list this device of the list of devices of this action
""" """
if data['documents'] == OrderedSet(): if data['documents'] == OrderedSet():
return return
@ -636,7 +721,7 @@ class RevokeDocument(ActionWithMultipleDocuments):
@validates_schema @validates_schema
def validate_documents(self, data): def validate_documents(self, data):
"""Check if there are or no one before confirmation, """Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable This is not checked in the view becouse the list of documents is inmutable
""" """
if data['documents'] == OrderedSet(): if data['documents'] == OrderedSet():
@ -663,7 +748,7 @@ class ConfirmRevokeDocument(ActionWithMultipleDocuments):
@validates_schema @validates_schema
def validate_documents(self, data): def validate_documents(self, data):
"""Check if there are or no one before confirmation, """Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable This is not checked in the view becouse the list of documents is inmutable
""" """
if data['documents'] == OrderedSet(): if data['documents'] == OrderedSet():
@ -691,26 +776,23 @@ class Trade(ActionWithMultipleDevices):
validate=Length(max=STR_SIZE), validate=Length(max=STR_SIZE),
data_key='userToEmail', data_key='userToEmail',
missing='', missing='',
required=False required=False,
) )
user_to = NestedOn(s_user.User, dump_only=True, data_key='userTo') user_to = NestedOn(s_user.User, dump_only=True, data_key='userTo')
user_from_email = SanitizedStr( user_from_email = SanitizedStr(
validate=Length(max=STR_SIZE), validate=Length(max=STR_SIZE),
data_key='userFromEmail', data_key='userFromEmail',
missing='', missing='',
required=False required=False,
) )
user_from = NestedOn(s_user.User, dump_only=True, data_key='userFrom') user_from = NestedOn(s_user.User, dump_only=True, data_key='userFrom')
code = SanitizedStr(validate=Length(max=STR_SIZE), data_key='code', required=False) code = SanitizedStr(validate=Length(max=STR_SIZE), data_key='code', required=False)
confirm = Boolean( confirm = Boolean(
data_key='confirms', data_key='confirms',
missing=True, missing=True,
description="""If you need confirmation of the user you need actevate this field""" description="""If you need confirmation of the user you need actevate this field""",
) )
lot = NestedOn('Lot', lot = NestedOn('Lot', many=False, required=True, only_query='id')
many=False,
required=True,
only_query='id')
@pre_load @pre_load
def adding_devices(self, data: dict): def adding_devices(self, data: dict):

View File

@ -1,18 +1,20 @@
""" This is the view for Snapshots """ """ This is the view for Snapshots """
import os
import json import json
import os
import shutil import shutil
from datetime import datetime from datetime import datetime
from flask import current_app as app, g from flask import current_app as app
from flask import g
from marshmallow import ValidationError
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import RateComputer, Snapshot from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.device.models import Computer from ereuse_devicehub.resources.device.models import Computer
from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.enums import SnapshotSoftware, Severity from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
from ereuse_devicehub.resources.user.exceptions import InsufficientPermission from ereuse_devicehub.resources.user.exceptions import InsufficientPermission
@ -59,48 +61,35 @@ def move_json(tmp_snapshots, path_name, user, live=False):
os.remove(path_name) os.remove(path_name)
class SnapshotView(): class SnapshotMixin:
"""Performs a Snapshot. sync = Sync()
See `Snapshot` section in docs for more info. def build(self, snapshot_json=None): # noqa: C901
""" if not snapshot_json:
# Note that if we set the device / components into the snapshot snapshot_json = self.snapshot_json
# model object, when we flush them to the db we will flush device = snapshot_json.pop('device') # type: Computer
# snapshot, and we want to wait to flush snapshot at the end
def __init__(self, snapshot_json: dict, resource_def, schema):
self.schema = schema
self.resource_def = resource_def
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
snapshot_json.pop('debug', None)
self.snapshot_json = resource_def.schema.load(snapshot_json)
self.response = self.build()
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
def post(self):
return self.response
def build(self):
device = self.snapshot_json.pop('device') # type: Computer
components = None components = None
if self.snapshot_json['software'] == (SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid): if snapshot_json['software'] == (
components = self.snapshot_json.pop('components', None) # type: List[Component] SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid
):
components = snapshot_json.pop('components', None) # type: List[Component]
if isinstance(device, Computer) and device.hid: if isinstance(device, Computer) and device.hid:
device.add_mac_to_hid(components_snap=components) device.add_mac_to_hid(components_snap=components)
snapshot = Snapshot(**self.snapshot_json) snapshot = Snapshot(**snapshot_json)
# Remove new actions from devices so they don't interfere with sync # Remove new actions from devices so they don't interfere with sync
actions_device = set(e for e in device.actions_one) actions_device = set(e for e in device.actions_one)
device.actions_one.clear() device.actions_one.clear()
if components: if components:
actions_components = tuple(set(e for e in c.actions_one) for c in components) actions_components = tuple(
set(e for e in c.actions_one) for c in components
)
for component in components: for component in components:
component.actions_one.clear() component.actions_one.clear()
assert not device.actions_one assert not device.actions_one
assert all(not c.actions_one for c in components) if components else True assert all(not c.actions_one for c in components) if components else True
db_device, remove_actions = self.resource_def.sync.run(device, components) db_device, remove_actions = self.sync.run(device, components)
del device # Do not use device anymore del device # Do not use device anymore
snapshot.device = db_device snapshot.device = db_device
@ -120,24 +109,63 @@ class SnapshotView():
# Check ownership of (non-component) device to from current.user # Check ownership of (non-component) device to from current.user
if db_device.owner_id != g.user.id: if db_device.owner_id != g.user.id:
raise InsufficientPermission() raise InsufficientPermission()
# Compute ratings
try:
rate_computer, price = RateComputer.compute(db_device)
except CannotRate:
pass
else:
snapshot.actions.add(rate_computer)
if price:
snapshot.actions.add(price)
elif snapshot.software == SnapshotSoftware.WorkbenchAndroid: elif snapshot.software == SnapshotSoftware.WorkbenchAndroid:
pass # TODO try except to compute RateMobile pass # TODO try except to compute RateMobile
# Check if HID is null and add Severity:Warning to Snapshot # Check if HID is null and add Severity:Warning to Snapshot
if snapshot.device.hid is None: if snapshot.device.hid is None:
snapshot.severity = Severity.Warning snapshot.severity = Severity.Warning
return snapshot
class SnapshotView(SnapshotMixin):
"""Performs a Snapshot.
See `Snapshot` section in docs for more info.
"""
# Note that if we set the device / components into the snapshot
# model object, when we flush them to the db we will flush
# snapshot, and we want to wait to flush snapshot at the end
def __init__(self, snapshot_json: dict, resource_def, schema):
from ereuse_devicehub.parser.models import SnapshotsLog
self.schema = schema
self.resource_def = resource_def
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
snapshot_json.pop('debug', None)
try:
self.snapshot_json = resource_def.schema.load(snapshot_json)
snapshot = self.build()
except Exception as err:
txt = "{}".format(err)
uuid = snapshot_json.get('uuid')
version = snapshot_json.get('version')
error = SnapshotsLog(
description=txt,
snapshot_uuid=uuid,
severity=Severity.Error,
version=str(version),
)
error.save(commit=True)
raise err
db.session.add(snapshot) db.session.add(snapshot)
snap_log = SnapshotsLog(
description='Ok',
snapshot_uuid=snapshot.uuid,
severity=Severity.Info,
version=str(snapshot.version),
snapshot=snapshot,
)
snap_log.save()
db.session().final_flush() db.session().final_flush()
ret = self.schema.jsonify(snapshot) # transform it back self.response = self.schema.jsonify(snapshot) # transform it back
ret.status_code = 201 self.response.status_code = 201
db.session.commit() db.session.commit()
return ret move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
def post(self):
return self.response

View File

@ -1,29 +1,34 @@
from flask import g from flask import g
from sqlalchemy.util import OrderedSet
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import (Trade, Confirm, from ereuse_devicehub.inventory.models import Transfer
Revoke, RevokeDocument, ConfirmDocument, from ereuse_devicehub.resources.action.models import (
ConfirmRevokeDocument) Confirm,
from ereuse_devicehub.resources.user.models import User ConfirmDocument,
ConfirmRevokeDocument,
Revoke,
RevokeDocument,
Trade,
)
from ereuse_devicehub.resources.lot.views import delete_from_trade from ereuse_devicehub.resources.lot.views import delete_from_trade
from ereuse_devicehub.resources.user.models import User
class TradeView(): class TradeView:
"""Handler for manager the trade action register from post """Handler for manager the trade action register from post
request_post = { request_post = {
'type': 'Trade', 'type': 'Trade',
'devices': [device_id], 'devices': [device_id],
'documents': [document_id], 'documents': [document_id],
'userFrom': user2.email, 'userFrom': user2.email,
'userTo': user.email, 'userTo': user.email,
'price': 10, 'price': 10,
'date': "2020-12-01T02:00:00+00:00", 'date': "2020-12-01T02:00:00+00:00",
'lot': lot['id'], 'lot': lot['id'],
'confirm': True, 'confirm': True,
} }
""" """
@ -37,6 +42,7 @@ class TradeView():
db.session.add(self.trade) db.session.add(self.trade)
self.create_confirmations() self.create_confirmations()
self.create_automatic_trade() self.create_automatic_trade()
self.create_transfer()
def post(self): def post(self):
db.session().final_flush() db.session().final_flush()
@ -52,15 +58,15 @@ class TradeView():
# owner of the lot # owner of the lot
if self.trade.confirm: if self.trade.confirm:
if self.trade.devices: if self.trade.devices:
confirm_devs = Confirm(user=g.user, confirm_devs = Confirm(
action=self.trade, user=g.user, action=self.trade, devices=self.trade.devices
devices=self.trade.devices) )
db.session.add(confirm_devs) db.session.add(confirm_devs)
if self.trade.documents: if self.trade.documents:
confirm_docs = ConfirmDocument(user=g.user, confirm_docs = ConfirmDocument(
action=self.trade, user=g.user, action=self.trade, documents=self.trade.documents
documents=self.trade.documents) )
db.session.add(confirm_docs) db.session.add(confirm_docs)
return return
@ -70,12 +76,12 @@ class TradeView():
txt = "You do not participate in this trading" txt = "You do not participate in this trading"
raise ValidationError(txt) raise ValidationError(txt)
confirm_from = Confirm(user=self.trade.user_from, confirm_from = Confirm(
action=self.trade, user=self.trade.user_from, action=self.trade, devices=self.trade.devices
devices=self.trade.devices) )
confirm_to = Confirm(user=self.trade.user_to, confirm_to = Confirm(
action=self.trade, user=self.trade.user_to, action=self.trade, devices=self.trade.devices
devices=self.trade.devices) )
db.session.add(confirm_from) db.session.add(confirm_from)
db.session.add(confirm_to) db.session.add(confirm_to)
@ -124,6 +130,25 @@ class TradeView():
db.session.add(user) db.session.add(user)
self.data['user_from'] = user self.data['user_from'] = user
def create_transfer(self):
code = self.trade.code
confirm = self.trade.confirm
lot = self.trade.lot
user_from = None
user_to = None
if not self.trade.user_from.phantom:
user_from = self.trade.user_from
if not self.trade.user_to.phantom:
user_to = self.trade.user_to
if (user_from and user_to) or not code or confirm:
return
self.transfer = Transfer(
code=code, user_from=user_from, user_to=user_to, lot=lot
)
db.session.add(self.transfer)
def create_automatic_trade(self) -> None: def create_automatic_trade(self) -> None:
# not do nothing if it's neccesary confirmation explicity # not do nothing if it's neccesary confirmation explicity
if self.trade.confirm: if self.trade.confirm:
@ -134,15 +159,15 @@ class TradeView():
dev.change_owner(self.trade.user_to) dev.change_owner(self.trade.user_to)
class ConfirmMixin(): class ConfirmMixin:
""" """
Very Important: Very Important:
============== ==============
All of this Views than inherit of this class is executed for users All of this Views than inherit of this class is executed for users
than is not owner of the Trade action. than is not owner of the Trade action.
The owner of Trade action executed this actions of confirm and revoke from the The owner of Trade action executed this actions of confirm and revoke from the
lot lot
""" """
@ -167,24 +192,27 @@ class ConfirmMixin():
class ConfirmView(ConfirmMixin): class ConfirmView(ConfirmMixin):
"""Handler for manager the Confirmation register from post """Handler for manager the Confirmation register from post
request_confirm = { request_confirm = {
'type': 'Confirm', 'type': 'Confirm',
'action': trade.id, 'action': trade.id,
'devices': [device_id] 'devices': [device_id]
} }
""" """
Model = Confirm Model = Confirm
def validate(self, data): def validate(self, data):
"""If there are one device than have one confirmation, """If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action then remove the list this device of the list of devices of this action
""" """
real_devices = [] real_devices = []
trade = data['action'] trade = data['action']
lot = trade.lot lot = trade.lot
for dev in data['devices']: for dev in data['devices']:
if dev.trading(lot, simple=True) not in ['NeedConfirmation', 'NeedConfirmRevoke']: if dev.trading(lot, simple=True) not in [
'NeedConfirmation',
'NeedConfirmRevoke',
]:
raise ValidationError('Some devices not possible confirm.') raise ValidationError('Some devices not possible confirm.')
# Change the owner for every devices # Change the owner for every devices
@ -197,11 +225,11 @@ class ConfirmView(ConfirmMixin):
class RevokeView(ConfirmMixin): class RevokeView(ConfirmMixin):
"""Handler for manager the Revoke register from post """Handler for manager the Revoke register from post
request_revoke = { request_revoke = {
'type': 'Revoke', 'type': 'Revoke',
'action': trade.id, 'action': trade.id,
'devices': [device_id], 'devices': [device_id],
} }
""" """
@ -223,15 +251,15 @@ class RevokeView(ConfirmMixin):
self.model = delete_from_trade(lot, devices) self.model = delete_from_trade(lot, devices)
class ConfirmDocumentMixin(): class ConfirmDocumentMixin:
""" """
Very Important: Very Important:
============== ==============
All of this Views than inherit of this class is executed for users All of this Views than inherit of this class is executed for users
than is not owner of the Trade action. than is not owner of the Trade action.
The owner of Trade action executed this actions of confirm and revoke from the The owner of Trade action executed this actions of confirm and revoke from the
lot lot
""" """
@ -256,18 +284,18 @@ class ConfirmDocumentMixin():
class ConfirmDocumentView(ConfirmDocumentMixin): class ConfirmDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Confirmation register from post """Handler for manager the Confirmation register from post
request_confirm = { request_confirm = {
'type': 'Confirm', 'type': 'Confirm',
'action': trade.id, 'action': trade.id,
'documents': [document_id], 'documents': [document_id],
} }
""" """
Model = ConfirmDocument Model = ConfirmDocument
def validate(self, data): def validate(self, data):
"""If there are one device than have one confirmation, """If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action then remove the list this device of the list of devices of this action
""" """
for doc in data['documents']: for doc in data['documents']:
ac = doc.trading ac = doc.trading
@ -280,11 +308,11 @@ class ConfirmDocumentView(ConfirmDocumentMixin):
class RevokeDocumentView(ConfirmDocumentMixin): class RevokeDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Revoke register from post """Handler for manager the Revoke register from post
request_revoke = { request_revoke = {
'type': 'Revoke', 'type': 'Revoke',
'action': trade.id, 'action': trade.id,
'documents': [document_id], 'documents': [document_id],
} }
""" """
@ -299,7 +327,9 @@ class RevokeDocumentView(ConfirmDocumentMixin):
for doc in data['documents']: for doc in data['documents']:
if not doc.trading in ['Document Confirmed', 'Confirm']: if not doc.trading in ['Document Confirmed', 'Confirm']:
txt = 'Some of documents do not have enough to confirm for to do a revoke' txt = (
'Some of documents do not have enough to confirm for to do a revoke'
)
ValidationError(txt) ValidationError(txt)
### End check ### ### End check ###
@ -307,11 +337,11 @@ class RevokeDocumentView(ConfirmDocumentMixin):
class ConfirmRevokeDocumentView(ConfirmDocumentMixin): class ConfirmRevokeDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Confirmation register from post """Handler for manager the Confirmation register from post
request_confirm_revoke = { request_confirm_revoke = {
'type': 'ConfirmRevoke', 'type': 'ConfirmRevoke',
'action': action_revoke.id, 'action': action_revoke.id,
'documents': [document_id], 'documents': [document_id],
} }
""" """

View File

@ -1,35 +1,49 @@
""" This is the view for Snapshots """ """ This is the view for Snapshots """
import jwt
import ereuse_utils
from datetime import timedelta from datetime import timedelta
from distutils.version import StrictVersion from distutils.version import StrictVersion
from uuid import UUID from uuid import UUID
from flask import current_app as app, request, g import ereuse_utils
import jwt
from flask import current_app as app
from flask import g, request
from teal.db import ResourceNotFound from teal.db import ResourceNotFound
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
from teal.resource import View from teal.resource import View
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.query import things_response from ereuse_devicehub.query import things_response
from ereuse_devicehub.resources.action.models import (Action, Snapshot, VisualTest, from ereuse_devicehub.resources.action.models import (
InitTransfer, Live, Allocate, Deallocate, Action,
Trade, Confirm, Revoke) Allocate,
Confirm,
Deallocate,
InitTransfer,
Live,
Revoke,
Snapshot,
Trade,
VisualTest,
)
from ereuse_devicehub.resources.action.views import trade as trade_view from ereuse_devicehub.resources.action.views import trade as trade_view
from ereuse_devicehub.resources.action.views.snapshot import SnapshotView, save_json, move_json
from ereuse_devicehub.resources.action.views.documents import ErasedView from ereuse_devicehub.resources.action.views.documents import ErasedView
from ereuse_devicehub.resources.device.models import Device, Computer, DataStorage from ereuse_devicehub.resources.action.views.snapshot import (
SnapshotView,
move_json,
save_json,
)
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.enums import Severity
SUPPORTED_WORKBENCH = StrictVersion('11.0') SUPPORTED_WORKBENCH = StrictVersion('11.0')
class AllocateMix(): class AllocateMix:
model = None model = None
def post(self): def post(self):
""" Create one res_obj """ """Create one res_obj"""
res_json = request.get_json() res_json = request.get_json()
res_obj = self.model(**res_json) res_obj = self.model(**res_json)
db.session.add(res_obj) db.session.add(res_obj)
@ -40,13 +54,18 @@ class AllocateMix():
return ret return ret
def find(self, args: dict): def find(self, args: dict):
res_objs = self.model.query.filter_by(author=g.user) \ res_objs = (
.order_by(self.model.created.desc()) \ self.model.query.filter_by(author=g.user)
.order_by(self.model.created.desc())
.paginate(per_page=200) .paginate(per_page=200)
)
return things_response( return things_response(
self.schema.dump(res_objs.items, many=True, nested=0), self.schema.dump(res_objs.items, many=True, nested=0),
res_objs.page, res_objs.per_page, res_objs.total, res_objs.page,
res_objs.prev_num, res_objs.next_num res_objs.per_page,
res_objs.total,
res_objs.prev_num,
res_objs.next_num,
) )
@ -99,7 +118,9 @@ class LiveView(View):
if not serial_number: if not serial_number:
"""There aren't any disk""" """There aren't any disk"""
raise ResourceNotFound("There aren't any disk in this device {}".format(device)) raise ResourceNotFound(
"There aren't any disk in this device {}".format(device)
)
return usage_time_hdd, serial_number return usage_time_hdd, serial_number
def get_hid(self, snapshot): def get_hid(self, snapshot):
@ -109,8 +130,11 @@ class LiveView(View):
return None return None
if not components: if not components:
return device.hid return device.hid
macs = [c.serial_number for c in components macs = [
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.sort() macs.sort()
mac = '' mac = ''
hid = device.hid hid = device.hid
@ -124,12 +148,10 @@ class LiveView(View):
def live(self, snapshot): def live(self, snapshot):
"""If the device.allocated == True, then this snapshot create an action live.""" """If the device.allocated == True, then this snapshot create an action live."""
hid = self.get_hid(snapshot) hid = self.get_hid(snapshot)
if not hid or not Device.query.filter( if not hid or not Device.query.filter(Device.hid == hid).count():
Device.hid == hid).count():
raise ValidationError('Device not exist.') raise ValidationError('Device not exist.')
device = Device.query.filter( device = Device.query.filter(Device.hid == hid, Device.allocated == True).one()
Device.hid == hid, Device.allocated == True).one()
# Is not necessary # Is not necessary
if not device: if not device:
raise ValidationError('Device not exist.') raise ValidationError('Device not exist.')
@ -138,16 +160,18 @@ class LiveView(View):
usage_time_hdd, serial_number = self.get_hdd_details(snapshot, device) usage_time_hdd, serial_number = self.get_hdd_details(snapshot, device)
data_live = {'usage_time_hdd': usage_time_hdd, data_live = {
'serial_number': serial_number, 'usage_time_hdd': usage_time_hdd,
'snapshot_uuid': snapshot['uuid'], 'serial_number': serial_number,
'description': '', 'snapshot_uuid': snapshot['uuid'],
'software': snapshot['software'], 'description': '',
'software_version': snapshot['version'], 'software': snapshot['software'],
'licence_version': snapshot['licence_version'], 'software_version': snapshot['version'],
'author_id': device.owner_id, 'licence_version': snapshot['licence_version'],
'agent_id': device.owner.individual.id, 'author_id': device.owner_id,
'device': device} 'agent_id': device.owner.individual.id,
'device': device,
}
live = Live(**data_live) live = Live(**data_live)
@ -172,7 +196,12 @@ class LiveView(View):
def decode_snapshot(data): def decode_snapshot(data):
try: try:
return jwt.decode(data['data'], app.config['JWT_PASS'], algorithms="HS256", json_encoder=ereuse_utils.JSONEncoder) return jwt.decode(
data['data'],
app.config['JWT_PASS'],
algorithms="HS256",
json_encoder=ereuse_utils.JSONEncoder,
)
except jwt.exceptions.InvalidSignatureError as err: except jwt.exceptions.InvalidSignatureError as err:
txt = 'Invalid snapshot' txt = 'Invalid snapshot'
raise ValidationError(txt) raise ValidationError(txt)
@ -200,13 +229,13 @@ class ActionView(View):
# TODO @cayop uncomment at four weeks # TODO @cayop uncomment at four weeks
# if not 'data' in json: # if not 'data' in json:
# txt = 'Invalid snapshot' # txt = 'Invalid snapshot'
# raise ValidationError(txt) # raise ValidationError(txt)
# snapshot_data = decode_snapshot(json) # snapshot_data = decode_snapshot(json)
snapshot_data = json snapshot_data = json
if 'data' in json: if 'data' in json and isinstance(json['data'], str):
snapshot_data = decode_snapshot(json) snapshot_data = decode_snapshot(json)
if not snapshot_data: if not snapshot_data:
@ -248,7 +277,9 @@ class ActionView(View):
return confirm.post() return confirm.post()
if json['type'] == 'ConfirmRevokeDocument': if json['type'] == 'ConfirmRevokeDocument':
confirm_revoke = trade_view.ConfirmRevokeDocumentView(json, resource_def, self.schema) confirm_revoke = trade_view.ConfirmRevokeDocumentView(
json, resource_def, self.schema
)
return confirm_revoke.post() return confirm_revoke.post()
if json['type'] == 'DataWipe': if json['type'] == 'DataWipe':

View File

@ -812,6 +812,7 @@ class Computer(Device):
transfer_state.comment = TransferState.__doc__ transfer_state.comment = TransferState.__doc__
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True) receiver_id = db.Column(UUID(as_uuid=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)
uuid = db.Column(UUID(as_uuid=True), nullable=True)
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
if args: if args:

View File

@ -136,6 +136,7 @@ class Computer(Device):
owner_id = UUID(data_key='ownerID') owner_id = UUID(data_key='ownerID')
transfer_state = EnumField(enums.TransferState, description=m.Computer.transfer_state.comment) transfer_state = EnumField(enums.TransferState, description=m.Computer.transfer_state.comment)
receiver_id = UUID(data_key='receiverID') receiver_id = UUID(data_key='receiverID')
uuid = UUID(required=False)
class Desktop(Computer): class Desktop(Computer):

View File

@ -8,6 +8,7 @@ import inflection
@unique @unique
class SnapshotSoftware(Enum): class SnapshotSoftware(Enum):
"""The software used to perform the Snapshot.""" """The software used to perform the Snapshot."""
Workbench = 'Workbench' Workbench = 'Workbench'
WorkbenchAndroid = 'WorkbenchAndroid' WorkbenchAndroid = 'WorkbenchAndroid'
AndroidApp = 'AndroidApp' AndroidApp = 'AndroidApp'
@ -36,6 +37,7 @@ class RatingRange(IntEnum):
3. Medium. 3. Medium.
4. High. 4. High.
""" """
VERY_LOW = 1 VERY_LOW = 1
LOW = 2 LOW = 2
MEDIUM = 3 MEDIUM = 3
@ -69,6 +71,7 @@ class PriceSoftware(Enum):
@unique @unique
class AppearanceRange(Enum): class AppearanceRange(Enum):
"""Grades the imperfections that aesthetically affect the device, but not its usage.""" """Grades the imperfections that aesthetically affect the device, but not its usage."""
Z = 'Z. The device is new' Z = 'Z. The device is new'
A = 'A. Is like new; without visual damage' A = 'A. Is like new; without visual damage'
B = 'B. Is in really good condition; small visual damage in difficult places to spot' B = 'B. Is in really good condition; small visual damage in difficult places to spot'
@ -83,6 +86,7 @@ class AppearanceRange(Enum):
@unique @unique
class FunctionalityRange(Enum): class FunctionalityRange(Enum):
"""Grades the defects of a device that affect its usage.""" """Grades the defects of a device that affect its usage."""
A = 'A. All the buttons works perfectly, no screen/camera defects and chassis without usage issues' A = 'A. All the buttons works perfectly, no screen/camera defects and chassis without usage issues'
B = 'B. There is a button difficult to press or unstable it, a screen/camera defect or chassis problem' B = 'B. There is a button difficult to press or unstable it, a screen/camera defect or chassis problem'
C = 'C. Chassis defects or multiple buttons don\'t work; broken or unusable it, some screen/camera defect' C = 'C. Chassis defects or multiple buttons don\'t work; broken or unusable it, some screen/camera defect'
@ -95,6 +99,7 @@ class FunctionalityRange(Enum):
@unique @unique
class BatteryHealthRange(Enum): class BatteryHealthRange(Enum):
"""Grade the battery health status, depending on self report Android system""" """Grade the battery health status, depending on self report Android system"""
A = 'A. The battery health is very good' A = 'A. The battery health is very good'
B = 'B. Battery health is good' B = 'B. Battery health is good'
C = 'C. Battery health is overheat / over voltage status but can stand the minimum duration' C = 'C. Battery health is overheat / over voltage status but can stand the minimum duration'
@ -109,6 +114,7 @@ class BatteryHealthRange(Enum):
@unique @unique
class BiosAccessRange(Enum): class BiosAccessRange(Enum):
"""How difficult it has been to set the bios to boot from the network.""" """How difficult it has been to set the bios to boot from the network."""
A = 'A. If by pressing a key you could access a boot menu with the network boot' A = 'A. If by pressing a key you could access a boot menu with the network boot'
B = 'B. You had to get into the BIOS, and in less than 5 steps you could set the network boot' B = 'B. You had to get into the BIOS, and in less than 5 steps you could set the network boot'
C = 'C. Like B, but with more than 5 steps' C = 'C. Like B, but with more than 5 steps'
@ -139,6 +145,7 @@ class ImageSoftware(Enum):
@unique @unique
class ImageMimeTypes(Enum): class ImageMimeTypes(Enum):
"""Supported image Mimetypes for Devicehub.""" """Supported image Mimetypes for Devicehub."""
jpg = 'image/jpeg' jpg = 'image/jpeg'
png = 'image/png' png = 'image/png'
@ -149,6 +156,7 @@ BOX_RATE_3 = 1, 3
# After looking at own databases # After looking at own databases
@unique @unique
class RamInterface(Enum): class RamInterface(Enum):
""" """
@ -163,6 +171,7 @@ class RamInterface(Enum):
here for those cases where there is no more specific information. here for those cases where there is no more specific information.
Please, try to always use DDRø-6 denominations. Please, try to always use DDRø-6 denominations.
""" """
SDRAM = 'SDRAM' SDRAM = 'SDRAM'
DDR = 'DDR SDRAM' DDR = 'DDR SDRAM'
DDR2 = 'DDR2 SDRAM' DDR2 = 'DDR2 SDRAM'
@ -170,6 +179,7 @@ class RamInterface(Enum):
DDR4 = 'DDR4 SDRAM' DDR4 = 'DDR4 SDRAM'
DDR5 = 'DDR5 SDRAM' DDR5 = 'DDR5 SDRAM'
DDR6 = 'DDR6 SDRAM' DDR6 = 'DDR6 SDRAM'
LPDDR3 = 'LPDDR3'
def __str__(self): def __str__(self):
return self.value return self.value
@ -189,6 +199,7 @@ class DataStorageInterface(Enum):
ATA = 'ATA' ATA = 'ATA'
USB = 'USB' USB = 'USB'
PCI = 'PCI' PCI = 'PCI'
NVME = 'NVME'
def __str__(self): def __str__(self):
return self.value return self.value
@ -211,6 +222,7 @@ class DisplayTech(Enum):
@unique @unique
class ComputerChassis(Enum): class ComputerChassis(Enum):
"""The chassis of a computer.""" """The chassis of a computer."""
Tower = 'Tower' Tower = 'Tower'
Docking = 'Docking' Docking = 'Docking'
AllInOne = 'All in one' AllInOne = 'All in one'
@ -235,6 +247,7 @@ class ReceiverRole(Enum):
The role that the receiver takes in the reception; The role that the receiver takes in the reception;
the meaning of the reception. the meaning of the reception.
""" """
Intermediary = 'Generic user in the workflow of the device.' Intermediary = 'Generic user in the workflow of the device.'
FinalUser = 'The user that will use the device.' FinalUser = 'The user that will use the device.'
CollectionPoint = 'A collection point.' CollectionPoint = 'A collection point.'
@ -244,6 +257,7 @@ class ReceiverRole(Enum):
class PrinterTechnology(Enum): class PrinterTechnology(Enum):
"""Technology of the printer.""" """Technology of the printer."""
Toner = 'Toner / Laser' Toner = 'Toner / Laser'
Inkjet = 'Liquid inkjet' Inkjet = 'Liquid inkjet'
SolidInk = 'Solid ink' SolidInk = 'Solid ink'
@ -260,6 +274,7 @@ class CameraFacing(Enum):
@unique @unique
class BatteryHealth(Enum): class BatteryHealth(Enum):
"""The battery health status as in Android.""" """The battery health status as in Android."""
Cold = 'Cold' Cold = 'Cold'
Dead = 'Dead' Dead = 'Dead'
Good = 'Good' Good = 'Good'
@ -274,6 +289,7 @@ class BatteryTechnology(Enum):
https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power
adding ``Alkaline``. adding ``Alkaline``.
""" """
LiIon = 'Lithium-ion' LiIon = 'Lithium-ion'
NiCd = 'Nickel-Cadmium' NiCd = 'Nickel-Cadmium'
NiMH = 'Nickel-metal hydride' NiMH = 'Nickel-metal hydride'
@ -329,10 +345,11 @@ class PhysicalErasureMethod(Enum):
and non able to be re-built. and non able to be re-built.
""" """
Shred = 'Reduction of the data-storage to the required certified ' \ Shred = 'Reduction of the data-storage to the required certified ' 'standard sizes.'
'standard sizes.' Disintegration = (
Disintegration = 'Reduction of the data-storage to smaller sizes ' \ 'Reduction of the data-storage to smaller sizes '
'than the certified standard ones.' 'than the certified standard ones.'
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -362,20 +379,21 @@ class ErasureStandards(Enum):
def from_data_storage(cls, erasure) -> Set['ErasureStandards']: def from_data_storage(cls, erasure) -> Set['ErasureStandards']:
"""Returns a set of erasure standards.""" """Returns a set of erasure standards."""
from ereuse_devicehub.resources.action import models as actions from ereuse_devicehub.resources.action import models as actions
standards = set() standards = set()
if isinstance(erasure, actions.EraseSectors): if isinstance(erasure, actions.EraseSectors):
with suppress(ValueError): with suppress(ValueError):
first_step, *other_steps = erasure.steps first_step, *other_steps = erasure.steps
if isinstance(first_step, actions.StepZero) \ if isinstance(first_step, actions.StepZero) and all(
and all(isinstance(step, actions.StepRandom) for step in other_steps): isinstance(step, actions.StepRandom) for step in other_steps
):
standards.add(cls.HMG_IS5) standards.add(cls.HMG_IS5)
return standards return standards
@unique @unique
class TransferState(IntEnum): class TransferState(IntEnum):
"""State of transfer for a given Lot of devices. """State of transfer for a given Lot of devices."""
"""
""" """
* Initial: No transfer action in place. * Initial: No transfer action in place.

View File

@ -5,12 +5,11 @@ from typing import Union
from boltons import urlutils from boltons import urlutils
from citext import CIText from citext import CIText
from flask import g from flask import g
from flask_login import current_user
from sqlalchemy import TEXT from sqlalchemy import TEXT
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import LtreeType from sqlalchemy_utils import LtreeType
from sqlalchemy_utils.types.ltree import LQUERY from sqlalchemy_utils.types.ltree import LQUERY
from teal.db import CASCADE_OWN, UUIDLtree, check_range, IntEnum from teal.db import CASCADE_OWN, IntEnum, UUIDLtree, check_range
from teal.resource import url_for_resource from teal.resource import url_for_resource
from ereuse_devicehub.db import create_view, db, exp, f from ereuse_devicehub.db import create_view, db, exp, f
@ -21,70 +20,88 @@ from ereuse_devicehub.resources.user.models import User
class Lot(Thing): class Lot(Thing):
id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default id = db.Column(
UUID(as_uuid=True), primary_key=True
) # uuid is generated on init by default
name = db.Column(CIText(), nullable=False) name = db.Column(CIText(), nullable=False)
description = db.Column(CIText()) description = db.Column(CIText())
description.comment = """A comment about the lot.""" description.comment = """A comment about the lot."""
closed = db.Column(db.Boolean, default=False, nullable=False) closed = db.Column(db.Boolean, default=False, nullable=False)
closed.comment = """A closed lot cannot be modified anymore.""" closed.comment = """A closed lot cannot be modified anymore."""
devices = db.relationship(Device, devices = db.relationship(
backref=db.backref('lots', lazy=True, collection_class=set), Device,
secondary=lambda: LotDevice.__table__, backref=db.backref('lots', lazy=True, collection_class=set),
lazy=True, secondary=lambda: LotDevice.__table__,
collection_class=set) lazy=True,
collection_class=set,
)
"""The **children** devices that the lot has. """The **children** devices that the lot has.
Note that the lot can have more devices, if they are inside Note that the lot can have more devices, if they are inside
descendant lots. descendant lots.
""" """
parents = db.relationship(lambda: Lot, parents = db.relationship(
viewonly=True, lambda: Lot,
lazy=True, viewonly=True,
collection_class=set, lazy=True,
secondary=lambda: LotParent.__table__, collection_class=set,
primaryjoin=lambda: Lot.id == LotParent.child_id, secondary=lambda: LotParent.__table__,
secondaryjoin=lambda: LotParent.parent_id == Lot.id, primaryjoin=lambda: Lot.id == LotParent.child_id,
cascade='refresh-expire', # propagate changes outside ORM secondaryjoin=lambda: LotParent.parent_id == Lot.id,
backref=db.backref('children', cascade='refresh-expire', # propagate changes outside ORM
viewonly=True, backref=db.backref(
lazy=True, 'children',
cascade='refresh-expire', viewonly=True,
collection_class=set) lazy=True,
) cascade='refresh-expire',
collection_class=set,
),
)
"""The parent lots.""" """The parent lots."""
all_devices = db.relationship(Device, all_devices = db.relationship(
viewonly=True, Device,
lazy=True, viewonly=True,
collection_class=set, lazy=True,
secondary=lambda: LotDeviceDescendants.__table__, collection_class=set,
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id, secondary=lambda: LotDeviceDescendants.__table__,
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id) primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id,
)
"""All devices, including components, inside this lot and its """All devices, including components, inside this lot and its
descendants. descendants.
""" """
amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0) amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0)
owner_id = db.Column(UUID(as_uuid=True), owner_id = db.Column(
db.ForeignKey(User.id), UUID(as_uuid=True),
nullable=False, db.ForeignKey(User.id),
default=lambda: g.user.id) nullable=False,
default=lambda: g.user.id,
)
owner = db.relationship(User, primaryjoin=owner_id == User.id) owner = 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_address = db.Column(CIText(), receiver_address = db.Column(
db.ForeignKey(User.email), CIText(),
nullable=False, db.ForeignKey(User.email),
default=lambda: g.user.email) nullable=False,
default=lambda: g.user.email,
)
receiver = db.relationship(User, primaryjoin=receiver_address == User.email) receiver = db.relationship(User, primaryjoin=receiver_address == User.email)
def __init__(self, name: str, closed: bool = closed.default.arg, def __init__(
description: str = None) -> None: self, name: str, closed: bool = closed.default.arg, description: str = None
) -> None:
"""Initializes a lot """Initializes a lot
:param name: :param name:
:param closed: :param closed:
""" """
super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description) super().__init__(
id=uuid.uuid4(), name=name, closed=closed, description=description
)
Path(self) # Lots have always one edge per default. Path(self) # Lots have always one edge per default.
@property @property
@ -102,20 +119,32 @@ class Lot(Thing):
@property @property
def is_temporary(self): def is_temporary(self):
return not bool(self.trade) return not bool(self.trade) and not bool(self.transfer)
@property @property
def is_incoming(self): def is_incoming(self):
return bool(self.trade and self.trade.user_to == current_user) if self.trade:
return self.trade.user_to == g.user
if self.transfer:
return self.transfer.user_to == g.user
return False
@property @property
def is_outgoing(self): def is_outgoing(self):
return bool(self.trade and self.trade.user_from == current_user) if self.trade:
return self.trade.user_from == g.user
if self.transfer:
return self.transfer.user_from == g.user
return False
@classmethod @classmethod
def descendantsq(cls, id): def descendantsq(cls, id):
_id = UUIDLtree.convert(id) _id = UUIDLtree.convert(id)
return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY)) return (cls.id == Path.lot_id) & Path.path.lquery(
exp.cast('*.{}.*'.format(_id), LQUERY)
)
@classmethod @classmethod
def roots(cls): def roots(cls):
@ -176,13 +205,17 @@ class Lot(Thing):
if isinstance(child, Lot): if isinstance(child, Lot):
return Path.has_lot(self.id, child.id) return Path.has_lot(self.id, child.id)
elif isinstance(child, Device): elif isinstance(child, Device):
device = db.session.query(LotDeviceDescendants) \ device = (
.filter(LotDeviceDescendants.device_id == child.id) \ db.session.query(LotDeviceDescendants)
.filter(LotDeviceDescendants.ancestor_lot_id == self.id) \ .filter(LotDeviceDescendants.device_id == child.id)
.filter(LotDeviceDescendants.ancestor_lot_id == self.id)
.one_or_none() .one_or_none()
)
return device return device
else: else:
raise TypeError('Lot only contains devices and lots, not {}'.format(child.__class__)) raise TypeError(
'Lot only contains devices and lots, not {}'.format(child.__class__)
)
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Lot {0.name} devices={0.devices!r}>'.format(self) return '<Lot {0.name} devices={0.devices!r}>'.format(self)
@ -192,35 +225,44 @@ class LotDevice(db.Model):
device_id = db.Column(db.BigInteger, db.ForeignKey(Device.id), primary_key=True) device_id = db.Column(db.BigInteger, db.ForeignKey(Device.id), primary_key=True)
lot_id = db.Column(UUID(as_uuid=True), db.ForeignKey(Lot.id), primary_key=True) lot_id = db.Column(UUID(as_uuid=True), db.ForeignKey(Lot.id), primary_key=True)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
author_id = db.Column(UUID(as_uuid=True), author_id = db.Column(
db.ForeignKey(User.id), UUID(as_uuid=True),
nullable=False, db.ForeignKey(User.id),
default=lambda: g.user.id) nullable=False,
default=lambda: g.user.id,
)
author = db.relationship(User, primaryjoin=author_id == User.id) author = db.relationship(User, primaryjoin=author_id == User.id)
author_id.comment = """The user that put the device in the lot.""" author_id.comment = """The user that put the device in the lot."""
class Path(db.Model): class Path(db.Model):
id = db.Column(db.UUID(as_uuid=True), id = db.Column(
primary_key=True, db.UUID(as_uuid=True),
server_default=db.text('gen_random_uuid()')) primary_key=True,
server_default=db.text('gen_random_uuid()'),
)
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
lot = db.relationship(Lot, lot = db.relationship(
backref=db.backref('paths', Lot,
lazy=True, backref=db.backref(
collection_class=set, 'paths', lazy=True, collection_class=set, cascade=CASCADE_OWN
cascade=CASCADE_OWN), ),
primaryjoin=Lot.id == lot_id) primaryjoin=Lot.id == lot_id,
)
path = db.Column(LtreeType, nullable=False) path = db.Column(LtreeType, nullable=False)
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')) created = db.Column(
db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')
)
created.comment = """When Devicehub created this.""" created.comment = """When Devicehub created this."""
__table_args__ = ( __table_args__ = (
# dag.delete_edge needs to disable internally/temporarily the unique constraint # dag.delete_edge needs to disable internally/temporarily the unique constraint
db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'), db.UniqueConstraint(
path, name='path_unique', deferrable=True, initially='immediate'
),
db.Index('path_gist', path, postgresql_using='gist'), db.Index('path_gist', path, postgresql_using='gist'),
db.Index('path_btree', path, postgresql_using='btree'), db.Index('path_btree', path, postgresql_using='btree'),
db.Index('lot_id_index', lot_id, postgresql_using='hash') db.Index('lot_id_index', lot_id, postgresql_using='hash'),
) )
def __init__(self, lot: Lot) -> None: def __init__(self, lot: Lot) -> None:
@ -243,7 +285,9 @@ class Path(db.Model):
child_id = UUIDLtree.convert(child_id) child_id = UUIDLtree.convert(child_id)
return bool( return bool(
db.session.execute( db.session.execute(
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(parent_id, child_id) "SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(
parent_id, child_id
)
).first() ).first()
) )
@ -263,47 +307,73 @@ class LotDeviceDescendants(db.Model):
"""Ancestor lot table.""" """Ancestor lot table."""
_desc = Lot.__table__.alias() _desc = Lot.__table__.alias()
"""Descendant lot table.""" """Descendant lot table."""
lot_device = _desc \ lot_device = _desc.join(LotDevice, _desc.c.id == LotDevice.lot_id).join(
.join(LotDevice, _desc.c.id == LotDevice.lot_id) \ Path, _desc.c.id == Path.lot_id
.join(Path, _desc.c.id == Path.lot_id) )
"""Join: Path -- Lot -- LotDevice""" """Join: Path -- Lot -- LotDevice"""
descendants = "path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') " \ descendants = (
"|| '.*' AS LQUERY))".format(_ancestor.name) "path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') "
"|| '.*' AS LQUERY))".format(_ancestor.name)
)
"""Query that gets the descendants of the ancestor lot.""" """Query that gets the descendants of the ancestor lot."""
devices = db.select([ devices = (
LotDevice.device_id, db.select(
_desc.c.id.label('parent_lot_id'), [
_ancestor.c.id.label('ancestor_lot_id'), LotDevice.device_id,
None _desc.c.id.label('parent_lot_id'),
]).select_from(_ancestor).select_from(lot_device).where(db.text(descendants)) _ancestor.c.id.label('ancestor_lot_id'),
None,
]
)
.select_from(_ancestor)
.select_from(lot_device)
.where(db.text(descendants))
)
# Components # Components
_parent_device = Device.__table__.alias(name='parent_device') _parent_device = Device.__table__.alias(name='parent_device')
"""The device that has the access to the lot.""" """The device that has the access to the lot."""
lot_device_component = lot_device \ lot_device_component = lot_device.join(
.join(_parent_device, _parent_device.c.id == LotDevice.device_id) \ _parent_device, _parent_device.c.id == LotDevice.device_id
.join(Component, _parent_device.c.id == Component.parent_id) ).join(Component, _parent_device.c.id == Component.parent_id)
"""Join: Path -- Lot -- LotDevice -- ParentDevice (Device) -- Component""" """Join: Path -- Lot -- LotDevice -- ParentDevice (Device) -- Component"""
components = db.select([ components = (
Component.id.label('device_id'), db.select(
_desc.c.id.label('parent_lot_id'), [
_ancestor.c.id.label('ancestor_lot_id'), Component.id.label('device_id'),
LotDevice.device_id.label('device_parent_id'), _desc.c.id.label('parent_lot_id'),
]).select_from(_ancestor).select_from(lot_device_component).where(db.text(descendants)) _ancestor.c.id.label('ancestor_lot_id'),
LotDevice.device_id.label('device_parent_id'),
]
)
.select_from(_ancestor)
.select_from(lot_device_component)
.where(db.text(descendants))
)
__table__ = create_view('lot_device_descendants', devices.union(components)) __table__ = create_view('lot_device_descendants', devices.union(components))
class LotParent(db.Model): class LotParent(db.Model):
i = f.index(Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_'))) i = f.index(
Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_'))
)
__table__ = create_view( __table__ = create_view(
'lot_parent', 'lot_parent',
db.select([ db.select(
Path.lot_id.label('child_id'), [
exp.cast(f.replace(exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'), Path.lot_id.label('child_id'),
UUID).label('parent_id') exp.cast(
]).select_from(Path).where(i > 0), f.replace(
exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'
),
UUID,
).label('parent_id'),
]
)
.select_from(Path)
.where(i > 0),
) )

View File

@ -1,20 +1,22 @@
import uuid import uuid
from sqlalchemy.util import OrderedSet
from collections import deque from collections import deque
from enum import Enum from enum import Enum
from typing import Dict, List, Set, Union from typing import Dict, List, Set, Union
import marshmallow as ma import marshmallow as ma
from flask import Response, jsonify, request, g from flask import Response, g, jsonify, request
from marshmallow import Schema as MarshmallowSchema, fields as f from marshmallow import Schema as MarshmallowSchema
from marshmallow import fields as f
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.util import OrderedSet
from teal.marshmallow import EnumField from teal.marshmallow import EnumField
from teal.resource import View from teal.resource import View
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.models import Transfer
from ereuse_devicehub.query import things_response from ereuse_devicehub.query import things_response
from ereuse_devicehub.resources.device.models import Device, Computer from ereuse_devicehub.resources.action.models import Confirm, Revoke, Trade
from ereuse_devicehub.resources.action.models import Trade, Confirm, Revoke from ereuse_devicehub.resources.device.models import Computer, Device
from ereuse_devicehub.resources.lot.models import Lot, Path from ereuse_devicehub.resources.lot.models import Lot, Path
@ -27,6 +29,7 @@ class LotView(View):
"""Allowed arguments for the ``find`` """Allowed arguments for the ``find``
method (GET collection) endpoint method (GET collection) endpoint
""" """
format = EnumField(LotFormat, missing=None) format = EnumField(LotFormat, missing=None)
search = f.Str(missing=None) search = f.Str(missing=None)
type = f.Str(missing=None) type = f.Str(missing=None)
@ -42,12 +45,26 @@ class LotView(View):
return ret return ret
def patch(self, id): def patch(self, id):
patch_schema = self.resource_def.SCHEMA(only=( patch_schema = self.resource_def.SCHEMA(
'name', 'description', 'transfer_state', 'receiver_address', 'amount', 'devices', only=(
'owner_address'), partial=True) 'name',
'description',
'transfer_state',
'receiver_address',
'amount',
'devices',
'owner_address',
),
partial=True,
)
l = request.get_json(schema=patch_schema) l = request.get_json(schema=patch_schema)
lot = Lot.query.filter_by(id=id).one() lot = Lot.query.filter_by(id=id).one()
device_fields = ['transfer_state', 'receiver_address', 'amount', 'owner_address'] device_fields = [
'transfer_state',
'receiver_address',
'amount',
'owner_address',
]
computers = [x for x in lot.all_devices if isinstance(x, Computer)] computers = [x for x in lot.all_devices if isinstance(x, Computer)]
for key, value in l.items(): for key, value in l.items():
setattr(lot, key, value) setattr(lot, key, value)
@ -84,7 +101,7 @@ class LotView(View):
ret = { ret = {
'items': {l['id']: l for l in lots}, 'items': {l['id']: l for l in lots},
'tree': self.ui_tree(), 'tree': self.ui_tree(),
'url': request.path 'url': request.path,
} }
else: else:
query = Lot.query query = Lot.query
@ -95,15 +112,28 @@ class LotView(View):
lots = query.paginate(per_page=6 if args['search'] else query.count()) lots = query.paginate(per_page=6 if args['search'] else query.count())
return things_response( return things_response(
self.schema.dump(lots.items, many=True, nested=2), self.schema.dump(lots.items, many=True, nested=2),
lots.page, lots.per_page, lots.total, lots.prev_num, lots.next_num lots.page,
lots.per_page,
lots.total,
lots.prev_num,
lots.next_num,
) )
return jsonify(ret) return jsonify(ret)
def visibility_filter(self, query): def visibility_filter(self, query):
query = query.outerjoin(Trade) \ query = (
.filter(or_(Trade.user_from == g.user, query.outerjoin(Trade)
Trade.user_to == g.user, .outerjoin(Transfer)
Lot.owner_id == g.user.id)) .filter(
or_(
Trade.user_from == g.user,
Trade.user_to == g.user,
Lot.owner_id == g.user.id,
Transfer.user_from == g.user,
Transfer.user_to == g.user,
)
)
)
return query return query
def type_filter(self, query, args): def type_filter(self, query, args):
@ -111,13 +141,23 @@ class LotView(View):
# temporary # temporary
if lot_type == "temporary": if lot_type == "temporary":
return query.filter(Lot.trade == None) return query.filter(Lot.trade == None).filter(Lot.transfer == None)
if lot_type == "incoming": if lot_type == "incoming":
return query.filter(Lot.trade and Trade.user_to == g.user) return query.filter(
or_(
Lot.trade and Trade.user_to == g.user,
Lot.transfer and Transfer.user_to == g.user,
)
).all()
if lot_type == "outgoing": if lot_type == "outgoing":
return query.filter(Lot.trade and Trade.user_from == g.user) return query.filter(
or_(
Lot.trade and Trade.user_from == g.user,
Lot.transfer and Transfer.user_from == g.user,
)
).all()
return query return query
@ -152,10 +192,7 @@ class LotView(View):
# does lot_id exist already in node? # does lot_id exist already in node?
node = next(part for part in nodes if lot_id == part['id']) node = next(part for part in nodes if lot_id == part['id'])
except StopIteration: except StopIteration:
node = { node = {'id': lot_id, 'nodes': []}
'id': lot_id,
'nodes': []
}
nodes.append(node) nodes.append(node)
if path: if path:
cls._p(node['nodes'], path) cls._p(node['nodes'], path)
@ -175,15 +212,17 @@ class LotView(View):
class LotBaseChildrenView(View): class LotBaseChildrenView(View):
"""Base class for adding / removing children devices and """Base class for adding / removing children devices and
lots from a lot. lots from a lot.
""" """
def __init__(self, definition: 'Resource', **kw) -> None: def __init__(self, definition: 'Resource', **kw) -> None:
super().__init__(definition, **kw) super().__init__(definition, **kw)
self.list_args = self.ListArgs() self.list_args = self.ListArgs()
def get_ids(self) -> Set[uuid.UUID]: def get_ids(self) -> Set[uuid.UUID]:
args = self.QUERY_PARSER.parse(self.list_args, request, locations=('querystring',)) args = self.QUERY_PARSER.parse(
self.list_args, request, locations=('querystring',)
)
return set(args['id']) return set(args['id'])
def get_lot(self, id: uuid.UUID) -> Lot: def get_lot(self, id: uuid.UUID) -> Lot:
@ -247,8 +286,9 @@ class LotDeviceView(LotBaseChildrenView):
if not ids: if not ids:
return return
devices = set(Device.query.filter(Device.id.in_(ids)).filter( devices = set(
Device.owner == g.user)) Device.query.filter(Device.id.in_(ids)).filter(Device.owner == g.user)
)
lot.devices.update(devices) lot.devices.update(devices)
@ -271,8 +311,9 @@ class LotDeviceView(LotBaseChildrenView):
txt = 'This is not your lot' txt = 'This is not your lot'
raise ma.ValidationError(txt) raise ma.ValidationError(txt)
devices = set(Device.query.filter(Device.id.in_(ids)).filter( devices = set(
Device.owner_id == g.user.id)) Device.query.filter(Device.id.in_(ids)).filter(Device.owner_id == g.user.id)
)
lot.devices.difference_update(devices) lot.devices.difference_update(devices)
@ -311,9 +352,7 @@ def delete_from_trade(lot: Lot, devices: List):
phantom = lot.trade.user_from phantom = lot.trade.user_from
phantom_revoke = Revoke( phantom_revoke = Revoke(
action=lot.trade, action=lot.trade, user=phantom, devices=set(without_confirms)
user=phantom,
devices=set(without_confirms)
) )
db.session.add(phantom_revoke) db.session.add(phantom_revoke)

View File

@ -116,6 +116,15 @@
</a> </a>
</li><!-- End Dashboard Nav --> </li><!-- End Dashboard Nav -->
<li class="nav-heading">Snapshots</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.snapshotslist') }}">
<i class="bi-menu-button-wide"></i>
<span>Uploaded Snapshots</span>
</a>
</li>
<li class="nav-heading">Devices</li> <li class="nav-heading">Devices</li>
<li class="nav-item"> <li class="nav-item">

View File

@ -37,10 +37,19 @@
<!-- Bordered Tabs --> <!-- Bordered Tabs -->
<div class="d-flex align-items-center justify-content-between row"> <div class="d-flex align-items-center justify-content-between row">
<h3 class="col-sm-12 col-md-5"><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3> <div class="col-sm-12 col-md-5">
<h3>
<a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a>
</h3>
{% if lot.transfer.code and lot.transfer.user_to and not lot.transfer.user_to.phantom %}
<span>{{ lot.transfer.code }} <i class="bi bi-arrow-right"></i> {{ lot.transfer.user_to.email }}</span>
{% elif lot.transfer.code and lot.transfer.user_from and not lot.transfer.user_from.phantom %}
<span>{{ lot.transfer.user_from.email }} <i class="bi bi-arrow-right"></i> {{ lot.transfer.code }}</span>
{% endif %}
</div>
<div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions --> <div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions -->
{% if lot.is_temporary %} {% if lot.is_temporary or not lot.transfer.closed %}
{% if 1 == 2 %}{# <!-- TODO (@slamora) Don't render Trade buttons until implementation is finished --> #} {% if 1 == 2 %}{# <!-- TODO (@slamora) Don't render Trade buttons until implementation is finished --> #}
<a class="me-2" href="javascript:newTrade('user_from')"> <a class="me-2" href="javascript:newTrade('user_from')">
@ -75,9 +84,38 @@
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button>
</li> </li>
{% if lot.transfer %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-transfer">
Transfer ({% if lot.transfer.closed %}<span class="text-danger">Closed</span>{% else %}<span class="text-success">Open</span>{% endif %})
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-delivery-note">
Delivery Note
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-receiver-note">
Receiver Note
</button>
</li>
{% endif %}
</ul> </ul>
{% endif %} {% endif %}
<div class="tab-content pt-1"> <div class="tab-content pt-1">
{% if lot and lot.is_temporary %}
<div class="tab-pane active show mb-5">
<a type="button" href="{{ url_for('inventory.new_transfer', lot_id=lot.id, type_id='outgoing') }}" class="btn btn-primary" style="float: right;">
Outgoing Transfer
</a>
<a type="button" href="{{ url_for('inventory.new_transfer', lot_id=lot.id, type_id='incoming') }}" class="btn btn-primary" style="float: right; margin-right: 15px;">
Incoming Transfer
</a>
<div style="display: block;"></div>
</div>
{% endif %}
<div id="devices-list" class="tab-pane fade devices-list active show"> <div id="devices-list" class="tab-pane fade devices-list active show">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label> <label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown ml-1">
@ -438,6 +476,103 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="edit-transfer" class="tab-pane fade edit-transfer">
<h5 class="card-title">Transfer</h5>
<form method="post" action="{{ url_for('inventory.edit_transfer', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_transfer.csrf_token }}
{% for field in form_transfer %}
{% if field != form_transfer.csrf_token %}
<div class="col-12">
{% if field != form_transfer.type %}
{{ field.label(class_="form-label") }}
{% if field == form_transfer.code %}
<span class="text-danger">*</span>
{% endif %}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<div id="edit-delivery-note" class="tab-pane fade edit-delivery-note">
<h5 class="card-title">Delivery Note</h5>
<form method="post" action="{{ url_for('inventory.delivery_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_delivery.csrf_token }}
{% for field in form_delivery %}
{% if field != form_delivery.csrf_token %}
<div class="col-12">
{% if field != form_delivery.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
<div id="edit-receiver-note" class="tab-pane fade edit-receiver-note">
<h5 class="card-title">Receiver Note</h5>
<form method="post" action="{{ url_for('inventory.receiver_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_receiver.csrf_token }}
{% for field in form_receiver %}
{% if field != form_receiver.csrf_token %}
<div class="col-12">
{% if field != form_receiver.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
{% endif %} {% endif %}
</div><!-- End Bordered Tabs --> </div><!-- End Bordered Tabs -->

View File

@ -0,0 +1,73 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<!-- TODO@slamora replace with lot list URL when exists -->
<li class="breadcrumb-item"><a href="#TODO-lot-list">Lots</a></li>
<li class="breadcrumb-item">Transfer</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div class="col-12">
{% if field != form.type %}
{{ field.label(class_="form-label") }}
{% if field == form.code %}
<span class="text-danger">*</span>
{% endif %}
{{ field }}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=form._tmp_lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

View File

@ -0,0 +1,54 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item active">{{ page_title }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body pt-3">
<h3>{{ snapshot_sid }} | {{ snapshot_uuid }}</h3>
<!-- Bordered Tabs -->
<div class="tab-content pt-2">
<div class="tab-pane fade show active">
<h5 class="card-title">Traceability log Details</h5>
<div class="list-group col-6">
{% for log in snapshots_log %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ log.get_status() }}</h5>
<small class="text-muted">{{ log.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
<p class="mb-1">
Device:
{{ log.get_device() }}<br />
Version: {{ log.version }}<br />
</p>
<p>
<small class="text-muted">
{{ log.description }}
</small>
</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock main %}

View File

@ -0,0 +1,88 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item active">Snapshots</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body pt-3" style="min-height: 650px;">
<!-- Bordered Tabs -->
<div class="tab-content pt-5">
<div id="devices-list" class="tab-pane fade devices-list active show">
<div class="tab-content pt-2">
<table class="table">
<thead>
<tr>
<th scope="col">SID</th>
<th scope="col">Snapshot id</th>
<th scope="col">Version</th>
<th scope="col">DHID</th>
<th scope="col">Status</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Time</th>
</tr>
</thead>
<tbody>
{% for snap in snapshots_log %}
<tr>
<td>
{% if snap.sid %}
<a href="{{ url_for('inventory.snapshot_detail', snapshot_uuid=snap.snapshot_uuid) }}">
{{ snap.sid }}
</a>
{% endif %}
</td>
<td>
<a href="{{ url_for('inventory.snapshot_detail', snapshot_uuid=snap.snapshot_uuid) }}">
{{ snap.snapshot_uuid }}
</a>
</td>
<td>
{{ snap.version }}
</td>
<td>
{% if snap.device %}
<a href="{{ url_for('inventory.device_details', id=snap.device) }}">
{{ snap.device }}
</a>
{% endif %}
</td>
<td>
{{ snap.status }}
</td>
<td>{{ snap.created.strftime('%H:%M %d-%m-%Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- End Bordered Tabs -->
</div>
</div>
</div>
<div id="NotificationsContainer" style="position: absolute; bottom: 0; right: 0; margin: 10px; margin-top: 70px; width: calc(100% - 310px);"></div>
</div>
</div>
</section>
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
</script>
{% endblock main %}

View File

@ -1,4 +1,4 @@
[settings] [settings]
TOKEN = {{ token }} DH_TOKEN = {{ token }}
URL = {{ url }} DH_URL = {{ url }}

View File

@ -49,7 +49,8 @@ class LogoutView(View):
return flask.redirect(flask.url_for('core.login')) return flask.redirect(flask.url_for('core.login'))
class GenericMixView(View): class GenericMixin(View):
methods = ['GET']
decorators = [login_required] decorators = [login_required]
def get_lots(self): def get_lots(self):
@ -74,7 +75,7 @@ class GenericMixView(View):
return self.context return self.context
class UserProfileView(GenericMixView): class UserProfileView(GenericMixin):
decorators = [login_required] decorators = [login_required]
template_name = 'ereuse_devicehub/user_profile.html' template_name = 'ereuse_devicehub/user_profile.html'

View File

@ -10,12 +10,12 @@ from ereuse_devicehub import auth
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.user.models import Session from ereuse_devicehub.resources.user.models import Session
from ereuse_devicehub.views import GenericMixView from ereuse_devicehub.views import GenericMixin
workbench = Blueprint('workbench', __name__, url_prefix='/workbench') workbench = Blueprint('workbench', __name__, url_prefix='/workbench')
class SettingsView(GenericMixView): class SettingsView(GenericMixin):
decorators = [login_required] decorators = [login_required]
template_name = 'workbench/settings.html' template_name = 'workbench/settings.html'
page_title = "Workbench Settings" page_title = "Workbench Settings"

View File

@ -5,6 +5,7 @@ Use this as a starting point.
""" """
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
from ereuse_devicehub.api.views import api
from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices from ereuse_devicehub.inventory.views import devices
@ -16,6 +17,7 @@ app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
app.register_blueprint(core) app.register_blueprint(core)
app.register_blueprint(devices) app.register_blueprint(devices)
app.register_blueprint(labels) app.register_blueprint(labels)
app.register_blueprint(api)
app.register_blueprint(workbench) app.register_blueprint(workbench)
# configure & enable CSRF of Flask-WTF # configure & enable CSRF of Flask-WTF

View File

@ -33,7 +33,7 @@ SQLAlchemy==1.3.24
SQLAlchemy-Utils==0.33.11 SQLAlchemy-Utils==0.33.11
teal==0.2.0a38 teal==0.2.0a38
webargs==5.5.3 webargs==5.5.3
Werkzeug==0.15.3 Werkzeug==0.15.5
sqlalchemy-citext==1.3.post0 sqlalchemy-citext==1.3.post0
flask-weasyprint==0.5 flask-weasyprint==0.5
weasyprint==44 weasyprint==44
@ -43,3 +43,5 @@ tqdm==4.32.2
python-decouple==3.3 python-decouple==3.3
python-dotenv==0.14.0 python-dotenv==0.14.0
pyjwt==2.4.0 pyjwt==2.4.0
pint==0.9
py-dmidecode==0.1.0

View File

@ -1,4 +1,5 @@
import io import io
import json
import uuid import uuid
from contextlib import redirect_stdout from contextlib import redirect_stdout
from datetime import datetime from datetime import datetime
@ -13,6 +14,7 @@ from decouple import config
from psycopg2 import IntegrityError from psycopg2 import IntegrityError
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from ereuse_devicehub.api.views import api
from ereuse_devicehub.client import Client, UserClient, UserClientFlask from ereuse_devicehub.client import Client, UserClient, UserClientFlask
from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -59,6 +61,7 @@ def _app(config: TestConfig) -> Devicehub:
app.register_blueprint(core) app.register_blueprint(core)
app.register_blueprint(devices) app.register_blueprint(devices)
app.register_blueprint(labels) app.register_blueprint(labels)
app.register_blueprint(api)
app.register_blueprint(workbench) app.register_blueprint(workbench)
app.config["SQLALCHEMY_RECORD_QUERIES"] = True app.config["SQLALCHEMY_RECORD_QUERIES"] = True
app.config['PROFILE'] = True app.config['PROFILE'] = True
@ -208,6 +211,11 @@ def file(name: str) -> dict:
return json_encode(yaml2json(name)) return json_encode(yaml2json(name))
def file_json(name):
with Path(__file__).parent.joinpath('files').joinpath(name).open() as f:
return json.loads(f.read())
def file_workbench(name: str) -> dict: def file_workbench(name: str) -> dict:
"""Opens and parses a YAML file from the ``files`` subdir.""" """Opens and parses a YAML file from the ``files`` subdir."""
with Path(__file__).parent.joinpath('workbench_files').joinpath( with Path(__file__).parent.joinpath('workbench_files').joinpath(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
Type;Chassis;Serial Number;Model;Manufacturer;Registered in;Physical state;Allocate state;Lifecycle state;Price;Processor;RAM (MB);Data Storage Size (MB);Rate;Range;Processor Rate;Processor Range;RAM Rate;RAM Range;Data Storage Rate;Data Storage Range Type;Chassis;Serial Number;Model;Manufacturer;Registered in;Physical state;Allocate state;Lifecycle state;Price;Processor;RAM (MB);Data Storage Size (MB)
Desktop;Microtower;d1s;d1ml;d1mr;Mon May 16 09:34:22 2022;;;;;p1ml;0;0;1.0;Very low;1.0;Very low;1.0;Very low;1.0;Very low Desktop;Microtower;d1s;d1ml;d1mr;Mon May 16 19:08:44 2022;;;;;p1ml;0;0

1 Type Chassis Serial Number Model Manufacturer Registered in Physical state Allocate state Lifecycle state Price Processor RAM (MB) Data Storage Size (MB) Rate Range Processor Rate Processor Range RAM Rate RAM Range Data Storage Rate Data Storage Range
2 Desktop Microtower d1s d1ml d1mr Mon May 16 09:34:22 2022 Mon May 16 19:08:44 2022 p1ml 0 0 1.0 Very low 1.0 Very low 1.0 Very low 1.0 Very low

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,273 @@
{
"components": [
{
"resolutionHeight": 600,
"model": "AUO LCD Monitor",
"manufacturer": "AUO \"AUO\"",
"size": 10.0,
"resolutionWidth": 1024,
"productionDate": "2009-01-04T00:00:00",
"refreshRate": 60,
"technology": "LCD",
"type": "Display",
"serialNumber": null,
"actions": []
},
{
"type": "NetworkAdapter",
"model": "AR9285 Wireless Network Adapter",
"serialNumber": "74:2f:68:8b:fd:c8",
"manufacturer": "Qualcomm Atheros",
"wireless": true,
"actions": []
},
{
"type": "NetworkAdapter",
"model": "AR8152 v2.0 Fast Ethernet",
"serialNumber": "14:da:e9:42:f6:7c",
"manufacturer": "Qualcomm Atheros",
"speed": 100,
"wireless": false,
"actions": []
},
{
"type": "Processor",
"cores": 1,
"threads": 1,
"address": 64,
"model": "Intel Atom CPU N455 @ 1.66GHz",
"serialNumber": null,
"manufacturer": "Intel Corp.",
"speed": 1.667,
"actions": [
{
"type": "BenchmarkProcessorSysbench",
"rate": 164.0803,
"elapsed": 164
},
{
"type": "BenchmarkProcessor",
"rate": 6666.24,
"elapsed": 0
}
]
},
{
"type": "GraphicCard",
"model": "Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller",
"serialNumber": null,
"memory": 256.0,
"manufacturer": "Intel Corporation",
"actions": []
},
{
"type": "SoundCard",
"model": "NM10/ICH7 Family High Definition Audio Controller",
"serialNumber": null,
"manufacturer": "Intel Corporation",
"actions": []
},
{
"type": "SoundCard",
"model": "USB 2.0 UVC VGA WebCam",
"serialNumber": "0x0001",
"manufacturer": "Azurewave",
"actions": []
},
{
"type": "RamModule",
"format": "DIMM",
"model": null,
"size": 1024,
"interface": "DDR3",
"serialNumber": null,
"manufacturer": null,
"speed": 667.0,
"actions": []
},
{
"size": 1024.0,
"actions": [],
"format": "SODIMM",
"model": "48594D503131325336344350362D53362020",
"interface": "DDR3",
"type": "RamModule",
"manufacturer": "Hynix Semiconductor",
"serialNumber": "4F43487B",
"speed": 667.0
},
{
"type": "HardDrive",
"model": "HTS54322",
"size": 238475,
"interface": "ATA",
"serialNumber": "E2024242CV86HJ",
"manufacturer": "Hitachi",
"actions": [
{
"type": "BenchmarkDataStorage",
"elapsed": 16,
"writeSpeed": 21.8,
"readSpeed": 66.2
},
{
"type": "TestDataStorage",
"length": "Extended",
"elapsed": 2,
"severity": "Error",
"status": "Unspecified Error. Self-test not started."
},
{
"type": "EraseBasic",
"steps": [
{
"type": "StepRandom",
"startTime": "2018-07-03T09:15:22.257059+00:00",
"severity": "Info",
"endTime": "2018-07-03T10:32:11.843190+00:00"
}
],
"startTime": "2018-07-03T09:15:22.256074+00:00",
"severity": "Info",
"endTime": "2018-07-03T10:32:11.848455+00:00"
}
]
},
{
"size": 160041.88569599998,
"variant": "1A01",
"actions": [
{
"type": "EraseBasic",
"steps": [
{
"type": "StepRandom",
"endTime": "2019-10-23T08:35:31.400587+00:00",
"severity": "Info",
"startTime": "2019-10-23T07:49:54.410830+00:00"
}
],
"endTime": "2019-10-23T08:35:31.400988+00:00",
"severity": "Error",
"startTime": "2019-10-23T07:49:54.410193+00:00"
},
{
"elapsed": 22,
"writeSpeed": 17.3,
"readSpeed": 41.6,
"type": "BenchmarkDataStorage"
},
{
"status": "Completed without error",
"reallocatedSectorCount": 0,
"currentPendingSectorCount": 0,
"assessment": true,
"severity": "Info",
"offlineUncorrectable": 0,
"lifetime": 4692,
"type": "TestDataStorage",
"length": "Short",
"elapsed": 118,
"reportedUncorrectableErrors": 1513,
"powerCycleCount": 5293
}
],
"model": "WDC WD1600BEVT-2",
"interface": "ATA",
"type": "DataStorage",
"manufacturer": "Western Digital",
"serialNumber": "WD-WX11A80W7430"
},
{
"actions": [
{
"writeSpeed": 17.1,
"type": "BenchmarkDataStorage",
"elapsed": 22,
"readSpeed": 41.1
},
{
"type": "EraseSectors",
"startTime": "2019-08-19T16:48:19.689794+00:00",
"steps": [
{
"startTime": "2019-08-19T16:48:19.690458+00:00",
"type": "StepRandom",
"severity": "Info",
"endTime": "2019-08-19T17:34:22.930562+00:00"
},
{
"startTime": "2019-08-19T17:34:22.690458+00:00",
"type": "StepZero",
"severity": "Info",
"endTime": "2019-08-19T18:34:22.930562+00:00"
}
],
"severity": "Info",
"endTime": "2019-08-19T18:34:22.930959+00:00"
},
{
"currentPendingSectorCount": 0,
"lifetime": 4673,
"elapsed": 115,
"reallocatedSectorCount": 0,
"powerCycleCount": 5231,
"status": "Completed without error",
"assessment": true,
"type": "TestDataStorage",
"severity": "Info",
"length": "Short",
"offlineUncorrectable": 0
}
],
"model": "WDC WD1600BEVT-2",
"manufacturer": "Western Digital",
"size": 160042.0,
"interface": "ATA",
"serialNumber": "WD-WX11A80W7430",
"type": "SolidStateDrive",
"variant": "1A01"
},
{
"type": "Motherboard",
"serial": 1,
"firewire": 0,
"model": "1001PXD",
"slots": 2,
"pcmcia": 0,
"serialNumber": "Eee0123456789",
"usb": 5,
"manufacturer": "ASUSTeK Computer INC.",
"actions": [
{
"type": "TestBios",
"accessRange": "C"
}
]
}
],
"elapsed": 4875,
"uuid": "3fd12a01-c04e-4fd8-9e64-2660c459e725",
"version": "11.0b11",
"type": "Snapshot",
"software": "Workbench",
"device": {
"type": "Laptop",
"model": "1001PXD",
"serialNumber": "B8OAAS048287",
"manufacturer": "ASUSTeK Computer INC.",
"chassis": "Netbook",
"actions": [
{
"type": "BenchmarkRamSysbench",
"rate": 15.7188,
"elapsed": 16
},
{
"type": "StressTest",
"severity": "Info",
"elapsed": 60
}
]
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,199 @@
{
"closed": true,
"components": [
{
"interface": "ATA",
"size": "160042.0 MB",
"serialNumber": "WD-WX11A80W7430",
"type": "HardDrive",
"variant": "1A01",
"model": "WDC WD1600BEVT-2",
"manufacturer": "Western Digital",
"actions": [
{
"severity": "Info",
"steps": [
{
"severity": "Info",
"endTime": "2019-09-10T11:37:07.459534+00:00",
"startTime": "2019-09-10T10:51:20.208391+00:00",
"type": "StepRandom"
}
],
"startTime": "2019-09-10T10:51:20.207733+00:00",
"endTime": "2019-09-10T11:37:07.459940+00:00",
"type": "EraseBasic"
},
{
"type": "BenchmarkDataStorage",
"writeSpeed": "17.1 MB / second",
"readSpeed": "67.8 MB / second",
"elapsed": 20
},
{
"offlineUncorrectable": 0,
"lifetime": 4675,
"assessment": true,
"reallocatedSectorCount": 0,
"elapsed": 118,
"currentPendingSectorCount": 0,
"type": "TestDataStorage",
"status": "Completed without error",
"severity": "Info",
"powerCycleCount": 5238,
"length": "Short"
}
]
},
{
"interface": "DDR2",
"serialNumber": "4F43487B",
"speed": "667.0 megahertz",
"type": "RamModule",
"size": "1024.0 mebibyte",
"model": "48594D503131325336344350362D53362020",
"format": "SODIMM",
"manufacturer": "Hynix Semiconductor",
"actions": []
},
{
"address": 64,
"serialNumber": null,
"actions": [
{
"type": "BenchmarkProcessor",
"rate": 6650.38,
"elapsed": 0
},
{
"type": "BenchmarkProcessorSysbench",
"rate": 164.4318,
"elapsed": 164
}
],
"speed": "1.0 gigahertz",
"type": "Processor",
"brand": "Atom",
"generation": null,
"cores": 1,
"manufacturer": "Intel Corp.",
"model": "Intel Atom CPU N450 @ 1.66GHz",
"threads": 2
},
{
"technology": "LCD",
"refreshRate": "60 hertz",
"serialNumber": null,
"size": "10.0 inch",
"resolutionWidth": 1024,
"type": "Display",
"productionDate": "2009-01-04T00:00:00",
"model": "AUO LCD Monitor",
"manufacturer": "AUO \"AUO\"",
"actions": [],
"resolutionHeight": 600
},
{
"wireless": false,
"serialNumber": "88:ae:1d:a6:f3:d0",
"speed": "100.0 megabit / second",
"type": "NetworkAdapter",
"variant": "c1",
"model": "AR8152 v1.1 Fast Ethernet",
"manufacturer": "Qualcomm Atheros",
"actions": []
},
{
"wireless": true,
"serialNumber": "00:26:c7:8e:cb:8c",
"speed": null,
"type": "NetworkAdapter",
"variant": "00",
"model": "Centrino Wireless-N 1000 Condor Peak",
"manufacturer": "Intel Corporation",
"actions": []
},
{
"technology": "LiIon",
"serialNumber": null,
"type": "Battery",
"size": "2200 hour * milliampere",
"model": "AL10A31",
"manufacturer": "SANYO",
"actions": [
{
"severity": "Info",
"voltage": "12613.0 millivolt",
"size": "662.0 hour * milliampere",
"cycleCount": null,
"type": "MeasureBattery"
}
]
},
{
"memory": null,
"serialNumber": null,
"type": "GraphicCard",
"model": "Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller",
"manufacturer": "Intel Corporation",
"actions": []
},
{
"type": "SoundCard",
"model": "NM10/ICH7 Family High Definition Audio Controller",
"manufacturer": "Intel Corporation",
"serialNumber": null,
"actions": []
},
{
"type": "SoundCard",
"model": "1.3M WebCam",
"manufacturer": "XPA970VW0",
"serialNumber": null,
"actions": []
},
{
"manufacturer": "Acer",
"usb": 5,
"type": "Motherboard",
"model": "AOHAPPY",
"ramMaxSize": 4,
"ramSlots": 2,
"serialNumber": "Base Board Serial Number",
"pcmcia": 0,
"slots": 1,
"firewire": 0,
"serial": 1,
"version": "V3.05(DDR2)",
"biosDate": "2010-08-12T00:00:00",
"actions": []
}
],
"uuid": "9c169711-3d72-4e6c-aabf-de9b3af56b54",
"device": {
"serialNumber": "LUSEA0D010038879A01601",
"type": "Laptop",
"sku": null,
"model": "AOHAPPY",
"chassis": "Netbook",
"manufacturer": "Acer",
"actions": [
{
"type": "BenchmarkRamSysbench",
"rate": 19.2586,
"elapsed": 19
},
{
"severity": "Info",
"elapsed": 60,
"type": "StressTest"
}
],
"version": "V3.05"
},
"type": "Snapshot",
"endTime": "2019-09-10T10:44:41.324414+00:00",
"elapsed": 3146,
"software": "Workbench",
"version": "11.0b9"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"device": {"dataStorageSize": 99, "serialNumber": "02:00:00:00:00:00", "model": "Motorola One Vision", "type": "Mobile", "ramSize": 31138, "displaySize": 9, "manufacturer": "Motorola"}, "software": "WorkbenchAndroid", "type": "Snapshot", "uuid": "958d697f-af34-4410-85d6-adb906d46161", "version": "0.0.2"}

View File

@ -32,6 +32,7 @@ def test_api_docs(client: Client):
'/actions/', '/actions/',
'/allocates/', '/allocates/',
'/apidocs', '/apidocs',
'/api/inventory/',
'/deallocates/', '/deallocates/',
'/deliverynotes/', '/deliverynotes/',
'/devices/', '/devices/',
@ -60,8 +61,14 @@ def test_api_docs(client: Client):
'/inventory/lot/{id}/del/', '/inventory/lot/{id}/del/',
'/inventory/lot/{lot_id}/device/', '/inventory/lot/{lot_id}/device/',
'/inventory/lot/{lot_id}/device/add/', '/inventory/lot/{lot_id}/device/add/',
'/inventory/lot/{lot_id}/deliverynote/',
'/inventory/lot/{lot_id}/receivernote/',
'/inventory/lot/{lot_id}/trade-document/add/', '/inventory/lot/{lot_id}/trade-document/add/',
'/inventory/lot/{lot_id}/transfer/{type_id}/',
'/inventory/lot/{lot_id}/transfer/',
'/inventory/lot/{lot_id}/upload-snapshot/', '/inventory/lot/{lot_id}/upload-snapshot/',
'/inventory/snapshots/{snapshot_uuid}/',
'/inventory/snapshots/',
'/inventory/tag/devices/add/', '/inventory/tag/devices/add/',
'/inventory/tag/devices/{id}/del/', '/inventory/tag/devices/{id}/del/',
'/inventory/upload-snapshot/', '/inventory/upload-snapshot/',

View File

@ -130,6 +130,7 @@ def test_physical_properties():
'model': 'foo', 'model': 'foo',
'receiver_id': None, 'receiver_id': None,
'serial_number': 'foo-bar', 'serial_number': 'foo-bar',
'uuid': None,
'transfer_state': TransferState.Initial 'transfer_state': TransferState.Initial
} }
@ -480,7 +481,7 @@ def test_get_device_permissions(app: Devicehub, user: UserClient, user2: UserCli
s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot) s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot)
pc, res = user.get(res=d.Device, item=s['device']['devicehubID']) pc, res = user.get(res=d.Device, item=s['device']['devicehubID'])
assert res.status_code == 200 assert res.status_code == 200
assert len(pc['actions']) == 9 assert len(pc['actions']) == 7
html, _ = client.get(res=d.Device, item=s['device']['devicehubID'], accept=ANY) html, _ = client.get(res=d.Device, item=s['device']['devicehubID'], accept=ANY)
assert 'intel atom cpu n270 @ 1.60ghz' in html assert 'intel atom cpu n270 @ 1.60ghz' in html

View File

@ -181,7 +181,7 @@ def test_device_query(user: UserClient):
assert i['url'] == '/devices/' assert i['url'] == '/devices/'
assert i['items'][0]['url'] == '/devices/%s' % snapshot['device']['devicehubID'] assert i['items'][0]['url'] == '/devices/%s' % snapshot['device']['devicehubID']
pc = next(d for d in i['items'] if d['type'] == 'Desktop') pc = next(d for d in i['items'] if d['type'] == 'Desktop')
assert len(pc['actions']) == 4 assert len(pc['actions']) == 3
assert len(pc['components']) == 3 assert len(pc['components']) == 3
assert pc['tags'][0]['id'] == pc['devicehubID'] assert pc['tags'][0]['id'] == pc['devicehubID']

View File

@ -410,6 +410,7 @@ def test_export_computer_monitor(user: UserClient):
f = StringIO(csv_str) f = StringIO(csv_str)
obj_csv = csv.reader(f, f) obj_csv = csv.reader(f, f)
export_csv = list(obj_csv) export_csv = list(obj_csv)
# Open fixture csv and transform to list # Open fixture csv and transform to list
with Path(__file__).parent.joinpath('files').joinpath( with Path(__file__).parent.joinpath('files').joinpath(
'computer-monitor.csv' 'computer-monitor.csv'
@ -501,6 +502,7 @@ def test_report_devices_stock_control(user: UserClient, user2: UserClient):
accept='text/csv', accept='text/csv',
query=[('filter', {'type': ['Computer']})], query=[('filter', {'type': ['Computer']})],
) )
f = StringIO(csv_str) f = StringIO(csv_str)
obj_csv = csv.reader(f, f) obj_csv = csv.reader(f, f)
export_csv = list(obj_csv) export_csv = list(obj_csv)

View File

@ -174,7 +174,7 @@ def test_upload_snapshot(user3: UserClientFlask):
assert str(db_snapthot.uuid) == snapshot['uuid'] assert str(db_snapthot.uuid) == snapshot['uuid']
assert dev.type == 'Laptop' assert dev.type == 'Laptop'
assert dev.serial_number == 'b8oaas048285' assert dev.serial_number == 'b8oaas048285'
assert len(dev.actions) == 12 assert len(dev.actions) == 10
assert len(dev.components) == 9 assert len(dev.components) == 9
@ -495,7 +495,7 @@ def test_action_recycling(user3: UserClientFlask):
uri = '/inventory/action/add/' uri = '/inventory/action/add/'
body, status = user3.post(uri, data=data) body, status = user3.post(uri, data=data)
assert dev.actions[-1].type == 'EreusePrice' assert dev.actions[-1].type == 'Snapshot'
assert 'Action Allocate error!' in body assert 'Action Allocate error!' in body
# good request # good request
@ -815,7 +815,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
user3.post(uri, data=data) user3.post(uri, data=data)
assert dev.allocated_status.type == 'Allocate' assert dev.allocated_status.type == 'Allocate'
assert len(dev.actions) == 13 assert len(dev.actions) == 11
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -829,7 +829,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
body, status = user3.post(uri, data=data) body, status = user3.post(uri, data=data)
assert status == '200 OK' assert status == '200 OK'
assert dev.allocated_status.type == 'Deallocate' assert dev.allocated_status.type == 'Deallocate'
assert len(dev.actions) == 14 assert len(dev.actions) == 12
# is not possible to do an allocate between an allocate and an deallocate # is not possible to do an allocate between an allocate and an deallocate
data = { data = {
@ -858,7 +858,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
} }
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 14 assert len(dev.actions) == 12
@pytest.mark.mvp @pytest.mark.mvp
@ -881,7 +881,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
uri = '/inventory/action/allocate/add/' uri = '/inventory/action/allocate/add/'
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 13 assert len(dev.actions) == 11
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -893,7 +893,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
} }
body, status = user3.post(uri, data=data) body, status = user3.post(uri, data=data)
assert status == '200 OK' assert status == '200 OK'
assert len(dev.actions) == 14 assert len(dev.actions) == 12
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -907,7 +907,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
uri = '/inventory/action/allocate/add/' uri = '/inventory/action/allocate/add/'
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 15 assert len(dev.actions) == 13
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -918,7 +918,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
'end_users': 2, 'end_users': 2,
} }
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 16 assert len(dev.actions) == 14
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -929,7 +929,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
'end_users': 2, 'end_users': 2,
} }
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 17 assert len(dev.actions) == 15
data = { data = {
'csrf_token': generate_csrf(), 'csrf_token': generate_csrf(),
@ -940,7 +940,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
'end_users': 2, 'end_users': 2,
} }
user3.post(uri, data=data) user3.post(uri, data=data)
assert len(dev.actions) == 18 assert len(dev.actions) == 16
@pytest.mark.mvp @pytest.mark.mvp
@ -1084,3 +1084,228 @@ def test_wb_settings_register(user3: UserClientFlask):
assert "TOKEN = " in body assert "TOKEN = " in body
assert "URL = https://" in body assert "URL = https://" in body
assert "/api/inventory/" in body assert "/api/inventory/" in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_create_transfer(user3: UserClientFlask):
user3.get('/inventory/lot/add/')
lot_name = 'lot1'
data = {
'name': lot_name,
'csrf_token': generate_csrf(),
}
user3.post('/inventory/lot/add/', data=data)
lot = Lot.query.filter_by(name=lot_name).one()
lot_id = lot.id
uri = f'/inventory/lot/{lot_id}/transfer/incoming/'
body, status = user3.get(uri)
assert status == '200 OK'
assert 'Add new transfer' in body
assert 'Code' in body
assert 'Description' in body
assert 'Save' in body
data = {'csrf_token': generate_csrf(), 'code': 'AAA'}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Transfer created successfully!' in body
assert 'Delete Lot' in body
assert 'Incoming Lot' in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_transfer(user3: UserClientFlask):
# create lot
user3.get('/inventory/lot/add/')
lot_name = 'lot1'
data = {
'name': lot_name,
'csrf_token': generate_csrf(),
}
user3.post('/inventory/lot/add/', data=data)
lot = Lot.query.filter_by(name=lot_name).one()
# render temporary lot
lot_id = lot.id
uri = f'/inventory/lot/{lot_id}/device/'
body, status = user3.get(uri)
assert status == '200 OK'
assert 'Transfer (<span class="text-success">Open</span>)' not in body
assert '<i class="bi bi-trash"></i> Delete Lot' in body
# create new incoming lot
uri = f'/inventory/lot/{lot_id}/transfer/incoming/'
data = {'csrf_token': generate_csrf(), 'code': 'AAA'}
body, status = user3.post(uri, data=data)
assert 'Transfer (<span class="text-success">Open</span>)' in body
assert '<i class="bi bi-trash"></i> Delete Lot' in body
lot = Lot.query.filter()[1]
assert lot.transfer is not None
# edit transfer with errors
lot_id = lot.id
uri = f'/inventory/lot/{lot_id}/transfer/'
data = {
'csrf_token': generate_csrf(),
'code': 'AAA',
'description': 'one one one',
'date': datetime.datetime.now().date() + datetime.timedelta(15),
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Transfer updated error!' in body
assert 'one one one' not in body
assert '<i class="bi bi-trash"></i> Delete Lot' in body
assert 'Transfer (<span class="text-success">Open</span>)' in body
# # edit transfer successfully
data = {
'csrf_token': generate_csrf(),
'code': 'AAA',
'description': 'one one one',
'date': datetime.datetime.now().date() - datetime.timedelta(15),
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Transfer updated successfully!' in body
assert 'one one one' in body
assert '<i class="bi bi-trash"></i> Delete Lot' not in body
assert 'Transfer (<span class="text-danger">Closed</span>)' in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_deliverynote(user3: UserClientFlask):
# create lot
user3.get('/inventory/lot/add/')
lot_name = 'lot1'
data = {
'name': lot_name,
'csrf_token': generate_csrf(),
}
user3.post('/inventory/lot/add/', data=data)
lot = Lot.query.filter_by(name=lot_name).one()
lot_id = lot.id
# create new incoming lot
uri = f'/inventory/lot/{lot_id}/transfer/incoming/'
data = {'csrf_token': generate_csrf(), 'code': 'AAA'}
user3.post(uri, data=data)
lot = Lot.query.filter()[1]
lot_id = lot.id
# edit delivery with errors
uri = f'/inventory/lot/{lot_id}/deliverynote/'
data = {
'csrf_token': generate_csrf(),
'number': 'AAA',
'units': 10,
'weight': 50,
'date': datetime.datetime.now().date() + datetime.timedelta(15),
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Delivery Note updated error!' in body
# # edit transfer successfully
data['date'] = datetime.datetime.now().date() - datetime.timedelta(15)
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Delivery Note updated successfully!' in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_receivernote(user3: UserClientFlask):
# create lot
user3.get('/inventory/lot/add/')
lot_name = 'lot1'
data = {
'name': lot_name,
'csrf_token': generate_csrf(),
}
user3.post('/inventory/lot/add/', data=data)
lot = Lot.query.filter_by(name=lot_name).one()
lot_id = lot.id
# create new incoming lot
uri = f'/inventory/lot/{lot_id}/transfer/incoming/'
data = {'csrf_token': generate_csrf(), 'code': 'AAA'}
user3.post(uri, data=data)
lot = Lot.query.filter()[1]
lot_id = lot.id
# edit delivery with errors
uri = f'/inventory/lot/{lot_id}/receivernote/'
data = {
'csrf_token': generate_csrf(),
'number': 'AAA',
'units': 10,
'weight': 50,
'date': datetime.datetime.now().date() + datetime.timedelta(15),
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Receiver Note updated error!' in body
# # edit transfer successfully
data['date'] = datetime.datetime.now().date() - datetime.timedelta(15)
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Receiver Note updated successfully!' in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_notes_with_closed_transfer(user3: UserClientFlask):
# create lot
user3.get('/inventory/lot/add/')
lot_name = 'lot1'
data = {
'name': lot_name,
'csrf_token': generate_csrf(),
}
user3.post('/inventory/lot/add/', data=data)
lot = Lot.query.filter_by(name=lot_name).one()
lot_id = lot.id
# create new incoming lot
uri = f'/inventory/lot/{lot_id}/transfer/incoming/'
data = {'csrf_token': generate_csrf(), 'code': 'AAA'}
user3.post(uri, data=data)
lot = Lot.query.filter()[1]
lot_id = lot.id
# edit transfer adding date
uri = f'/inventory/lot/{lot_id}/transfer/'
data['date'] = datetime.datetime.now().date() - datetime.timedelta(15)
user3.post(uri, data=data)
assert lot.transfer.closed is True
# edit delivery with errors
uri = f'/inventory/lot/{lot_id}/deliverynote/'
data = {
'csrf_token': generate_csrf(),
'number': 'AAA',
'units': 10,
'weight': 50,
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Delivery Note updated error!' in body
# edit receiver with errors
uri = f'/inventory/lot/{lot_id}/receivernote/'
data = {
'csrf_token': generate_csrf(),
'number': 'AAA',
'units': 10,
'weight': 50,
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Receiver Note updated error!' in body

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,6 @@ def test_workbench_server_condensed(user: UserClient):
('BenchmarkProcessorSysbench', cpu_id), ('BenchmarkProcessorSysbench', cpu_id),
('StressTest', pc_id), ('StressTest', pc_id),
('EraseSectors', ssd_id), ('EraseSectors', ssd_id),
('EreusePrice', pc_id),
('BenchmarkRamSysbench', pc_id), ('BenchmarkRamSysbench', pc_id),
('BenchmarkProcessor', cpu_id), ('BenchmarkProcessor', cpu_id),
('Install', ssd_id), ('Install', ssd_id),
@ -49,7 +48,6 @@ def test_workbench_server_condensed(user: UserClient):
('BenchmarkDataStorage', ssd_id), ('BenchmarkDataStorage', ssd_id),
('BenchmarkDataStorage', hdd_id), ('BenchmarkDataStorage', hdd_id),
('TestDataStorage', ssd_id), ('TestDataStorage', ssd_id),
('RateComputer', pc_id)
} }
assert snapshot['closed'] assert snapshot['closed']
assert snapshot['severity'] == 'Info' assert snapshot['severity'] == 'Info'
@ -61,10 +59,6 @@ def test_workbench_server_condensed(user: UserClient):
assert device['networkSpeeds'] == [1000, 58] assert device['networkSpeeds'] == [1000, 58]
assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml' assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml'
assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes' assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes'
assert device['rate']['closed']
assert device['rate']['severity'] == 'Info'
assert device['rate']['rating'] == 1
assert device['rate']['type'] == RateComputer.t
# TODO JN why haven't same order in actions on each execution? # TODO JN why haven't same order in actions on each execution?
assert any([ac['type'] in [BenchmarkProcessor.t, BenchmarkRamSysbench.t] for ac in device['actions']]) assert any([ac['type'] in [BenchmarkProcessor.t, BenchmarkRamSysbench.t] for ac in device['actions']])
assert 'tag1' in [x['id'] for x in device['tags']] assert 'tag1' in [x['id'] for x in device['tags']]
@ -145,8 +139,6 @@ def test_real_hp_11(user: UserClient):
assert pc['hid'] == 'desktop-hewlett-packard-hp_compaq_8100_elite_sff-czc0408yjg-6c:62:6d:81:22:9f' assert pc['hid'] == 'desktop-hewlett-packard-hp_compaq_8100_elite_sff-czc0408yjg-6c:62:6d:81:22:9f'
assert pc['chassis'] == 'Tower' assert pc['chassis'] == 'Tower'
assert set(e['type'] for e in snapshot['actions']) == { assert set(e['type'] for e in snapshot['actions']) == {
'EreusePrice',
'RateComputer',
'BenchmarkDataStorage', 'BenchmarkDataStorage',
'BenchmarkProcessor', 'BenchmarkProcessor',
'BenchmarkProcessorSysbench', 'BenchmarkProcessorSysbench',
@ -156,7 +148,8 @@ def test_real_hp_11(user: UserClient):
'TestBios', 'TestBios',
'VisualTest' 'VisualTest'
} }
assert len(list(e['type'] for e in snapshot['actions'])) == 10
assert len(list(e['type'] for e in snapshot['actions'])) == 8
assert pc['networkSpeeds'] == [1000, None], 'Device has no WiFi' assert pc['networkSpeeds'] == [1000, None], 'Device has no WiFi'
assert pc['processorModel'] == 'intel core i3 cpu 530 @ 2.93ghz' assert pc['processorModel'] == 'intel core i3 cpu 530 @ 2.93ghz'
assert pc['ramSize'] == 8192 assert pc['ramSize'] == 8192
@ -175,6 +168,7 @@ def test_snapshot_real_eee_1001pxd_with_rate(user: UserClient):
"""Checks the values of the device, components, """Checks the values of the device, components,
actions and their relationships of a real pc. actions and their relationships of a real pc.
""" """
# import pdb; pdb.set_trace()
s = file('real-eee-1001pxd.snapshot.11') s = file('real-eee-1001pxd.snapshot.11')
snapshot, _ = user.post(res=em.Snapshot, data=s) snapshot, _ = user.post(res=em.Snapshot, data=s)
pc, _ = user.get(res=Device, item=snapshot['device']['devicehubID']) pc, _ = user.get(res=Device, item=snapshot['device']['devicehubID'])
@ -186,19 +180,10 @@ def test_snapshot_real_eee_1001pxd_with_rate(user: UserClient):
assert pc['hid'] == 'laptop-asustek_computer_inc-1001pxd-b8oaas048286-14:da:e9:42:f6:7c' assert pc['hid'] == 'laptop-asustek_computer_inc-1001pxd-b8oaas048286-14:da:e9:42:f6:7c'
assert len(pc['tags']) == 1 assert len(pc['tags']) == 1
assert pc['networkSpeeds'] == [100, 0], 'Although it has WiFi we do not know the speed' assert pc['networkSpeeds'] == [100, 0], 'Although it has WiFi we do not know the speed'
assert pc['rate']
rate = pc['rate']
# assert pc['actions'][0]['appearanceRange'] == 'A' # assert pc['actions'][0]['appearanceRange'] == 'A'
# assert pc['actions'][0]['functionalityRange'] == 'B' # assert pc['actions'][0]['functionalityRange'] == 'B'
# TODO add appearance and functionality Range in device[rate] # TODO add appearance and functionality Range in device[rate]
assert rate['processorRange'] == 'LOW'
assert rate['ramRange'] == 'LOW'
assert rate['ratingRange'] == 'LOW'
assert rate['ram'] == 1.53
# TODO add camelCase instead of snake_case
assert rate['dataStorage'] == 3.76
assert rate['type'] == 'RateComputer'
components = snapshot['components'] components = snapshot['components']
wifi = components[0] wifi = components[0]
assert wifi['hid'] == 'networkadapter-qualcomm_atheros-' \ assert wifi['hid'] == 'networkadapter-qualcomm_atheros-' \
@ -232,7 +217,7 @@ def test_snapshot_real_eee_1001pxd_with_rate(user: UserClient):
assert em.BenchmarkRamSysbench.t in action_types assert em.BenchmarkRamSysbench.t in action_types
assert em.StressTest.t in action_types assert em.StressTest.t in action_types
assert em.Snapshot.t in action_types assert em.Snapshot.t in action_types
assert len(actions) == 8 assert len(actions) == 6
gpu = components[3] gpu = components[3]
assert gpu['model'] == 'atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller' assert gpu['model'] == 'atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller'
assert gpu['manufacturer'] == 'intel corporation' assert gpu['manufacturer'] == 'intel corporation'
@ -242,7 +227,7 @@ def test_snapshot_real_eee_1001pxd_with_rate(user: UserClient):
assert em.BenchmarkRamSysbench.t in action_types assert em.BenchmarkRamSysbench.t in action_types
assert em.StressTest.t in action_types assert em.StressTest.t in action_types
assert em.Snapshot.t in action_types assert em.Snapshot.t in action_types
assert len(action_types) == 6 assert len(action_types) == 4
sound = components[4] sound = components[4]
assert sound['model'] == 'nm10/ich7 family high definition audio controller' assert sound['model'] == 'nm10/ich7 family high definition audio controller'
sound = components[5] sound = components[5]
@ -264,7 +249,7 @@ def test_snapshot_real_eee_1001pxd_with_rate(user: UserClient):
assert em.TestDataStorage.t in action_types assert em.TestDataStorage.t in action_types
assert em.EraseBasic.t in action_types assert em.EraseBasic.t in action_types
assert em.Snapshot.t in action_types assert em.Snapshot.t in action_types
assert len(action_types) == 9 assert len(action_types) == 7
erase = next(e for e in hdd['actions'] if e['type'] == em.EraseBasic.t) erase = next(e for e in hdd['actions'] if e['type'] == em.EraseBasic.t)
assert erase['endTime'] assert erase['endTime']
assert erase['startTime'] assert erase['startTime']