Merge pull request #212 from eReuse/feature/server-side-render-parser-3021
Feature/server side render parser 3021
This commit is contained in:
commit
083e763b25
|
@ -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'))
|
|
@ -1,39 +1,49 @@
|
|||
from distutils.version import StrictVersion
|
||||
from itertools import chain
|
||||
from typing import Set
|
||||
from decouple import config
|
||||
|
||||
from decouple import config
|
||||
from teal.auth import TokenAuth
|
||||
from teal.config import Config
|
||||
from teal.enums import Currency
|
||||
from teal.utils import import_resource
|
||||
|
||||
from ereuse_devicehub.resources import action, agent, deliverynote, inventory, \
|
||||
lot, tag, user
|
||||
from ereuse_devicehub.resources import (
|
||||
action,
|
||||
agent,
|
||||
deliverynote,
|
||||
inventory,
|
||||
lot,
|
||||
tag,
|
||||
user,
|
||||
)
|
||||
from ereuse_devicehub.resources.device import definitions
|
||||
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.versions import versions
|
||||
from ereuse_devicehub.resources.licences import licences
|
||||
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):
|
||||
RESOURCE_DEFINITIONS = set(chain(import_resource(definitions),
|
||||
import_resource(action),
|
||||
import_resource(user),
|
||||
import_resource(tag),
|
||||
import_resource(agent),
|
||||
import_resource(lot),
|
||||
import_resource(deliverynote),
|
||||
import_resource(documents),
|
||||
import_resource(tradedocument),
|
||||
import_resource(inventory),
|
||||
import_resource(versions),
|
||||
import_resource(licences),
|
||||
import_resource(metric_def),
|
||||
),)
|
||||
RESOURCE_DEFINITIONS = set(
|
||||
chain(
|
||||
import_resource(definitions),
|
||||
import_resource(action),
|
||||
import_resource(user),
|
||||
import_resource(tag),
|
||||
import_resource(agent),
|
||||
import_resource(lot),
|
||||
import_resource(deliverynote),
|
||||
import_resource(documents),
|
||||
import_resource(tradedocument),
|
||||
import_resource(inventory),
|
||||
import_resource(versions),
|
||||
import_resource(licences),
|
||||
import_resource(metric_def),
|
||||
),
|
||||
)
|
||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
DB_USER = config('DB_USER', 'dhub')
|
||||
|
@ -48,11 +58,12 @@ class DevicehubConfig(Config):
|
|||
db=DB_DATABASE,
|
||||
) # type: str
|
||||
SCHEMA = config('SCHEMA', 'dbtest')
|
||||
HOST = config('HOST', 'localhost')
|
||||
HOST = config('HOST', 'localhost')
|
||||
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
||||
"""The minimum version of ereuse.org workbench that this devicehub
|
||||
accepts. we recommend not changing this value.
|
||||
"""
|
||||
SCHEMA_WORKBENCH = ["1.0.0"]
|
||||
|
||||
TMP_SNAPSHOTS = config('TMP_SNAPSHOTS', '/tmp/snapshots')
|
||||
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"""
|
||||
API_DOC_CONFIG_TITLE = 'Devicehub'
|
||||
API_DOC_CONFIG_VERSION = '0.2'
|
||||
API_DOC_CONFIG_COMPONENTS = {
|
||||
'securitySchemes': {
|
||||
'bearerAuth': TokenAuth.API_DOCS
|
||||
}
|
||||
}
|
||||
API_DOC_CONFIG_COMPONENTS = {'securitySchemes': {'bearerAuth': TokenAuth.API_DOCS}}
|
||||
API_DOC_CLASS_DISCRIMINATOR = 'type'
|
||||
|
||||
PRICE_SOFTWARE = PriceSoftware.Ereuse
|
||||
|
|
|
@ -4,8 +4,10 @@ import json
|
|||
from json.decoder import JSONDecodeError
|
||||
|
||||
from boltons.urlutils import URL
|
||||
from flask import current_app as app
|
||||
from flask import g, request
|
||||
from flask_wtf import FlaskForm
|
||||
from marshmallow import ValidationError
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from wtforms import (
|
||||
|
@ -26,14 +28,20 @@ from wtforms import (
|
|||
from wtforms.fields import FormField
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.action.models import RateComputer, Snapshot, Trade
|
||||
from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate
|
||||
from ereuse_devicehub.inventory.models import DeliveryNote, ReceiverNote, Transfer
|
||||
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.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 (
|
||||
SAI,
|
||||
Cellphone,
|
||||
Computer,
|
||||
ComputerMonitor,
|
||||
Device,
|
||||
Keyboard,
|
||||
|
@ -44,12 +52,11 @@ from ereuse_devicehub.resources.device.models import (
|
|||
)
|
||||
from ereuse_devicehub.resources.device.sync import Sync
|
||||
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.lot.models import Lot
|
||||
from ereuse_devicehub.resources.tag.model import Tag
|
||||
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
|
||||
from ereuse_devicehub.resources.user.exceptions import InsufficientPermission
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
DEVICES = {
|
||||
|
@ -76,7 +83,7 @@ DEVICES = {
|
|||
],
|
||||
}
|
||||
|
||||
COMPUTERS = ['Desktop', 'Laptop', 'Server']
|
||||
COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
|
||||
|
||||
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
|
||||
MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"]
|
||||
|
@ -197,7 +204,7 @@ class LotForm(FlaskForm):
|
|||
return self.instance
|
||||
|
||||
|
||||
class UploadSnapshotForm(FlaskForm):
|
||||
class UploadSnapshotForm(SnapshotMixin, FlaskForm):
|
||||
snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()])
|
||||
|
||||
def validate(self, extra_validators=None):
|
||||
|
@ -237,20 +244,58 @@ class UploadSnapshotForm(FlaskForm):
|
|||
|
||||
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):
|
||||
if any([x == 'Error' for x in self.result.values()]):
|
||||
return
|
||||
# result = []
|
||||
self.sync = Sync()
|
||||
schema = SnapshotSchema()
|
||||
# self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
||||
# TODO @cayop get correct var config
|
||||
self.tmp_snapshots = '/tmp/'
|
||||
schema_lite = Snapshot_lite()
|
||||
devices = []
|
||||
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
||||
for filename, snapshot_json in self.snapshots:
|
||||
path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
|
||||
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)
|
||||
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'):
|
||||
self.result[filename] = 'Ok'
|
||||
|
@ -261,69 +306,7 @@ class UploadSnapshotForm(FlaskForm):
|
|||
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return response
|
||||
|
||||
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
|
||||
return self.result, devices
|
||||
|
||||
|
||||
class NewDeviceForm(FlaskForm):
|
||||
|
@ -559,7 +542,7 @@ class TagDeviceForm(FlaskForm):
|
|||
db.session.commit()
|
||||
|
||||
|
||||
class ActionFormMix(FlaskForm):
|
||||
class ActionFormMixin(FlaskForm):
|
||||
name = StringField(
|
||||
'Name',
|
||||
[validators.length(max=50)],
|
||||
|
@ -640,7 +623,7 @@ class ActionFormMix(FlaskForm):
|
|||
return self.type.data
|
||||
|
||||
|
||||
class NewActionForm(ActionFormMix):
|
||||
class NewActionForm(ActionFormMixin):
|
||||
def validate(self, extra_validators=None):
|
||||
is_valid = super().validate(extra_validators)
|
||||
|
||||
|
@ -653,7 +636,7 @@ class NewActionForm(ActionFormMix):
|
|||
return True
|
||||
|
||||
|
||||
class AllocateForm(ActionFormMix):
|
||||
class AllocateForm(ActionFormMixin):
|
||||
date = HiddenField('')
|
||||
start_time = DateField('Start time')
|
||||
end_time = DateField('End time', [validators.Optional()])
|
||||
|
@ -854,7 +837,7 @@ class DataWipeDocumentForm(Form):
|
|||
return self._obj
|
||||
|
||||
|
||||
class DataWipeForm(ActionFormMix):
|
||||
class DataWipeForm(ActionFormMixin):
|
||||
document = FormField(DataWipeDocumentForm)
|
||||
|
||||
def save(self):
|
||||
|
@ -881,7 +864,7 @@ class DataWipeForm(ActionFormMix):
|
|||
return self.instance
|
||||
|
||||
|
||||
class TradeForm(ActionFormMix):
|
||||
class TradeForm(ActionFormMixin):
|
||||
user_from = StringField(
|
||||
'Supplier',
|
||||
[validators.Optional()],
|
||||
|
@ -1118,3 +1101,208 @@ class TradeDocumentForm(FlaskForm):
|
|||
db.session.commit()
|
||||
|
||||
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
|
||||
|
|
|
@ -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',
|
||||
)
|
|
@ -16,30 +16,34 @@ from ereuse_devicehub.inventory.forms import (
|
|||
AdvancedSearchForm,
|
||||
AllocateForm,
|
||||
DataWipeForm,
|
||||
EditTransferForm,
|
||||
FilterForm,
|
||||
LotForm,
|
||||
NewActionForm,
|
||||
NewDeviceForm,
|
||||
NotesForm,
|
||||
TagDeviceForm,
|
||||
TradeDocumentForm,
|
||||
TradeForm,
|
||||
TransferForm,
|
||||
UploadSnapshotForm,
|
||||
)
|
||||
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.device.models import Computer, DataStorage, Device
|
||||
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
|
||||
from ereuse_devicehub.resources.hash_reports import insert_hash
|
||||
from ereuse_devicehub.resources.lot.models import Lot
|
||||
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')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceListMix(GenericMixView):
|
||||
class DeviceListMixin(GenericMixin):
|
||||
template_name = 'inventory/device_list.html'
|
||||
|
||||
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)
|
||||
devices = form_filter.search()
|
||||
lot = None
|
||||
form_transfer = ''
|
||||
form_delivery = ''
|
||||
form_receiver = ''
|
||||
|
||||
if lot_id:
|
||||
lot = lots.filter(Lot.id == lot_id).one()
|
||||
form_new_trade = TradeForm(
|
||||
lot=lot.id,
|
||||
user_to=g.user.email,
|
||||
user_from=g.user.email,
|
||||
)
|
||||
else:
|
||||
form_new_trade = ''
|
||||
if not lot.is_temporary and lot.transfer:
|
||||
form_transfer = EditTransferForm(lot_id=lot.id)
|
||||
form_delivery = NotesForm(lot_id=lot.id, type='Delivery')
|
||||
form_receiver = NotesForm(lot_id=lot.id, type='Receiver')
|
||||
|
||||
form_new_action = NewActionForm(lot=lot_id)
|
||||
self.context.update(
|
||||
|
@ -67,7 +71,9 @@ class DeviceListMix(GenericMixView):
|
|||
'form_new_action': form_new_action,
|
||||
'form_new_allocate': AllocateForm(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_print_labels': PrintLabelsForm(),
|
||||
'lot': lot,
|
||||
|
@ -94,7 +100,7 @@ class DeviceListMix(GenericMixView):
|
|||
return []
|
||||
|
||||
|
||||
class DeviceListView(DeviceListMix):
|
||||
class DeviceListView(DeviceListMixin):
|
||||
def dispatch_request(self, lot_id=None):
|
||||
only_unassigned = request.args.get(
|
||||
'only_unassigned', default=True, type=strtobool
|
||||
|
@ -103,7 +109,7 @@ class DeviceListView(DeviceListMix):
|
|||
return flask.render_template(self.template_name, **self.context)
|
||||
|
||||
|
||||
class AdvancedSearchView(DeviceListMix):
|
||||
class AdvancedSearchView(DeviceListMixin):
|
||||
methods = ['GET', 'POST']
|
||||
template_name = 'inventory/search.html'
|
||||
title = "Advanced Search"
|
||||
|
@ -116,7 +122,7 @@ class AdvancedSearchView(DeviceListMix):
|
|||
return flask.render_template(self.template_name, **self.context)
|
||||
|
||||
|
||||
class DeviceDetailView(GenericMixView):
|
||||
class DeviceDetailView(GenericMixin):
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/device_detail.html'
|
||||
|
||||
|
@ -137,7 +143,7 @@ class DeviceDetailView(GenericMixView):
|
|||
return flask.render_template(self.template_name, **self.context)
|
||||
|
||||
|
||||
class LotCreateView(GenericMixView):
|
||||
class LotCreateView(GenericMixin):
|
||||
methods = ['GET', 'POST']
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/lot.html'
|
||||
|
@ -160,7 +166,7 @@ class LotCreateView(GenericMixView):
|
|||
return flask.render_template(self.template_name, **self.context)
|
||||
|
||||
|
||||
class LotUpdateView(GenericMixView):
|
||||
class LotUpdateView(GenericMixin):
|
||||
methods = ['GET', 'POST']
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/lot.html'
|
||||
|
@ -201,7 +207,7 @@ class LotDeleteView(View):
|
|||
return flask.redirect(next_url)
|
||||
|
||||
|
||||
class UploadSnapshotView(GenericMixView):
|
||||
class UploadSnapshotView(GenericMixin):
|
||||
methods = ['GET', 'POST']
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/upload_snapshot.html'
|
||||
|
@ -217,18 +223,19 @@ class UploadSnapshotView(GenericMixView):
|
|||
}
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
snapshot = form.save(commit=False)
|
||||
snapshot, devices = form.save(commit=False)
|
||||
if lot_id:
|
||||
lots = self.context['lots']
|
||||
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.commit()
|
||||
|
||||
return flask.render_template(self.template_name, **self.context)
|
||||
|
||||
|
||||
class DeviceCreateView(GenericMixView):
|
||||
class DeviceCreateView(GenericMixin):
|
||||
methods = ['GET', 'POST']
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/device_create.html'
|
||||
|
@ -273,7 +280,7 @@ class TagLinkDeviceView(View):
|
|||
return flask.redirect(request.referrer)
|
||||
|
||||
|
||||
class TagUnlinkDeviceView(GenericMixView):
|
||||
class TagUnlinkDeviceView(GenericMixin):
|
||||
methods = ['POST', 'GET']
|
||||
decorators = [login_required]
|
||||
template_name = 'inventory/tag_unlink_device.html'
|
||||
|
@ -326,7 +333,7 @@ class NewActionView(View):
|
|||
return url_for('inventory.devicelist')
|
||||
|
||||
|
||||
class NewAllocateView(NewActionView, DeviceListMix):
|
||||
class NewAllocateView(DeviceListMixin, NewActionView):
|
||||
methods = ['POST']
|
||||
form_class = AllocateForm
|
||||
|
||||
|
@ -351,7 +358,7 @@ class NewAllocateView(NewActionView, DeviceListMix):
|
|||
return flask.redirect(next_url)
|
||||
|
||||
|
||||
class NewDataWipeView(NewActionView, DeviceListMix):
|
||||
class NewDataWipeView(DeviceListMixin, NewActionView):
|
||||
methods = ['POST']
|
||||
form_class = DataWipeForm
|
||||
|
||||
|
@ -372,7 +379,7 @@ class NewDataWipeView(NewActionView, DeviceListMix):
|
|||
return flask.redirect(next_url)
|
||||
|
||||
|
||||
class NewTradeView(NewActionView, DeviceListMix):
|
||||
class NewTradeView(DeviceListMixin, NewActionView):
|
||||
methods = ['POST']
|
||||
form_class = TradeForm
|
||||
|
||||
|
@ -414,6 +421,52 @@ class NewTradeDocumentView(View):
|
|||
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):
|
||||
methods = ['GET']
|
||||
decorators = [login_required]
|
||||
|
@ -525,6 +578,114 @@ class ExportsView(View):
|
|||
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/trade/add/', view_func=NewTradeView.as_view('trade_add'))
|
||||
devices.add_url_rule(
|
||||
|
@ -574,3 +735,24 @@ devices.add_url_rule(
|
|||
devices.add_url_rule(
|
||||
'/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'),
|
||||
)
|
||||
|
|
|
@ -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()}')
|
|
@ -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()}')
|
|
@ -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;")
|
|
@ -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()}')
|
|
@ -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;")
|
|
@ -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()}')
|
|
@ -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()}')
|
|
@ -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
|
|
@ -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)
|
|
@ -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 ''
|
|
@ -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()
|
|
@ -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'],
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
K = KiB = k = kb = KB
|
||||
M = MiB = m = mb = MB
|
||||
G = GiB = g = gb = GB
|
||||
T = TiB = t = tb = TB
|
|
@ -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
|
|
@ -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
|
@ -1,14 +1,28 @@
|
|||
import copy
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.tz import tzutc
|
||||
from flask import current_app as app, g
|
||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema, pre_load, post_load
|
||||
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \
|
||||
TimeDelta, UUID
|
||||
from flask import current_app as app
|
||||
from flask import g
|
||||
from marshmallow import Schema as MarshmallowSchema
|
||||
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 sqlalchemy.util import OrderedSet
|
||||
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 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.agent import schemas as s_agent
|
||||
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.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \
|
||||
PhysicalErasureMethod, R_POSITIVE, RatingRange, \
|
||||
Severity, SnapshotSoftware, TestDataStorageLength
|
||||
from ereuse_devicehub.resources.enums import (
|
||||
R_POSITIVE,
|
||||
AppearanceRange,
|
||||
BiosAccessRange,
|
||||
FunctionalityRange,
|
||||
PhysicalErasureMethod,
|
||||
RatingRange,
|
||||
Severity,
|
||||
SnapshotSoftware,
|
||||
TestDataStorageLength,
|
||||
)
|
||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||
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.models import User
|
||||
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
|
||||
|
||||
|
||||
class Action(Thing):
|
||||
__doc__ = m.Action.__doc__
|
||||
id = UUID(dump_only=True)
|
||||
name = SanitizedStr(default='',
|
||||
validate=Length(max=STR_BIG_SIZE),
|
||||
description=m.Action.name.comment)
|
||||
name = SanitizedStr(
|
||||
default='', validate=Length(max=STR_BIG_SIZE), description=m.Action.name.comment
|
||||
)
|
||||
closed = Boolean(missing=True, description=m.Action.closed.comment)
|
||||
severity = EnumField(Severity, description=m.Action.severity.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)
|
||||
author = NestedOn(s_user.User, dump_only=True, exclude=('token',))
|
||||
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__)
|
||||
|
||||
@validates_schema
|
||||
def validate_times(self, data: dict):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if data.get('end_time') and data.get('start_time'):
|
||||
|
@ -67,24 +91,27 @@ class ActionWithOneDevice(Action):
|
|||
|
||||
class ActionWithMultipleDocuments(Action):
|
||||
__doc__ = m.ActionWithMultipleTradeDocuments.__doc__
|
||||
documents = NestedOn(s_document.TradeDocument,
|
||||
many=True,
|
||||
required=True, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet)
|
||||
documents = NestedOn(
|
||||
s_document.TradeDocument,
|
||||
many=True,
|
||||
required=True, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet,
|
||||
)
|
||||
|
||||
|
||||
class ActionWithMultipleDevices(Action):
|
||||
__doc__ = m.ActionWithMultipleDevices.__doc__
|
||||
devices = NestedOn(s_device.Device,
|
||||
many=True,
|
||||
required=True, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet)
|
||||
devices = NestedOn(
|
||||
s_device.Device,
|
||||
many=True,
|
||||
required=True, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet,
|
||||
)
|
||||
|
||||
|
||||
class ActionWithMultipleDevicesCheckingOwner(ActionWithMultipleDevices):
|
||||
|
||||
@post_load
|
||||
def check_owner_of_device(self, data):
|
||||
for dev in data['devices']:
|
||||
|
@ -102,20 +129,29 @@ class Remove(ActionWithOneDevice):
|
|||
|
||||
class Allocate(ActionWithMultipleDevicesCheckingOwner):
|
||||
__doc__ = m.Allocate.__doc__
|
||||
start_time = DateTime(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)
|
||||
final_user_code = SanitizedStr(data_key="finalUserCode",
|
||||
validate=Length(min=1, max=STR_BIG_SIZE),
|
||||
required=False,
|
||||
description='This is a internal code for mainteing the secrets of the \
|
||||
personal datas of the new holder')
|
||||
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")])
|
||||
start_time = DateTime(
|
||||
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
|
||||
)
|
||||
final_user_code = SanitizedStr(
|
||||
data_key="finalUserCode",
|
||||
validate=Length(min=1, max=STR_BIG_SIZE),
|
||||
required=False,
|
||||
description='This is a internal code for mainteing the secrets of the \
|
||||
personal datas of the new holder',
|
||||
)
|
||||
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
|
||||
def validate_allocate(self, data: dict):
|
||||
|
@ -136,12 +172,15 @@ class Allocate(ActionWithMultipleDevicesCheckingOwner):
|
|||
|
||||
class Deallocate(ActionWithMultipleDevicesCheckingOwner):
|
||||
__doc__ = m.Deallocate.__doc__
|
||||
start_time = DateTime(data_key='startTime', required=True,
|
||||
description=m.Action.start_time.comment)
|
||||
transaction = SanitizedStr(validate=Length(min=1, max=STR_BIG_SIZE),
|
||||
required=False,
|
||||
description='The code used from the owner for \
|
||||
relation with external tool.')
|
||||
start_time = DateTime(
|
||||
data_key='startTime', required=True, description=m.Action.start_time.comment
|
||||
)
|
||||
transaction = SanitizedStr(
|
||||
validate=Length(min=1, max=STR_BIG_SIZE),
|
||||
required=False,
|
||||
description='The code used from the owner for \
|
||||
relation with external tool.',
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_deallocate(self, data: dict):
|
||||
|
@ -232,7 +271,9 @@ class MeasureBattery(Test):
|
|||
__doc__ = m.MeasureBattery.__doc__
|
||||
size = Integer(required=True, description=m.MeasureBattery.size.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)
|
||||
|
||||
|
||||
|
@ -289,28 +330,32 @@ class TestBios(Test):
|
|||
class VisualTest(Test):
|
||||
__doc__ = m.VisualTest.__doc__
|
||||
appearance_range = EnumField(AppearanceRange, data_key='appearanceRange')
|
||||
functionality_range = EnumField(FunctionalityRange,
|
||||
data_key='functionalityRange')
|
||||
functionality_range = EnumField(FunctionalityRange, data_key='functionalityRange')
|
||||
labelling = Boolean()
|
||||
|
||||
|
||||
class Rate(ActionWithOneDevice):
|
||||
__doc__ = m.Rate.__doc__
|
||||
rating = Integer(validate=Range(*R_POSITIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate._rating.comment)
|
||||
version = Version(dump_only=True,
|
||||
description=m.Rate.version.comment)
|
||||
appearance = Integer(validate=Range(enums.R_NEGATIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate._appearance.comment)
|
||||
functionality = Integer(validate=Range(enums.R_NEGATIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate._functionality.comment)
|
||||
rating_range = EnumField(RatingRange,
|
||||
dump_only=True,
|
||||
data_key='ratingRange',
|
||||
description=m.Rate.rating_range.__doc__)
|
||||
rating = Integer(
|
||||
validate=Range(*R_POSITIVE), dump_only=True, description=m.Rate._rating.comment
|
||||
)
|
||||
version = Version(dump_only=True, description=m.Rate.version.comment)
|
||||
appearance = Integer(
|
||||
validate=Range(enums.R_NEGATIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate._appearance.comment,
|
||||
)
|
||||
functionality = Integer(
|
||||
validate=Range(enums.R_NEGATIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate._functionality.comment,
|
||||
)
|
||||
rating_range = EnumField(
|
||||
RatingRange,
|
||||
dump_only=True,
|
||||
data_key='ratingRange',
|
||||
description=m.Rate.rating_range.__doc__,
|
||||
)
|
||||
|
||||
|
||||
class RateComputer(Rate):
|
||||
|
@ -320,19 +365,25 @@ class RateComputer(Rate):
|
|||
data_storage = Float(dump_only=True, data_key='dataStorage')
|
||||
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')
|
||||
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):
|
||||
__doc__ = m.Price.__doc__
|
||||
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
|
||||
price = Decimal(places=m.Price.SCALE,
|
||||
rounding=m.Price.ROUND,
|
||||
required=True,
|
||||
description=m.Price.price.comment)
|
||||
price = Decimal(
|
||||
places=m.Price.SCALE,
|
||||
rounding=m.Price.ROUND,
|
||||
required=True,
|
||||
description=m.Price.price.comment,
|
||||
)
|
||||
version = Version(dump_only=True, description=m.Price.version.comment)
|
||||
rating = NestedOn(Rate, dump_only=True, description=m.Price.rating_id.comment)
|
||||
|
||||
|
@ -356,9 +407,11 @@ class EreusePrice(Price):
|
|||
|
||||
class Install(ActionWithOneDevice):
|
||||
__doc__ = m.Install.__doc__
|
||||
name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE),
|
||||
required=True,
|
||||
description='The name of the OS installed.')
|
||||
name = SanitizedStr(
|
||||
validate=Length(min=4, max=STR_BIG_SIZE),
|
||||
required=True,
|
||||
description='The name of the OS installed.',
|
||||
)
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
|
||||
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))
|
||||
|
||||
|
@ -372,18 +425,23 @@ class Snapshot(ActionWithOneDevice):
|
|||
See docs for more info.
|
||||
"""
|
||||
uuid = UUID()
|
||||
software = EnumField(SnapshotSoftware,
|
||||
required=True,
|
||||
description='The software that generated this Snapshot.')
|
||||
sid = String(required=False)
|
||||
software = EnumField(
|
||||
SnapshotSoftware,
|
||||
required=True,
|
||||
description='The software that generated this Snapshot.',
|
||||
)
|
||||
version = Version(required=True, description='The version of the software.')
|
||||
actions = NestedOn(Action, many=True, dump_only=True)
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
||||
components = NestedOn(s_device.Component,
|
||||
many=True,
|
||||
description='A list of components that are inside of the device'
|
||||
'at the moment of this Snapshot.'
|
||||
'Order is preserved, so the component num 0 when'
|
||||
'submitting is the component num 0 when returning it back.')
|
||||
components = NestedOn(
|
||||
s_device.Component,
|
||||
many=True,
|
||||
description='A list of components that are inside of the device'
|
||||
'at the moment of this Snapshot.'
|
||||
'Order is preserved, so the component num 0 when'
|
||||
'submitting is the component num 0 when returning it back.',
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_workbench_version(self, data: dict):
|
||||
|
@ -391,16 +449,21 @@ class Snapshot(ActionWithOneDevice):
|
|||
if data['version'] < app.config['MIN_WORKBENCH']:
|
||||
raise ValidationError(
|
||||
'Min. supported Workbench version is '
|
||||
'{} but yours is {}.'.format(app.config['MIN_WORKBENCH'], data['version']),
|
||||
field_names=['version']
|
||||
'{} but yours is {}.'.format(
|
||||
app.config['MIN_WORKBENCH'], data['version']
|
||||
),
|
||||
field_names=['version'],
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
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:
|
||||
raise ValidationError('Only Workbench can add component info',
|
||||
field_names=['components'])
|
||||
raise ValidationError(
|
||||
'Only Workbench can add component info', field_names=['components']
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_only_workbench_fields(self, data: dict):
|
||||
|
@ -408,22 +471,32 @@ class Snapshot(ActionWithOneDevice):
|
|||
# todo test
|
||||
if data['software'] == SnapshotSoftware.Workbench:
|
||||
if not data.get('uuid', None):
|
||||
raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid',
|
||||
field_names=['uuid'])
|
||||
raise ValidationError(
|
||||
'Snapshots from Workbench and WorkbenchAndroid must have uuid',
|
||||
field_names=['uuid'],
|
||||
)
|
||||
if data.get('elapsed', None) is None:
|
||||
raise ValidationError('Snapshots from Workbench must have elapsed',
|
||||
field_names=['elapsed'])
|
||||
raise ValidationError(
|
||||
'Snapshots from Workbench must have elapsed',
|
||||
field_names=['elapsed'],
|
||||
)
|
||||
elif data['software'] == SnapshotSoftware.WorkbenchAndroid:
|
||||
if not data.get('uuid', None):
|
||||
raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid',
|
||||
field_names=['uuid'])
|
||||
raise ValidationError(
|
||||
'Snapshots from Workbench and WorkbenchAndroid must have uuid',
|
||||
field_names=['uuid'],
|
||||
)
|
||||
else:
|
||||
if data.get('uuid', None):
|
||||
raise ValidationError('Only Snapshots from Workbench or WorkbenchAndroid can have uuid',
|
||||
field_names=['uuid'])
|
||||
raise ValidationError(
|
||||
'Only Snapshots from Workbench or WorkbenchAndroid can have uuid',
|
||||
field_names=['uuid'],
|
||||
)
|
||||
if data.get('elapsed', None):
|
||||
raise ValidationError('Only Snapshots from Workbench can have elapsed',
|
||||
field_names=['elapsed'])
|
||||
raise ValidationError(
|
||||
'Only Snapshots from Workbench can have elapsed',
|
||||
field_names=['elapsed'],
|
||||
)
|
||||
|
||||
|
||||
class ToRepair(ActionWithMultipleDevicesCheckingOwner):
|
||||
|
@ -440,16 +513,20 @@ class Ready(ActionWithMultipleDevicesCheckingOwner):
|
|||
|
||||
class ActionStatus(Action):
|
||||
rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
|
||||
devices = NestedOn(s_device.Device,
|
||||
many=True,
|
||||
required=False, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet)
|
||||
documents = NestedOn(s_document.TradeDocument,
|
||||
many=True,
|
||||
required=False, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet)
|
||||
devices = NestedOn(
|
||||
s_device.Device,
|
||||
many=True,
|
||||
required=False, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet,
|
||||
)
|
||||
documents = NestedOn(
|
||||
s_document.TradeDocument,
|
||||
many=True,
|
||||
required=False, # todo test ensuring len(devices) >= 1
|
||||
only_query='id',
|
||||
collection_class=OrderedSet,
|
||||
)
|
||||
|
||||
@pre_load
|
||||
def put_devices(self, data: dict):
|
||||
|
@ -508,20 +585,28 @@ class Live(ActionWithOneDevice):
|
|||
See docs for more info.
|
||||
"""
|
||||
uuid = UUID()
|
||||
software = EnumField(SnapshotSoftware,
|
||||
required=True,
|
||||
description='The software that generated this Snapshot.')
|
||||
software = EnumField(
|
||||
SnapshotSoftware,
|
||||
required=True,
|
||||
description='The software that generated this Snapshot.',
|
||||
)
|
||||
version = Version(required=True, description='The version of the software.')
|
||||
final_user_code = SanitizedStr(data_key="finalUserCode", dump_only=True)
|
||||
licence_version = Version(required=True, description='The version of the software.')
|
||||
components = NestedOn(s_device.Component,
|
||||
many=True,
|
||||
description='A list of components that are inside of the device'
|
||||
'at the moment of this Snapshot.'
|
||||
'Order is preserved, so the component num 0 when'
|
||||
'submitting is the component num 0 when returning it back.')
|
||||
usage_time_allocate = TimeDelta(data_key='usageTimeAllocate', required=False,
|
||||
precision=TimeDelta.HOURS, dump_only=True)
|
||||
components = NestedOn(
|
||||
s_device.Component,
|
||||
many=True,
|
||||
description='A list of components that are inside of the device'
|
||||
'at the moment of this Snapshot.'
|
||||
'Order is preserved, so the component num 0 when'
|
||||
'submitting is the component num 0 when returning it back.',
|
||||
)
|
||||
usage_time_allocate = TimeDelta(
|
||||
data_key='usageTimeAllocate',
|
||||
required=False,
|
||||
precision=TimeDelta.HOURS,
|
||||
dump_only=True,
|
||||
)
|
||||
|
||||
|
||||
class Organize(ActionWithMultipleDevices):
|
||||
|
@ -570,7 +655,7 @@ class Revoke(ActionWithMultipleDevices):
|
|||
@validates_schema
|
||||
def validate_documents(self, data):
|
||||
"""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():
|
||||
|
@ -610,7 +695,7 @@ class ConfirmDocument(ActionWithMultipleDocuments):
|
|||
@validates_schema
|
||||
def validate_documents(self, data):
|
||||
"""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():
|
||||
return
|
||||
|
@ -636,7 +721,7 @@ class RevokeDocument(ActionWithMultipleDocuments):
|
|||
@validates_schema
|
||||
def validate_documents(self, data):
|
||||
"""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():
|
||||
|
@ -663,7 +748,7 @@ class ConfirmRevokeDocument(ActionWithMultipleDocuments):
|
|||
@validates_schema
|
||||
def validate_documents(self, data):
|
||||
"""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():
|
||||
|
@ -691,26 +776,23 @@ class Trade(ActionWithMultipleDevices):
|
|||
validate=Length(max=STR_SIZE),
|
||||
data_key='userToEmail',
|
||||
missing='',
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
user_to = NestedOn(s_user.User, dump_only=True, data_key='userTo')
|
||||
user_from_email = SanitizedStr(
|
||||
validate=Length(max=STR_SIZE),
|
||||
data_key='userFromEmail',
|
||||
missing='',
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
user_from = NestedOn(s_user.User, dump_only=True, data_key='userFrom')
|
||||
code = SanitizedStr(validate=Length(max=STR_SIZE), data_key='code', required=False)
|
||||
confirm = Boolean(
|
||||
data_key='confirms',
|
||||
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',
|
||||
many=False,
|
||||
required=True,
|
||||
only_query='id')
|
||||
lot = NestedOn('Lot', many=False, required=True, only_query='id')
|
||||
|
||||
@pre_load
|
||||
def adding_devices(self, data: dict):
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
""" This is the view for Snapshots """
|
||||
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
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 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.action.rate.v1_0 import CannotRate
|
||||
from ereuse_devicehub.resources.enums import SnapshotSoftware, Severity
|
||||
from ereuse_devicehub.resources.device.sync import Sync
|
||||
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
|
||||
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)
|
||||
|
||||
|
||||
class SnapshotView():
|
||||
"""Performs a Snapshot.
|
||||
class SnapshotMixin:
|
||||
sync = Sync()
|
||||
|
||||
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):
|
||||
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
|
||||
def build(self, snapshot_json=None): # noqa: C901
|
||||
if not snapshot_json:
|
||||
snapshot_json = self.snapshot_json
|
||||
device = snapshot_json.pop('device') # type: Computer
|
||||
components = None
|
||||
if self.snapshot_json['software'] == (SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid):
|
||||
components = self.snapshot_json.pop('components', None) # type: List[Component]
|
||||
if snapshot_json['software'] == (
|
||||
SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid
|
||||
):
|
||||
components = snapshot_json.pop('components', None) # type: List[Component]
|
||||
if isinstance(device, Computer) and device.hid:
|
||||
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
|
||||
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)
|
||||
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.resource_def.sync.run(device, components)
|
||||
db_device, remove_actions = self.sync.run(device, components)
|
||||
|
||||
del device # Do not use device anymore
|
||||
snapshot.device = db_device
|
||||
|
@ -120,24 +109,63 @@ class SnapshotView():
|
|||
# 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
|
||||
|
||||
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)
|
||||
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()
|
||||
ret = self.schema.jsonify(snapshot) # transform it back
|
||||
ret.status_code = 201
|
||||
self.response = self.schema.jsonify(snapshot) # transform it back
|
||||
self.response.status_code = 201
|
||||
db.session.commit()
|
||||
return ret
|
||||
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
|
||||
|
||||
def post(self):
|
||||
return self.response
|
||||
|
|
|
@ -1,29 +1,34 @@
|
|||
from flask import g
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from teal.marshmallow import ValidationError
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.action.models import (Trade, Confirm,
|
||||
Revoke, RevokeDocument, ConfirmDocument,
|
||||
ConfirmRevokeDocument)
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
from ereuse_devicehub.inventory.models import Transfer
|
||||
from ereuse_devicehub.resources.action.models import (
|
||||
Confirm,
|
||||
ConfirmDocument,
|
||||
ConfirmRevokeDocument,
|
||||
Revoke,
|
||||
RevokeDocument,
|
||||
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
|
||||
|
||||
request_post = {
|
||||
'type': 'Trade',
|
||||
'devices': [device_id],
|
||||
'documents': [document_id],
|
||||
'userFrom': user2.email,
|
||||
'userTo': user.email,
|
||||
'price': 10,
|
||||
'date': "2020-12-01T02:00:00+00:00",
|
||||
'lot': lot['id'],
|
||||
'confirm': True,
|
||||
}
|
||||
request_post = {
|
||||
'type': 'Trade',
|
||||
'devices': [device_id],
|
||||
'documents': [document_id],
|
||||
'userFrom': user2.email,
|
||||
'userTo': user.email,
|
||||
'price': 10,
|
||||
'date': "2020-12-01T02:00:00+00:00",
|
||||
'lot': lot['id'],
|
||||
'confirm': True,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
@ -37,6 +42,7 @@ class TradeView():
|
|||
db.session.add(self.trade)
|
||||
self.create_confirmations()
|
||||
self.create_automatic_trade()
|
||||
self.create_transfer()
|
||||
|
||||
def post(self):
|
||||
db.session().final_flush()
|
||||
|
@ -52,15 +58,15 @@ class TradeView():
|
|||
# owner of the lot
|
||||
if self.trade.confirm:
|
||||
if self.trade.devices:
|
||||
confirm_devs = Confirm(user=g.user,
|
||||
action=self.trade,
|
||||
devices=self.trade.devices)
|
||||
confirm_devs = Confirm(
|
||||
user=g.user, action=self.trade, devices=self.trade.devices
|
||||
)
|
||||
db.session.add(confirm_devs)
|
||||
|
||||
if self.trade.documents:
|
||||
confirm_docs = ConfirmDocument(user=g.user,
|
||||
action=self.trade,
|
||||
documents=self.trade.documents)
|
||||
confirm_docs = ConfirmDocument(
|
||||
user=g.user, action=self.trade, documents=self.trade.documents
|
||||
)
|
||||
db.session.add(confirm_docs)
|
||||
return
|
||||
|
||||
|
@ -70,12 +76,12 @@ class TradeView():
|
|||
txt = "You do not participate in this trading"
|
||||
raise ValidationError(txt)
|
||||
|
||||
confirm_from = Confirm(user=self.trade.user_from,
|
||||
action=self.trade,
|
||||
devices=self.trade.devices)
|
||||
confirm_to = Confirm(user=self.trade.user_to,
|
||||
action=self.trade,
|
||||
devices=self.trade.devices)
|
||||
confirm_from = Confirm(
|
||||
user=self.trade.user_from, action=self.trade, devices=self.trade.devices
|
||||
)
|
||||
confirm_to = Confirm(
|
||||
user=self.trade.user_to, action=self.trade, devices=self.trade.devices
|
||||
)
|
||||
db.session.add(confirm_from)
|
||||
db.session.add(confirm_to)
|
||||
|
||||
|
@ -124,6 +130,25 @@ class TradeView():
|
|||
db.session.add(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:
|
||||
# not do nothing if it's neccesary confirmation explicity
|
||||
if self.trade.confirm:
|
||||
|
@ -134,15 +159,15 @@ class TradeView():
|
|||
dev.change_owner(self.trade.user_to)
|
||||
|
||||
|
||||
class ConfirmMixin():
|
||||
class ConfirmMixin:
|
||||
"""
|
||||
Very Important:
|
||||
==============
|
||||
All of this Views than inherit of this class is executed for users
|
||||
than is not owner of the Trade action.
|
||||
Very Important:
|
||||
==============
|
||||
All of this Views than inherit of this class is executed for users
|
||||
than is not owner of the Trade action.
|
||||
|
||||
The owner of Trade action executed this actions of confirm and revoke from the
|
||||
lot
|
||||
The owner of Trade action executed this actions of confirm and revoke from the
|
||||
lot
|
||||
|
||||
"""
|
||||
|
||||
|
@ -167,24 +192,27 @@ class ConfirmMixin():
|
|||
class ConfirmView(ConfirmMixin):
|
||||
"""Handler for manager the Confirmation register from post
|
||||
|
||||
request_confirm = {
|
||||
'type': 'Confirm',
|
||||
'action': trade.id,
|
||||
'devices': [device_id]
|
||||
}
|
||||
request_confirm = {
|
||||
'type': 'Confirm',
|
||||
'action': trade.id,
|
||||
'devices': [device_id]
|
||||
}
|
||||
"""
|
||||
|
||||
Model = Confirm
|
||||
|
||||
def validate(self, data):
|
||||
"""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 = []
|
||||
trade = data['action']
|
||||
lot = trade.lot
|
||||
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.')
|
||||
|
||||
# Change the owner for every devices
|
||||
|
@ -197,11 +225,11 @@ class ConfirmView(ConfirmMixin):
|
|||
class RevokeView(ConfirmMixin):
|
||||
"""Handler for manager the Revoke register from post
|
||||
|
||||
request_revoke = {
|
||||
'type': 'Revoke',
|
||||
'action': trade.id,
|
||||
'devices': [device_id],
|
||||
}
|
||||
request_revoke = {
|
||||
'type': 'Revoke',
|
||||
'action': trade.id,
|
||||
'devices': [device_id],
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
@ -223,15 +251,15 @@ class RevokeView(ConfirmMixin):
|
|||
self.model = delete_from_trade(lot, devices)
|
||||
|
||||
|
||||
class ConfirmDocumentMixin():
|
||||
class ConfirmDocumentMixin:
|
||||
"""
|
||||
Very Important:
|
||||
==============
|
||||
All of this Views than inherit of this class is executed for users
|
||||
than is not owner of the Trade action.
|
||||
Very Important:
|
||||
==============
|
||||
All of this Views than inherit of this class is executed for users
|
||||
than is not owner of the Trade action.
|
||||
|
||||
The owner of Trade action executed this actions of confirm and revoke from the
|
||||
lot
|
||||
The owner of Trade action executed this actions of confirm and revoke from the
|
||||
lot
|
||||
|
||||
"""
|
||||
|
||||
|
@ -256,18 +284,18 @@ class ConfirmDocumentMixin():
|
|||
class ConfirmDocumentView(ConfirmDocumentMixin):
|
||||
"""Handler for manager the Confirmation register from post
|
||||
|
||||
request_confirm = {
|
||||
'type': 'Confirm',
|
||||
'action': trade.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
request_confirm = {
|
||||
'type': 'Confirm',
|
||||
'action': trade.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
"""
|
||||
|
||||
Model = ConfirmDocument
|
||||
|
||||
def validate(self, data):
|
||||
"""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']:
|
||||
ac = doc.trading
|
||||
|
@ -280,11 +308,11 @@ class ConfirmDocumentView(ConfirmDocumentMixin):
|
|||
class RevokeDocumentView(ConfirmDocumentMixin):
|
||||
"""Handler for manager the Revoke register from post
|
||||
|
||||
request_revoke = {
|
||||
'type': 'Revoke',
|
||||
'action': trade.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
request_revoke = {
|
||||
'type': 'Revoke',
|
||||
'action': trade.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
@ -299,7 +327,9 @@ class RevokeDocumentView(ConfirmDocumentMixin):
|
|||
|
||||
for doc in data['documents']:
|
||||
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)
|
||||
### End check ###
|
||||
|
||||
|
@ -307,11 +337,11 @@ class RevokeDocumentView(ConfirmDocumentMixin):
|
|||
class ConfirmRevokeDocumentView(ConfirmDocumentMixin):
|
||||
"""Handler for manager the Confirmation register from post
|
||||
|
||||
request_confirm_revoke = {
|
||||
'type': 'ConfirmRevoke',
|
||||
'action': action_revoke.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
request_confirm_revoke = {
|
||||
'type': 'ConfirmRevoke',
|
||||
'action': action_revoke.id,
|
||||
'documents': [document_id],
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,35 +1,49 @@
|
|||
""" This is the view for Snapshots """
|
||||
|
||||
import jwt
|
||||
import ereuse_utils
|
||||
from datetime import timedelta
|
||||
from distutils.version import StrictVersion
|
||||
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.marshmallow import ValidationError
|
||||
from teal.resource import View
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.query import things_response
|
||||
from ereuse_devicehub.resources.action.models import (Action, Snapshot, VisualTest,
|
||||
InitTransfer, Live, Allocate, Deallocate,
|
||||
Trade, Confirm, Revoke)
|
||||
from ereuse_devicehub.resources.action.models import (
|
||||
Action,
|
||||
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.snapshot import SnapshotView, save_json, move_json
|
||||
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
|
||||
|
||||
SUPPORTED_WORKBENCH = StrictVersion('11.0')
|
||||
|
||||
|
||||
class AllocateMix():
|
||||
class AllocateMix:
|
||||
model = None
|
||||
|
||||
def post(self):
|
||||
""" Create one res_obj """
|
||||
"""Create one res_obj"""
|
||||
res_json = request.get_json()
|
||||
res_obj = self.model(**res_json)
|
||||
db.session.add(res_obj)
|
||||
|
@ -40,13 +54,18 @@ class AllocateMix():
|
|||
return ret
|
||||
|
||||
def find(self, args: dict):
|
||||
res_objs = self.model.query.filter_by(author=g.user) \
|
||||
.order_by(self.model.created.desc()) \
|
||||
res_objs = (
|
||||
self.model.query.filter_by(author=g.user)
|
||||
.order_by(self.model.created.desc())
|
||||
.paginate(per_page=200)
|
||||
)
|
||||
return things_response(
|
||||
self.schema.dump(res_objs.items, many=True, nested=0),
|
||||
res_objs.page, res_objs.per_page, res_objs.total,
|
||||
res_objs.prev_num, res_objs.next_num
|
||||
res_objs.page,
|
||||
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:
|
||||
"""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
|
||||
|
||||
def get_hid(self, snapshot):
|
||||
|
@ -109,8 +130,11 @@ class LiveView(View):
|
|||
return None
|
||||
if not components:
|
||||
return device.hid
|
||||
macs = [c.serial_number for c in components
|
||||
if c.type == 'NetworkAdapter' and c.serial_number is not None]
|
||||
macs = [
|
||||
c.serial_number
|
||||
for c in components
|
||||
if c.type == 'NetworkAdapter' and c.serial_number is not None
|
||||
]
|
||||
macs.sort()
|
||||
mac = ''
|
||||
hid = device.hid
|
||||
|
@ -124,12 +148,10 @@ class LiveView(View):
|
|||
def live(self, snapshot):
|
||||
"""If the device.allocated == True, then this snapshot create an action live."""
|
||||
hid = self.get_hid(snapshot)
|
||||
if not hid or not Device.query.filter(
|
||||
Device.hid == hid).count():
|
||||
if not hid or not Device.query.filter(Device.hid == hid).count():
|
||||
raise ValidationError('Device not exist.')
|
||||
|
||||
device = Device.query.filter(
|
||||
Device.hid == hid, Device.allocated == True).one()
|
||||
device = Device.query.filter(Device.hid == hid, Device.allocated == True).one()
|
||||
# Is not necessary
|
||||
if not device:
|
||||
raise ValidationError('Device not exist.')
|
||||
|
@ -138,16 +160,18 @@ class LiveView(View):
|
|||
|
||||
usage_time_hdd, serial_number = self.get_hdd_details(snapshot, device)
|
||||
|
||||
data_live = {'usage_time_hdd': usage_time_hdd,
|
||||
'serial_number': serial_number,
|
||||
'snapshot_uuid': snapshot['uuid'],
|
||||
'description': '',
|
||||
'software': snapshot['software'],
|
||||
'software_version': snapshot['version'],
|
||||
'licence_version': snapshot['licence_version'],
|
||||
'author_id': device.owner_id,
|
||||
'agent_id': device.owner.individual.id,
|
||||
'device': device}
|
||||
data_live = {
|
||||
'usage_time_hdd': usage_time_hdd,
|
||||
'serial_number': serial_number,
|
||||
'snapshot_uuid': snapshot['uuid'],
|
||||
'description': '',
|
||||
'software': snapshot['software'],
|
||||
'software_version': snapshot['version'],
|
||||
'licence_version': snapshot['licence_version'],
|
||||
'author_id': device.owner_id,
|
||||
'agent_id': device.owner.individual.id,
|
||||
'device': device,
|
||||
}
|
||||
|
||||
live = Live(**data_live)
|
||||
|
||||
|
@ -172,7 +196,12 @@ class LiveView(View):
|
|||
|
||||
def decode_snapshot(data):
|
||||
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:
|
||||
txt = 'Invalid snapshot'
|
||||
raise ValidationError(txt)
|
||||
|
@ -200,13 +229,13 @@ class ActionView(View):
|
|||
|
||||
# TODO @cayop uncomment at four weeks
|
||||
# if not 'data' in json:
|
||||
# txt = 'Invalid snapshot'
|
||||
# raise ValidationError(txt)
|
||||
# txt = 'Invalid snapshot'
|
||||
# raise ValidationError(txt)
|
||||
|
||||
# snapshot_data = decode_snapshot(json)
|
||||
|
||||
snapshot_data = json
|
||||
if 'data' in json:
|
||||
if 'data' in json and isinstance(json['data'], str):
|
||||
snapshot_data = decode_snapshot(json)
|
||||
|
||||
if not snapshot_data:
|
||||
|
@ -248,7 +277,9 @@ class ActionView(View):
|
|||
return confirm.post()
|
||||
|
||||
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()
|
||||
|
||||
if json['type'] == 'DataWipe':
|
||||
|
|
|
@ -812,6 +812,7 @@ class Computer(Device):
|
|||
transfer_state.comment = TransferState.__doc__
|
||||
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
|
||||
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
||||
uuid = db.Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
if args:
|
||||
|
|
|
@ -136,6 +136,7 @@ class Computer(Device):
|
|||
owner_id = UUID(data_key='ownerID')
|
||||
transfer_state = EnumField(enums.TransferState, description=m.Computer.transfer_state.comment)
|
||||
receiver_id = UUID(data_key='receiverID')
|
||||
uuid = UUID(required=False)
|
||||
|
||||
|
||||
class Desktop(Computer):
|
||||
|
|
|
@ -8,6 +8,7 @@ import inflection
|
|||
@unique
|
||||
class SnapshotSoftware(Enum):
|
||||
"""The software used to perform the Snapshot."""
|
||||
|
||||
Workbench = 'Workbench'
|
||||
WorkbenchAndroid = 'WorkbenchAndroid'
|
||||
AndroidApp = 'AndroidApp'
|
||||
|
@ -36,6 +37,7 @@ class RatingRange(IntEnum):
|
|||
3. Medium.
|
||||
4. High.
|
||||
"""
|
||||
|
||||
VERY_LOW = 1
|
||||
LOW = 2
|
||||
MEDIUM = 3
|
||||
|
@ -69,6 +71,7 @@ class PriceSoftware(Enum):
|
|||
@unique
|
||||
class AppearanceRange(Enum):
|
||||
"""Grades the imperfections that aesthetically affect the device, but not its usage."""
|
||||
|
||||
Z = 'Z. The device is new'
|
||||
A = 'A. Is like new; without visual damage'
|
||||
B = 'B. Is in really good condition; small visual damage in difficult places to spot'
|
||||
|
@ -83,6 +86,7 @@ class AppearanceRange(Enum):
|
|||
@unique
|
||||
class FunctionalityRange(Enum):
|
||||
"""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'
|
||||
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'
|
||||
|
@ -95,6 +99,7 @@ class FunctionalityRange(Enum):
|
|||
@unique
|
||||
class BatteryHealthRange(Enum):
|
||||
"""Grade the battery health status, depending on self report Android system"""
|
||||
|
||||
A = 'A. The battery health is very good'
|
||||
B = 'B. Battery health is good'
|
||||
C = 'C. Battery health is overheat / over voltage status but can stand the minimum duration'
|
||||
|
@ -109,6 +114,7 @@ class BatteryHealthRange(Enum):
|
|||
@unique
|
||||
class BiosAccessRange(Enum):
|
||||
"""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'
|
||||
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'
|
||||
|
@ -139,6 +145,7 @@ class ImageSoftware(Enum):
|
|||
@unique
|
||||
class ImageMimeTypes(Enum):
|
||||
"""Supported image Mimetypes for Devicehub."""
|
||||
|
||||
jpg = 'image/jpeg'
|
||||
png = 'image/png'
|
||||
|
||||
|
@ -149,6 +156,7 @@ BOX_RATE_3 = 1, 3
|
|||
|
||||
# After looking at own databases
|
||||
|
||||
|
||||
@unique
|
||||
class RamInterface(Enum):
|
||||
"""
|
||||
|
@ -163,6 +171,7 @@ class RamInterface(Enum):
|
|||
here for those cases where there is no more specific information.
|
||||
Please, try to always use DDRø-6 denominations.
|
||||
"""
|
||||
|
||||
SDRAM = 'SDRAM'
|
||||
DDR = 'DDR SDRAM'
|
||||
DDR2 = 'DDR2 SDRAM'
|
||||
|
@ -170,6 +179,7 @@ class RamInterface(Enum):
|
|||
DDR4 = 'DDR4 SDRAM'
|
||||
DDR5 = 'DDR5 SDRAM'
|
||||
DDR6 = 'DDR6 SDRAM'
|
||||
LPDDR3 = 'LPDDR3'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
@ -189,6 +199,7 @@ class DataStorageInterface(Enum):
|
|||
ATA = 'ATA'
|
||||
USB = 'USB'
|
||||
PCI = 'PCI'
|
||||
NVME = 'NVME'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
@ -211,6 +222,7 @@ class DisplayTech(Enum):
|
|||
@unique
|
||||
class ComputerChassis(Enum):
|
||||
"""The chassis of a computer."""
|
||||
|
||||
Tower = 'Tower'
|
||||
Docking = 'Docking'
|
||||
AllInOne = 'All in one'
|
||||
|
@ -235,6 +247,7 @@ class ReceiverRole(Enum):
|
|||
The role that the receiver takes in the reception;
|
||||
the meaning of the reception.
|
||||
"""
|
||||
|
||||
Intermediary = 'Generic user in the workflow of the device.'
|
||||
FinalUser = 'The user that will use the device.'
|
||||
CollectionPoint = 'A collection point.'
|
||||
|
@ -244,6 +257,7 @@ class ReceiverRole(Enum):
|
|||
|
||||
class PrinterTechnology(Enum):
|
||||
"""Technology of the printer."""
|
||||
|
||||
Toner = 'Toner / Laser'
|
||||
Inkjet = 'Liquid inkjet'
|
||||
SolidInk = 'Solid ink'
|
||||
|
@ -260,6 +274,7 @@ class CameraFacing(Enum):
|
|||
@unique
|
||||
class BatteryHealth(Enum):
|
||||
"""The battery health status as in Android."""
|
||||
|
||||
Cold = 'Cold'
|
||||
Dead = 'Dead'
|
||||
Good = 'Good'
|
||||
|
@ -274,6 +289,7 @@ class BatteryTechnology(Enum):
|
|||
https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power
|
||||
adding ``Alkaline``.
|
||||
"""
|
||||
|
||||
LiIon = 'Lithium-ion'
|
||||
NiCd = 'Nickel-Cadmium'
|
||||
NiMH = 'Nickel-metal hydride'
|
||||
|
@ -329,10 +345,11 @@ class PhysicalErasureMethod(Enum):
|
|||
and non able to be re-built.
|
||||
"""
|
||||
|
||||
Shred = 'Reduction of the data-storage to the required certified ' \
|
||||
'standard sizes.'
|
||||
Disintegration = 'Reduction of the data-storage to smaller sizes ' \
|
||||
'than the certified standard ones.'
|
||||
Shred = 'Reduction of the data-storage to the required certified ' 'standard sizes.'
|
||||
Disintegration = (
|
||||
'Reduction of the data-storage to smaller sizes '
|
||||
'than the certified standard ones.'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -362,20 +379,21 @@ class ErasureStandards(Enum):
|
|||
def from_data_storage(cls, erasure) -> Set['ErasureStandards']:
|
||||
"""Returns a set of erasure standards."""
|
||||
from ereuse_devicehub.resources.action import models as actions
|
||||
|
||||
standards = set()
|
||||
if isinstance(erasure, actions.EraseSectors):
|
||||
with suppress(ValueError):
|
||||
first_step, *other_steps = erasure.steps
|
||||
if isinstance(first_step, actions.StepZero) \
|
||||
and all(isinstance(step, actions.StepRandom) for step in other_steps):
|
||||
if isinstance(first_step, actions.StepZero) and all(
|
||||
isinstance(step, actions.StepRandom) for step in other_steps
|
||||
):
|
||||
standards.add(cls.HMG_IS5)
|
||||
return standards
|
||||
|
||||
|
||||
@unique
|
||||
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.
|
||||
|
@ -393,7 +411,7 @@ class TransferState(IntEnum):
|
|||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
@unique
|
||||
class SessionType(IntEnum):
|
||||
|
|
|
@ -5,12 +5,11 @@ from typing import Union
|
|||
from boltons import urlutils
|
||||
from citext import CIText
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import TEXT
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy_utils import LtreeType
|
||||
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 ereuse_devicehub.db import create_view, db, exp, f
|
||||
|
@ -21,70 +20,88 @@ from ereuse_devicehub.resources.user.models import User
|
|||
|
||||
|
||||
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)
|
||||
description = db.Column(CIText())
|
||||
description.comment = """A comment about the lot."""
|
||||
closed = db.Column(db.Boolean, default=False, nullable=False)
|
||||
closed.comment = """A closed lot cannot be modified anymore."""
|
||||
|
||||
devices = db.relationship(Device,
|
||||
backref=db.backref('lots', lazy=True, collection_class=set),
|
||||
secondary=lambda: LotDevice.__table__,
|
||||
lazy=True,
|
||||
collection_class=set)
|
||||
devices = db.relationship(
|
||||
Device,
|
||||
backref=db.backref('lots', lazy=True, collection_class=set),
|
||||
secondary=lambda: LotDevice.__table__,
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
)
|
||||
"""The **children** devices that the lot has.
|
||||
|
||||
Note that the lot can have more devices, if they are inside
|
||||
descendant lots.
|
||||
"""
|
||||
parents = db.relationship(lambda: Lot,
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
secondary=lambda: LotParent.__table__,
|
||||
primaryjoin=lambda: Lot.id == LotParent.child_id,
|
||||
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
|
||||
cascade='refresh-expire', # propagate changes outside ORM
|
||||
backref=db.backref('children',
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
cascade='refresh-expire',
|
||||
collection_class=set)
|
||||
)
|
||||
parents = db.relationship(
|
||||
lambda: Lot,
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
secondary=lambda: LotParent.__table__,
|
||||
primaryjoin=lambda: Lot.id == LotParent.child_id,
|
||||
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
|
||||
cascade='refresh-expire', # propagate changes outside ORM
|
||||
backref=db.backref(
|
||||
'children',
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
cascade='refresh-expire',
|
||||
collection_class=set,
|
||||
),
|
||||
)
|
||||
"""The parent lots."""
|
||||
|
||||
all_devices = db.relationship(Device,
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
secondary=lambda: LotDeviceDescendants.__table__,
|
||||
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
|
||||
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id)
|
||||
all_devices = db.relationship(
|
||||
Device,
|
||||
viewonly=True,
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
secondary=lambda: LotDeviceDescendants.__table__,
|
||||
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
|
||||
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id,
|
||||
)
|
||||
"""All devices, including components, inside this lot and its
|
||||
descendants.
|
||||
"""
|
||||
amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0)
|
||||
owner_id = db.Column(UUID(as_uuid=True),
|
||||
db.ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id)
|
||||
owner_id = db.Column(
|
||||
UUID(as_uuid=True),
|
||||
db.ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id,
|
||||
)
|
||||
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
||||
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__
|
||||
receiver_address = db.Column(CIText(),
|
||||
db.ForeignKey(User.email),
|
||||
nullable=False,
|
||||
default=lambda: g.user.email)
|
||||
receiver_address = db.Column(
|
||||
CIText(),
|
||||
db.ForeignKey(User.email),
|
||||
nullable=False,
|
||||
default=lambda: g.user.email,
|
||||
)
|
||||
receiver = db.relationship(User, primaryjoin=receiver_address == User.email)
|
||||
|
||||
def __init__(self, name: str, closed: bool = closed.default.arg,
|
||||
description: str = None) -> None:
|
||||
def __init__(
|
||||
self, name: str, closed: bool = closed.default.arg, description: str = None
|
||||
) -> None:
|
||||
"""Initializes a lot
|
||||
:param name:
|
||||
: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.
|
||||
|
||||
@property
|
||||
|
@ -102,20 +119,32 @@ class Lot(Thing):
|
|||
|
||||
@property
|
||||
def is_temporary(self):
|
||||
return not bool(self.trade)
|
||||
return not bool(self.trade) and not bool(self.transfer)
|
||||
|
||||
@property
|
||||
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
|
||||
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
|
||||
def descendantsq(cls, 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
|
||||
def roots(cls):
|
||||
|
@ -176,13 +205,17 @@ class Lot(Thing):
|
|||
if isinstance(child, Lot):
|
||||
return Path.has_lot(self.id, child.id)
|
||||
elif isinstance(child, Device):
|
||||
device = db.session.query(LotDeviceDescendants) \
|
||||
.filter(LotDeviceDescendants.device_id == child.id) \
|
||||
.filter(LotDeviceDescendants.ancestor_lot_id == self.id) \
|
||||
device = (
|
||||
db.session.query(LotDeviceDescendants)
|
||||
.filter(LotDeviceDescendants.device_id == child.id)
|
||||
.filter(LotDeviceDescendants.ancestor_lot_id == self.id)
|
||||
.one_or_none()
|
||||
)
|
||||
return device
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
author_id = db.Column(UUID(as_uuid=True),
|
||||
db.ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id)
|
||||
author_id = db.Column(
|
||||
UUID(as_uuid=True),
|
||||
db.ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id,
|
||||
)
|
||||
author = db.relationship(User, primaryjoin=author_id == User.id)
|
||||
author_id.comment = """The user that put the device in the lot."""
|
||||
|
||||
|
||||
class Path(db.Model):
|
||||
id = db.Column(db.UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
server_default=db.text('gen_random_uuid()'))
|
||||
id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
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 = db.relationship(Lot,
|
||||
backref=db.backref('paths',
|
||||
lazy=True,
|
||||
collection_class=set,
|
||||
cascade=CASCADE_OWN),
|
||||
primaryjoin=Lot.id == lot_id)
|
||||
lot = db.relationship(
|
||||
Lot,
|
||||
backref=db.backref(
|
||||
'paths', lazy=True, collection_class=set, cascade=CASCADE_OWN
|
||||
),
|
||||
primaryjoin=Lot.id == lot_id,
|
||||
)
|
||||
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."""
|
||||
|
||||
__table_args__ = (
|
||||
# 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_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:
|
||||
|
@ -243,7 +285,9 @@ class Path(db.Model):
|
|||
child_id = UUIDLtree.convert(child_id)
|
||||
return bool(
|
||||
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()
|
||||
)
|
||||
|
||||
|
@ -263,47 +307,73 @@ class LotDeviceDescendants(db.Model):
|
|||
"""Ancestor lot table."""
|
||||
_desc = Lot.__table__.alias()
|
||||
"""Descendant lot table."""
|
||||
lot_device = _desc \
|
||||
.join(LotDevice, _desc.c.id == LotDevice.lot_id) \
|
||||
.join(Path, _desc.c.id == Path.lot_id)
|
||||
lot_device = _desc.join(LotDevice, _desc.c.id == LotDevice.lot_id).join(
|
||||
Path, _desc.c.id == Path.lot_id
|
||||
)
|
||||
"""Join: Path -- Lot -- LotDevice"""
|
||||
|
||||
descendants = "path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') " \
|
||||
"|| '.*' AS LQUERY))".format(_ancestor.name)
|
||||
descendants = (
|
||||
"path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') "
|
||||
"|| '.*' AS LQUERY))".format(_ancestor.name)
|
||||
)
|
||||
"""Query that gets the descendants of the ancestor lot."""
|
||||
devices = db.select([
|
||||
LotDevice.device_id,
|
||||
_desc.c.id.label('parent_lot_id'),
|
||||
_ancestor.c.id.label('ancestor_lot_id'),
|
||||
None
|
||||
]).select_from(_ancestor).select_from(lot_device).where(db.text(descendants))
|
||||
devices = (
|
||||
db.select(
|
||||
[
|
||||
LotDevice.device_id,
|
||||
_desc.c.id.label('parent_lot_id'),
|
||||
_ancestor.c.id.label('ancestor_lot_id'),
|
||||
None,
|
||||
]
|
||||
)
|
||||
.select_from(_ancestor)
|
||||
.select_from(lot_device)
|
||||
.where(db.text(descendants))
|
||||
)
|
||||
|
||||
# Components
|
||||
_parent_device = Device.__table__.alias(name='parent_device')
|
||||
"""The device that has the access to the lot."""
|
||||
lot_device_component = lot_device \
|
||||
.join(_parent_device, _parent_device.c.id == LotDevice.device_id) \
|
||||
.join(Component, _parent_device.c.id == Component.parent_id)
|
||||
lot_device_component = lot_device.join(
|
||||
_parent_device, _parent_device.c.id == LotDevice.device_id
|
||||
).join(Component, _parent_device.c.id == Component.parent_id)
|
||||
"""Join: Path -- Lot -- LotDevice -- ParentDevice (Device) -- Component"""
|
||||
|
||||
components = db.select([
|
||||
Component.id.label('device_id'),
|
||||
_desc.c.id.label('parent_lot_id'),
|
||||
_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))
|
||||
components = (
|
||||
db.select(
|
||||
[
|
||||
Component.id.label('device_id'),
|
||||
_desc.c.id.label('parent_lot_id'),
|
||||
_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))
|
||||
|
||||
|
||||
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(
|
||||
'lot_parent',
|
||||
db.select([
|
||||
Path.lot_id.label('child_id'),
|
||||
exp.cast(f.replace(exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'),
|
||||
UUID).label('parent_id')
|
||||
]).select_from(Path).where(i > 0),
|
||||
db.select(
|
||||
[
|
||||
Path.lot_id.label('child_id'),
|
||||
exp.cast(
|
||||
f.replace(
|
||||
exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'
|
||||
),
|
||||
UUID,
|
||||
).label('parent_id'),
|
||||
]
|
||||
)
|
||||
.select_from(Path)
|
||||
.where(i > 0),
|
||||
)
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import uuid
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Set, Union
|
||||
|
||||
import marshmallow as ma
|
||||
from flask import Response, jsonify, request, g
|
||||
from marshmallow import Schema as MarshmallowSchema, fields as f
|
||||
from flask import Response, g, jsonify, request
|
||||
from marshmallow import Schema as MarshmallowSchema
|
||||
from marshmallow import fields as f
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from teal.marshmallow import EnumField
|
||||
from teal.resource import View
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.inventory.models import Transfer
|
||||
from ereuse_devicehub.query import things_response
|
||||
from ereuse_devicehub.resources.device.models import Device, Computer
|
||||
from ereuse_devicehub.resources.action.models import Trade, Confirm, Revoke
|
||||
from ereuse_devicehub.resources.action.models import Confirm, Revoke, Trade
|
||||
from ereuse_devicehub.resources.device.models import Computer, Device
|
||||
from ereuse_devicehub.resources.lot.models import Lot, Path
|
||||
|
||||
|
||||
|
@ -27,6 +29,7 @@ class LotView(View):
|
|||
"""Allowed arguments for the ``find``
|
||||
method (GET collection) endpoint
|
||||
"""
|
||||
|
||||
format = EnumField(LotFormat, missing=None)
|
||||
search = f.Str(missing=None)
|
||||
type = f.Str(missing=None)
|
||||
|
@ -42,12 +45,26 @@ class LotView(View):
|
|||
return ret
|
||||
|
||||
def patch(self, id):
|
||||
patch_schema = self.resource_def.SCHEMA(only=(
|
||||
'name', 'description', 'transfer_state', 'receiver_address', 'amount', 'devices',
|
||||
'owner_address'), partial=True)
|
||||
patch_schema = self.resource_def.SCHEMA(
|
||||
only=(
|
||||
'name',
|
||||
'description',
|
||||
'transfer_state',
|
||||
'receiver_address',
|
||||
'amount',
|
||||
'devices',
|
||||
'owner_address',
|
||||
),
|
||||
partial=True,
|
||||
)
|
||||
l = request.get_json(schema=patch_schema)
|
||||
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)]
|
||||
for key, value in l.items():
|
||||
setattr(lot, key, value)
|
||||
|
@ -84,7 +101,7 @@ class LotView(View):
|
|||
ret = {
|
||||
'items': {l['id']: l for l in lots},
|
||||
'tree': self.ui_tree(),
|
||||
'url': request.path
|
||||
'url': request.path,
|
||||
}
|
||||
else:
|
||||
query = Lot.query
|
||||
|
@ -95,15 +112,28 @@ class LotView(View):
|
|||
lots = query.paginate(per_page=6 if args['search'] else query.count())
|
||||
return things_response(
|
||||
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)
|
||||
|
||||
def visibility_filter(self, query):
|
||||
query = query.outerjoin(Trade) \
|
||||
.filter(or_(Trade.user_from == g.user,
|
||||
Trade.user_to == g.user,
|
||||
Lot.owner_id == g.user.id))
|
||||
query = (
|
||||
query.outerjoin(Trade)
|
||||
.outerjoin(Transfer)
|
||||
.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
|
||||
|
||||
def type_filter(self, query, args):
|
||||
|
@ -111,13 +141,23 @@ class LotView(View):
|
|||
|
||||
# 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":
|
||||
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":
|
||||
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
|
||||
|
||||
|
@ -152,10 +192,7 @@ class LotView(View):
|
|||
# does lot_id exist already in node?
|
||||
node = next(part for part in nodes if lot_id == part['id'])
|
||||
except StopIteration:
|
||||
node = {
|
||||
'id': lot_id,
|
||||
'nodes': []
|
||||
}
|
||||
node = {'id': lot_id, 'nodes': []}
|
||||
nodes.append(node)
|
||||
if path:
|
||||
cls._p(node['nodes'], path)
|
||||
|
@ -175,15 +212,17 @@ class LotView(View):
|
|||
|
||||
class LotBaseChildrenView(View):
|
||||
"""Base class for adding / removing children devices and
|
||||
lots from a lot.
|
||||
"""
|
||||
lots from a lot.
|
||||
"""
|
||||
|
||||
def __init__(self, definition: 'Resource', **kw) -> None:
|
||||
super().__init__(definition, **kw)
|
||||
self.list_args = self.ListArgs()
|
||||
|
||||
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'])
|
||||
|
||||
def get_lot(self, id: uuid.UUID) -> Lot:
|
||||
|
@ -247,8 +286,9 @@ class LotDeviceView(LotBaseChildrenView):
|
|||
if not ids:
|
||||
return
|
||||
|
||||
devices = set(Device.query.filter(Device.id.in_(ids)).filter(
|
||||
Device.owner == g.user))
|
||||
devices = set(
|
||||
Device.query.filter(Device.id.in_(ids)).filter(Device.owner == g.user)
|
||||
)
|
||||
|
||||
lot.devices.update(devices)
|
||||
|
||||
|
@ -271,8 +311,9 @@ class LotDeviceView(LotBaseChildrenView):
|
|||
txt = 'This is not your lot'
|
||||
raise ma.ValidationError(txt)
|
||||
|
||||
devices = set(Device.query.filter(Device.id.in_(ids)).filter(
|
||||
Device.owner_id == g.user.id))
|
||||
devices = set(
|
||||
Device.query.filter(Device.id.in_(ids)).filter(Device.owner_id == g.user.id)
|
||||
)
|
||||
|
||||
lot.devices.difference_update(devices)
|
||||
|
||||
|
@ -311,9 +352,7 @@ def delete_from_trade(lot: Lot, devices: List):
|
|||
phantom = lot.trade.user_from
|
||||
|
||||
phantom_revoke = Revoke(
|
||||
action=lot.trade,
|
||||
user=phantom,
|
||||
devices=set(without_confirms)
|
||||
action=lot.trade, user=phantom, devices=set(without_confirms)
|
||||
)
|
||||
db.session.add(phantom_revoke)
|
||||
|
||||
|
|
|
@ -116,6 +116,15 @@
|
|||
</a>
|
||||
</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-item">
|
||||
|
|
|
@ -37,10 +37,19 @@
|
|||
<!-- Bordered Tabs -->
|
||||
|
||||
<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 -->
|
||||
{% 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 --> #}
|
||||
<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>
|
||||
</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>
|
||||
{% endif %}
|
||||
<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">
|
||||
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
|
||||
<div class="btn-group dropdown ml-1">
|
||||
|
@ -438,6 +476,103 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
|
||||
</div><!-- End Bordered Tabs -->
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -1,4 +1,4 @@
|
|||
[settings]
|
||||
|
||||
TOKEN = {{ token }}
|
||||
URL = {{ url }}
|
||||
DH_TOKEN = {{ token }}
|
||||
DH_URL = {{ url }}
|
||||
|
|
|
@ -49,7 +49,8 @@ class LogoutView(View):
|
|||
return flask.redirect(flask.url_for('core.login'))
|
||||
|
||||
|
||||
class GenericMixView(View):
|
||||
class GenericMixin(View):
|
||||
methods = ['GET']
|
||||
decorators = [login_required]
|
||||
|
||||
def get_lots(self):
|
||||
|
@ -74,7 +75,7 @@ class GenericMixView(View):
|
|||
return self.context
|
||||
|
||||
|
||||
class UserProfileView(GenericMixView):
|
||||
class UserProfileView(GenericMixin):
|
||||
decorators = [login_required]
|
||||
template_name = 'ereuse_devicehub/user_profile.html'
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ from ereuse_devicehub import auth
|
|||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.enums import SessionType
|
||||
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')
|
||||
|
||||
|
||||
class SettingsView(GenericMixView):
|
||||
class SettingsView(GenericMixin):
|
||||
decorators = [login_required]
|
||||
template_name = 'workbench/settings.html'
|
||||
page_title = "Workbench Settings"
|
||||
|
|
|
@ -5,6 +5,7 @@ Use this as a starting point.
|
|||
"""
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from ereuse_devicehub.api.views import api
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.inventory.views import devices
|
||||
|
@ -16,6 +17,7 @@ app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
|
|||
app.register_blueprint(core)
|
||||
app.register_blueprint(devices)
|
||||
app.register_blueprint(labels)
|
||||
app.register_blueprint(api)
|
||||
app.register_blueprint(workbench)
|
||||
|
||||
# configure & enable CSRF of Flask-WTF
|
||||
|
|
|
@ -33,7 +33,7 @@ SQLAlchemy==1.3.24
|
|||
SQLAlchemy-Utils==0.33.11
|
||||
teal==0.2.0a38
|
||||
webargs==5.5.3
|
||||
Werkzeug==0.15.3
|
||||
Werkzeug==0.15.5
|
||||
sqlalchemy-citext==1.3.post0
|
||||
flask-weasyprint==0.5
|
||||
weasyprint==44
|
||||
|
@ -43,3 +43,5 @@ tqdm==4.32.2
|
|||
python-decouple==3.3
|
||||
python-dotenv==0.14.0
|
||||
pyjwt==2.4.0
|
||||
pint==0.9
|
||||
py-dmidecode==0.1.0
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import json
|
||||
import uuid
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime
|
||||
|
@ -13,6 +14,7 @@ from decouple import config
|
|||
from psycopg2 import IntegrityError
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
|
||||
from ereuse_devicehub.api.views import api
|
||||
from ereuse_devicehub.client import Client, UserClient, UserClientFlask
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.db import db
|
||||
|
@ -59,6 +61,7 @@ def _app(config: TestConfig) -> Devicehub:
|
|||
app.register_blueprint(core)
|
||||
app.register_blueprint(devices)
|
||||
app.register_blueprint(labels)
|
||||
app.register_blueprint(api)
|
||||
app.register_blueprint(workbench)
|
||||
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||
app.config['PROFILE'] = True
|
||||
|
@ -208,6 +211,11 @@ def file(name: str) -> dict:
|
|||
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:
|
||||
"""Opens and parses a YAML file from the ``files`` subdir."""
|
||||
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
|
@ -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
|
||||
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
|
||||
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 19:08:44 2022;;;;;p1ml;0;0
|
||||
|
|
|
File diff suppressed because one or more lines are too long
|
@ -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
|
@ -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
|
@ -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"}
|
|
@ -32,6 +32,7 @@ def test_api_docs(client: Client):
|
|||
'/actions/',
|
||||
'/allocates/',
|
||||
'/apidocs',
|
||||
'/api/inventory/',
|
||||
'/deallocates/',
|
||||
'/deliverynotes/',
|
||||
'/devices/',
|
||||
|
@ -60,8 +61,14 @@ def test_api_docs(client: Client):
|
|||
'/inventory/lot/{id}/del/',
|
||||
'/inventory/lot/{lot_id}/device/',
|
||||
'/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}/transfer/{type_id}/',
|
||||
'/inventory/lot/{lot_id}/transfer/',
|
||||
'/inventory/lot/{lot_id}/upload-snapshot/',
|
||||
'/inventory/snapshots/{snapshot_uuid}/',
|
||||
'/inventory/snapshots/',
|
||||
'/inventory/tag/devices/add/',
|
||||
'/inventory/tag/devices/{id}/del/',
|
||||
'/inventory/upload-snapshot/',
|
||||
|
|
|
@ -130,6 +130,7 @@ def test_physical_properties():
|
|||
'model': 'foo',
|
||||
'receiver_id': None,
|
||||
'serial_number': 'foo-bar',
|
||||
'uuid': None,
|
||||
'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)
|
||||
pc, res = user.get(res=d.Device, item=s['device']['devicehubID'])
|
||||
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)
|
||||
assert 'intel atom cpu n270 @ 1.60ghz' in html
|
||||
|
|
|
@ -181,7 +181,7 @@ def test_device_query(user: UserClient):
|
|||
assert i['url'] == '/devices/'
|
||||
assert i['items'][0]['url'] == '/devices/%s' % snapshot['device']['devicehubID']
|
||||
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 pc['tags'][0]['id'] == pc['devicehubID']
|
||||
|
||||
|
|
|
@ -410,6 +410,7 @@ def test_export_computer_monitor(user: UserClient):
|
|||
f = StringIO(csv_str)
|
||||
obj_csv = csv.reader(f, f)
|
||||
export_csv = list(obj_csv)
|
||||
|
||||
# Open fixture csv and transform to list
|
||||
with Path(__file__).parent.joinpath('files').joinpath(
|
||||
'computer-monitor.csv'
|
||||
|
@ -501,6 +502,7 @@ def test_report_devices_stock_control(user: UserClient, user2: UserClient):
|
|||
accept='text/csv',
|
||||
query=[('filter', {'type': ['Computer']})],
|
||||
)
|
||||
|
||||
f = StringIO(csv_str)
|
||||
obj_csv = csv.reader(f, f)
|
||||
export_csv = list(obj_csv)
|
||||
|
|
|
@ -174,7 +174,7 @@ def test_upload_snapshot(user3: UserClientFlask):
|
|||
assert str(db_snapthot.uuid) == snapshot['uuid']
|
||||
assert dev.type == 'Laptop'
|
||||
assert dev.serial_number == 'b8oaas048285'
|
||||
assert len(dev.actions) == 12
|
||||
assert len(dev.actions) == 10
|
||||
assert len(dev.components) == 9
|
||||
|
||||
|
||||
|
@ -495,7 +495,7 @@ def test_action_recycling(user3: UserClientFlask):
|
|||
|
||||
uri = '/inventory/action/add/'
|
||||
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
|
||||
|
||||
# good request
|
||||
|
@ -815,7 +815,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
|
|||
|
||||
user3.post(uri, data=data)
|
||||
assert dev.allocated_status.type == 'Allocate'
|
||||
assert len(dev.actions) == 13
|
||||
assert len(dev.actions) == 11
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -829,7 +829,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
|
|||
body, status = user3.post(uri, data=data)
|
||||
assert status == '200 OK'
|
||||
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
|
||||
data = {
|
||||
|
@ -858,7 +858,7 @@ def test_action_allocate_deallocate_error(user3: UserClientFlask):
|
|||
}
|
||||
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 14
|
||||
assert len(dev.actions) == 12
|
||||
|
||||
|
||||
@pytest.mark.mvp
|
||||
|
@ -881,7 +881,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
uri = '/inventory/action/allocate/add/'
|
||||
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 13
|
||||
assert len(dev.actions) == 11
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -893,7 +893,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
}
|
||||
body, status = user3.post(uri, data=data)
|
||||
assert status == '200 OK'
|
||||
assert len(dev.actions) == 14
|
||||
assert len(dev.actions) == 12
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -907,7 +907,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
uri = '/inventory/action/allocate/add/'
|
||||
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 15
|
||||
assert len(dev.actions) == 13
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -918,7 +918,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
'end_users': 2,
|
||||
}
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 16
|
||||
assert len(dev.actions) == 14
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -929,7 +929,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
'end_users': 2,
|
||||
}
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 17
|
||||
assert len(dev.actions) == 15
|
||||
|
||||
data = {
|
||||
'csrf_token': generate_csrf(),
|
||||
|
@ -940,7 +940,7 @@ def test_action_allocate_deallocate_error2(user3: UserClientFlask):
|
|||
'end_users': 2,
|
||||
}
|
||||
user3.post(uri, data=data)
|
||||
assert len(dev.actions) == 18
|
||||
assert len(dev.actions) == 16
|
||||
|
||||
|
||||
@pytest.mark.mvp
|
||||
|
@ -1084,3 +1084,228 @@ def test_wb_settings_register(user3: UserClientFlask):
|
|||
assert "TOKEN = " in body
|
||||
assert "URL = https://" 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
|
@ -41,7 +41,6 @@ def test_workbench_server_condensed(user: UserClient):
|
|||
('BenchmarkProcessorSysbench', cpu_id),
|
||||
('StressTest', pc_id),
|
||||
('EraseSectors', ssd_id),
|
||||
('EreusePrice', pc_id),
|
||||
('BenchmarkRamSysbench', pc_id),
|
||||
('BenchmarkProcessor', cpu_id),
|
||||
('Install', ssd_id),
|
||||
|
@ -49,7 +48,6 @@ def test_workbench_server_condensed(user: UserClient):
|
|||
('BenchmarkDataStorage', ssd_id),
|
||||
('BenchmarkDataStorage', hdd_id),
|
||||
('TestDataStorage', ssd_id),
|
||||
('RateComputer', pc_id)
|
||||
}
|
||||
assert snapshot['closed']
|
||||
assert snapshot['severity'] == 'Info'
|
||||
|
@ -61,10 +59,6 @@ def test_workbench_server_condensed(user: UserClient):
|
|||
assert device['networkSpeeds'] == [1000, 58]
|
||||
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['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?
|
||||
assert any([ac['type'] in [BenchmarkProcessor.t, BenchmarkRamSysbench.t] for ac in device['actions']])
|
||||
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['chassis'] == 'Tower'
|
||||
assert set(e['type'] for e in snapshot['actions']) == {
|
||||
'EreusePrice',
|
||||
'RateComputer',
|
||||
'BenchmarkDataStorage',
|
||||
'BenchmarkProcessor',
|
||||
'BenchmarkProcessorSysbench',
|
||||
|
@ -156,7 +148,8 @@ def test_real_hp_11(user: UserClient):
|
|||
'TestBios',
|
||||
'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['processorModel'] == 'intel core i3 cpu 530 @ 2.93ghz'
|
||||
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,
|
||||
actions and their relationships of a real pc.
|
||||
"""
|
||||
# import pdb; pdb.set_trace()
|
||||
s = file('real-eee-1001pxd.snapshot.11')
|
||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||
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 len(pc['tags']) == 1
|
||||
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]['functionalityRange'] == 'B'
|
||||
# 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']
|
||||
wifi = components[0]
|
||||
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.StressTest.t in action_types
|
||||
assert em.Snapshot.t in action_types
|
||||
assert len(actions) == 8
|
||||
assert len(actions) == 6
|
||||
gpu = components[3]
|
||||
assert gpu['model'] == 'atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller'
|
||||
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.StressTest.t in action_types
|
||||
assert em.Snapshot.t in action_types
|
||||
assert len(action_types) == 6
|
||||
assert len(action_types) == 4
|
||||
sound = components[4]
|
||||
assert sound['model'] == 'nm10/ich7 family high definition audio controller'
|
||||
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.EraseBasic.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)
|
||||
assert erase['endTime']
|
||||
assert erase['startTime']
|
||||
|
|
Reference in New Issue