Merge branch 'feature/server-side-render-parser-3021' into feature/list-snapshots-view-#3113

This commit is contained in:
Cayo Puigdefabregas 2022-05-17 09:23:31 +02:00
commit bf547b6f80
48 changed files with 9010 additions and 1033 deletions

15
.babelrc.json Normal file
View File

@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
}
}
]
]
}

View File

@ -1,4 +1,5 @@
ereuse_devicehub/static/vendor
ereuse_devicehub/static/js/print.pdf.js
ereuse_devicehub/static/js/qrcode.js
*.build.js
*.min.js

View File

@ -9,6 +9,11 @@ ml).
## testing
## [2.1.1] - 2022-05-11
Hot fix release.
- [fixed] #256 JS support to old browsers using babel.
- [fixed] #266 Fix error when trade.document.url is None on device_list.html
## [2.1.0] - 2022-05-11
- [added] #219 Add functionality to searchbar (Lots and devices).
- [added] #222 Allow user to update its password.

View File

@ -2,6 +2,15 @@
## Writing code
### Javascript and compatibility with "old" browsers
**Warning:** This project is using babel compiler... You need run an additional build step to make build js file
```bash
npm install
npm run babel
```
NOTE: If you prefer you can use yarn instead, it's compatible
NOTE2: This only affect to file `ereuse_devicehub/static/js/main_inventory.js`.
### Coding style
#### Python style

View File

@ -1 +1 @@
__version__ = "2.2.0.alpha0"
__version__ = "2.1.2.alpha0"

View File

@ -15,7 +15,7 @@ from ereuse_devicehub.parser.models import SnapshotErrors
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite
from ereuse_devicehub.resources.action.views.snapshot import (
SnapshotMix,
SnapshotMixin,
move_json,
save_json,
)
@ -24,7 +24,7 @@ from ereuse_devicehub.resources.enums import Severity
api = Blueprint('api', __name__, url_prefix='/api')
class LoginMix(View):
class LoginMixin(View):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.authenticate()
@ -44,7 +44,7 @@ class LoginMix(View):
g.user = self.user
class InventoryView(LoginMix, SnapshotMix):
class InventoryView(LoginMixin, SnapshotMixin):
methods = ['POST']
def dispatch_request(self):

View File

@ -63,7 +63,7 @@ class DevicehubConfig(Config):
"""The minimum version of ereuse.org workbench that this devicehub
accepts. we recommend not changing this value.
"""
WORKBENCH_LITE = ["1.0.0"]
SCHEMA_WORKBENCH = ["1.0.0"]
TMP_SNAPSHOTS = config('TMP_SNAPSHOTS', '/tmp/snapshots')
TMP_LIVES = config('TMP_LIVES', '/tmp/lives')

View File

@ -33,17 +33,17 @@ 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 (
SnapshotMix,
SnapshotMixin,
move_json,
save_json,
)
from ereuse_devicehub.resources.device.models import (
SAI,
Cellphone,
ComputerMonitor,
Device,
Keyboard,
MemoryCardReader,
Monitor,
Mouse,
Smartphone,
Tablet,
@ -58,7 +58,7 @@ from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user.models import User
DEVICES = {
"All": ["All Devices", "All Components"],
"All": ["All Devices"],
"Computer": [
"All Computers",
"Desktop",
@ -79,55 +79,12 @@ DEVICES = {
"Smartphone",
"Cellphone",
],
"DataStorage": ["All DataStorage", "HardDrive", "SolidStateDrive"],
"Accessories & Peripherals": [
"All Peripherals",
"GraphicCard",
"Motherboard",
"NetworkAdapter",
"Processor",
"RamModule",
"SoundCard",
"Battery",
"Keyboard",
"Mouse",
"MemoryCardReader",
],
}
COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
COMPONENTS = [
'GraphicCard',
'DataStorage',
'HardDrive',
'DataStorage',
'SolidStateDrive',
'Motherboard',
'NetworkAdapter',
'Processor',
'RamModule',
'SoundCard',
'Display',
'Battery',
'Camera',
]
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"]
DATASTORAGE = ["HardDrive", "SolidStateDrive"]
PERIPHERALS = [
"GraphicCard",
"Motherboard",
"NetworkAdapter",
"Processor",
"RamModule",
"SoundCard",
"Battery",
"Keyboard",
"Mouse",
"MemoryCardReader",
]
class FilterForm(FlaskForm):
@ -139,6 +96,7 @@ class FilterForm(FlaskForm):
super().__init__(*args, **kwargs)
self.lots = lots
self.lot_id = lot_id
self.only_unassigned = kwargs.pop('only_unassigned', True)
self._get_types()
def _get_types(self):
@ -154,9 +112,9 @@ class FilterForm(FlaskForm):
device_ids = (d.id for d in self.lot.devices)
self.devices = Device.query.filter(Device.id.in_(device_ids))
else:
self.devices = Device.query.filter(Device.owner_id == g.user.id).filter_by(
lots=None
)
self.devices = Device.query.filter(Device.owner_id == g.user.id)
if self.only_unassigned:
self.devices = self.devices.filter_by(lots=None)
def search(self):
self.filter_from_lots()
@ -169,10 +127,7 @@ class FilterForm(FlaskForm):
# Generic Filters
if "All Devices" == self.device_type:
filter_type = COMPUTERS + ["Monitor"] + MOBILE
elif "All Components" == self.device_type:
filter_type = COMPONENTS
filter_type = COMPUTERS + MONITORS + MOBILE
elif "All Computers" == self.device_type:
filter_type = COMPUTERS
@ -183,12 +138,6 @@ class FilterForm(FlaskForm):
elif "All Mobile" == self.device_type:
filter_type = MOBILE
elif "All DataStorage" == self.device_type:
filter_type = DATASTORAGE
elif "All Peripherals" == self.device_type:
filter_type = PERIPHERALS
if filter_type:
self.devices = self.devices.filter(Device.type.in_(filter_type))
@ -233,7 +182,7 @@ class LotForm(FlaskForm):
return self.instance
class UploadSnapshotForm(FlaskForm, SnapshotMix):
class UploadSnapshotForm(SnapshotMixin, FlaskForm):
snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()])
def validate(self, extra_validators=None):
@ -275,7 +224,7 @@ class UploadSnapshotForm(FlaskForm, SnapshotMix):
def is_wb_lite_snapshot(self, version: str) -> bool:
is_lite = False
if version in app.config['WORKBENCH_LITE']:
if version in app.config['SCHEMA_WORKBENCH']:
is_lite = True
return is_lite
@ -356,7 +305,7 @@ class NewDeviceForm(FlaskForm):
"Smartphone": Smartphone,
"Tablet": Tablet,
"Cellphone": Cellphone,
"Monitor": Monitor,
"ComputerMonitor": ComputerMonitor,
"Mouse": Mouse,
"Keyboard": Keyboard,
"SAI": SAI,
@ -472,7 +421,7 @@ class NewDeviceForm(FlaskForm):
path_snapshot = save_json(json_snapshot, self.tmp_snapshots, g.user.email)
snapshot_json = schema.load(json_snapshot)
if self.type.data == 'Monitor':
if self.type.data == 'ComputerMonitor':
snapshot_json['device'].resolution_width = self.resolution.data
snapshot_json['device'].size = self.screen.data
@ -483,7 +432,7 @@ class NewDeviceForm(FlaskForm):
snapshot = upload_form.build(snapshot_json)
move_json(self.tmp_snapshots, path_snapshot, g.user.email)
if self.type.data == 'Monitor':
if self.type.data == 'ComputerMonitor':
snapshot.device.resolution = self.resolution.data
snapshot.device.screen = self.screen.data
@ -560,7 +509,7 @@ class TagDeviceForm(FlaskForm):
db.session.commit()
class ActionFormMix(FlaskForm):
class ActionFormMixin(FlaskForm):
name = StringField(
'Name',
[validators.length(max=50)],
@ -641,7 +590,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)
@ -654,7 +603,8 @@ 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()])
final_user_code = StringField(
@ -663,7 +613,7 @@ class AllocateForm(ActionFormMix):
transaction = StringField(
'Transaction', [validators.Optional(), validators.length(max=50)]
)
end_users = IntegerField('End users', [validators.Optional()])
end_users = IntegerField('Number of end users', [validators.Optional()])
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
@ -773,7 +723,7 @@ class DataWipeDocumentForm(Form):
return self._obj
class DataWipeForm(ActionFormMix):
class DataWipeForm(ActionFormMixin):
document = FormField(DataWipeDocumentForm)
def save(self):
@ -800,7 +750,7 @@ class DataWipeForm(ActionFormMix):
return self.instance
class TradeForm(ActionFormMix):
class TradeForm(ActionFormMixin):
user_from = StringField(
'Supplier',
[validators.Optional()],

View File

@ -1,5 +1,6 @@
import csv
import logging
from distutils.util import strtobool
from io import StringIO
import flask
@ -31,74 +32,78 @@ 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):
def get_context(self, lot_id, only_unassigned=True):
super().get_context()
lots = self.context['lots']
form_filter = FilterForm(lots, lot_id)
form_filter = FilterForm(lots, lot_id, only_unassigned=only_unassigned)
devices = form_filter.search()
lot = None
tags = (
Tag.query.filter(Tag.owner_id == current_user.id)
.filter(Tag.device_id.is_(None))
.order_by(Tag.id.asc())
)
if lot_id:
lot = lots.filter(Lot.id == lot_id).one()
form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(lot=lot.id)
form_new_trade = TradeForm(
lot=lot.id,
user_to=g.user.email,
user_from=g.user.email,
)
else:
form_new_action = NewActionForm()
form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm()
form_new_trade = ''
action_devices = form_new_action.devices.data
list_devices = []
if action_devices:
list_devices.extend([int(x) for x in action_devices.split(",")])
form_new_action = NewActionForm(lot=lot_id)
self.context.update(
{
'devices': devices,
'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'form_new_allocate': AllocateForm(lot=lot_id),
'form_new_datawipe': DataWipeForm(lot=lot_id),
'form_new_trade': form_new_trade,
'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(),
'lot': lot,
'tags': tags,
'list_devices': list_devices,
'tags': self.get_user_tags(),
'list_devices': self.get_selected_devices(form_new_action),
'unassigned_devices': only_unassigned,
}
)
return self.context
def get_user_tags(self):
return (
Tag.query.filter(Tag.owner_id == current_user.id)
.filter(Tag.device_id.is_(None))
.order_by(Tag.id.asc())
)
class DeviceListView(DeviceListMix):
def get_selected_devices(self, action_form):
"""Retrieve selected devices (when action form is submited)"""
action_devices = action_form.devices.data
if action_devices:
return [int(x) for x in action_devices.split(",")]
return []
class DeviceListView(DeviceListMixin):
def dispatch_request(self, lot_id=None):
self.get_context(lot_id)
only_unassigned = request.args.get(
'only_unassigned', default=True, type=strtobool
)
self.get_context(lot_id, only_unassigned)
return flask.render_template(self.template_name, **self.context)
class DeviceDetailView(GenericMixView):
class DeviceDetailView(GenericMixin):
decorators = [login_required]
template_name = 'inventory/device_detail.html'
@ -119,7 +124,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'
@ -142,7 +147,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'
@ -183,7 +188,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'
@ -211,7 +216,7 @@ class UploadSnapshotView(GenericMixView):
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'
@ -256,7 +261,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'
@ -309,7 +314,7 @@ class NewActionView(View):
return url_for('inventory.devicelist')
class NewAllocateView(NewActionView, DeviceListMix):
class NewAllocateView(DeviceListMixin, NewActionView):
methods = ['POST']
form_class = AllocateForm
@ -328,12 +333,13 @@ class NewAllocateView(NewActionView, DeviceListMix):
messages.error('Action {} error!'.format(self.form.type.data))
for k, v in self.form.errors.items():
value = ';'.join(v)
messages.error('Action Error {key}: {value}!'.format(key=k, value=value))
key = self.form[k].label.text
messages.error('Action Error {key}: {value}!'.format(key=key, value=value))
next_url = self.get_next_url()
return flask.redirect(next_url)
class NewDataWipeView(NewActionView, DeviceListMix):
class NewDataWipeView(DeviceListMixin, NewActionView):
methods = ['POST']
form_class = DataWipeForm
@ -354,7 +360,7 @@ class NewDataWipeView(NewActionView, DeviceListMix):
return flask.redirect(next_url)
class NewTradeView(NewActionView, DeviceListMix):
class NewTradeView(DeviceListMixin, NewActionView):
methods = ['POST']
form_class = TradeForm

View File

@ -23,7 +23,9 @@ class TagListView(View):
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(Tag.id)
tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(
Tag.created.desc()
)
context = {
'lots': lots,
'tags': tags,

View File

@ -26,11 +26,11 @@ class Snapshot_lite(Thing):
@validates_schema
def validate_workbench_version(self, data: dict):
if data['schema_api'] not in app.config['WORKBENCH_LITE']:
if data['schema_api'] not in app.config['SCHEMA_WORKBENCH']:
raise ValidationError(
'Min. supported Workbench version is '
'{} but yours is {}.'.format(
app.config['WORKBENCH_LITE'][0], data['version']
app.config['SCHEMA_WORKBENCH'][0], data['version']
),
field_names=['version'],
)

View File

@ -62,7 +62,7 @@ def move_json(tmp_snapshots, path_name, user, live=False):
os.remove(path_name)
class SnapshotMix:
class SnapshotMixin:
sync = Sync()
def build(self, snapshot_json=None): # noqa: C901
@ -119,7 +119,7 @@ class SnapshotMix:
return snapshot
class SnapshotView(SnapshotMix):
class SnapshotView(SnapshotMixin):
"""Performs a Snapshot.
See `Snapshot` section in docs for more info.

View File

@ -332,6 +332,35 @@ class Device(Thing):
with suppress(LookupError, ValueError):
return self.last_action_of(*states.Trading.actions())
@property
def allocated_status(self):
"""Show the actual status of device.
The status depend of one of this 3 actions:
- Allocate
- Deallocate
- InUse (Live register)
"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
return self.last_action_of(*states.Usage.actions())
@property
def physical_status(self):
"""Show the actual status of device for this owner.
The status depend of one of this 4 actions:
- ToPrepare
- Prepare
- DataWipe
- ToRepair
- Ready
"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
# import pdb; pdb.set_trace()
return self.last_action_of(*states.Physical.actions())
@property
def status(self):
"""Show the actual status of device for this owner.
@ -560,6 +589,20 @@ class Device(Thing):
args[POLYMORPHIC_ON] = cls.type
return args
def is_status(self, action):
from ereuse_devicehub.resources.device import states
if action.type in states.Usage.__members__:
return "Allocate State: "
if action.type in states.Status.__members__:
return "Lifecycle State: "
if action.type in states.Physical.__members__:
return "Physical State: "
return ""
def set_hid(self):
with suppress(TypeError):
self.hid = Naming.hid(

View File

@ -33,6 +33,7 @@ class Trading(State):
:cvar ProductDisposed: The device has been removed
from the facility. It does not mean end-of-life.
"""
Reserved = e.Reserve
Trade = e.Trade
Confirm = e.Confirm
@ -50,16 +51,17 @@ class Trading(State):
class Physical(State):
"""Physical states.
:cvar ToBeRepaired: The device has been selected for reparation.
:cvar Repaired: The device has been repaired.
:cvar Preparing: The device is going to be or being prepared.
:cvar Prepared: The device has been prepared.
:cvar Repair: The device has been repaired.
:cvar ToPrepare: The device is going to be or being prepared.
:cvar Prepare: The device has been prepared.
:cvar Ready: The device is in working conditions.
:cvar DataWipe: Do DataWipe over the device.
"""
ToBeRepaired = e.ToRepair
Repaired = e.Repair
Preparing = e.ToPrepare
Prepared = e.Prepare
ToPrepare = e.ToPrepare
Prepare = e.Prepare
DataWipe = e.DataWipe
ToRepair = e.ToRepair
Ready = e.Ready
@ -68,6 +70,7 @@ class Traking(State):
:cvar Receive: The device changes hands
"""
# Receive = e.Receive
pass
@ -79,6 +82,7 @@ class Usage(State):
:cvar Deallocate: The device is deallocate and return to the owner
:cvar InUse: The device is being reported to be in active use.
"""
Allocate = e.Allocate
Deallocate = e.Deallocate
InUse = e.Live
@ -91,6 +95,7 @@ class Status(State):
:cvar Recycling: The device is sended to recycling.
:cvar Management: The device is owned by one Manager.
"""
Use = e.Use
Refurbish = e.Refurbish
Recycling = e.Recycling

View File

@ -154,6 +154,37 @@
</tbody>
</table>
</div>
<h4>Actual Status</h4>
<ol>
<li>
<strong>
Lifecycle Status
</strong>
{% if device.status %}
{{ device.status.type }}
{% endif %}
</li>
<li>
<strong>
Allocate Status
</strong>
{% if device.allocated_status %}
{{ device.allocated_status.type }}
{% endif %}
</li>
<li>
<strong>
Physical Status
</strong>
{% if device.physical_status %}
{{ device.physical_status.type }}
{% endif %}
</li>
</ol>
<h4>Public traceability log of the device</h4>
<div class="text-right">
<small>Latest one.</small>
@ -162,10 +193,17 @@
{% for action in device.public_actions %}
<li>
<strong>
{{ action.type }}
{{ device.is_status(action) }}
{% if not device.is_status(action) %}
{{ action.type }}
{% endif %}
</strong>
{{ action }}
{% if device.is_status(action) %}
{{ action }} {{ action.type }}
{% else %}
{{ action }}
{% endif %}
<br>
<div class="text-muted">
<small>

View File

@ -70,16 +70,19 @@ class DeviceRow(OrderedDict):
self['Updated in (software)'] = device.updated
self['Updated in (web)'] = ''
self['Physical state'] = ''
if device.physical_status:
self['Physical state'] = device.physical_status.type
self['Allocate state'] = ''
if device.allocated_status:
self['Allocate state'] = device.allocated_status.type
try:
self['Physical state'] = device.last_action_of(
*states.Physical.actions()).t
except LookupError:
self['Physical state'] = ''
try:
self['Trading state'] = device.last_action_of(
self['Lifecycle state'] = device.last_action_of(
*states.Trading.actions()).t
except LookupError:
self['Trading state'] = ''
self['Lifecycle state'] = ''
if isinstance(device, d.Computer):
self['Processor'] = none2str(device.processor_model)
self['RAM (MB)'] = none2str(device.ram_size)
@ -367,15 +370,18 @@ class StockRow(OrderedDict):
self['Model'] = none2str(device.model)
self['Manufacturer'] = none2str(device.manufacturer)
self['Registered in'] = format(device.created, '%c')
self['Physical state'] = ''
if device.physical_status:
self['Physical state'] = device.physical_status.type
self['Allocate state'] = ''
if device.allocated_status:
self['Allocate state'] = device.allocated_status.type
try:
self['Physical state'] = device.last_action_of(
*states.Physical.actions()).t
self['Lifecycle state'] = device.last_action_of(*states.Trading.actions()).t
except LookupError:
self['Physical state'] = ''
try:
self['Trading state'] = device.last_action_of(*states.Trading.actions()).t
except LookupError:
self['Trading state'] = ''
self['Lifecycle state'] = ''
self['Price'] = none2str(device.price)
self['Processor'] = none2str(device.processor_model)
self['RAM (MB)'] = none2str(device.ram_size)

View File

@ -4,7 +4,7 @@ $(document).ready(() => {
})
function deviceInputs() {
if ($("#type").val() == "Monitor") {
if ($("#type").val() == "ComputerMonitor") {
$("#screen").show();
$("#resolution").show();
$("#imei").hide();

View File

@ -0,0 +1,621 @@
"use strict";
function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError("attempted to " + action + " private static field before its declaration"); } }
function _classCheckPrivateStaticAccess(receiver, classConstructor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } }
function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
$(document).ready(() => {
const show_allocate_form = $("#allocateModal").data("show-action-form");
const show_datawipe_form = $("#datawipeModal").data("show-action-form");
const show_trade_form = $("#tradeLotModal").data("show-action-form");
if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show();
newAllocate(show_allocate_form);
} else if (show_datawipe_form != "None") {
$("#datawipeModal .btn-primary").show();
newDataWipe(show_datawipe_form);
} else if (show_trade_form != "None") {
$("#tradeLotModal .btn-primary").show();
newTrade(show_trade_form);
} else {
$(".deviceSelect").on("change", deviceSelect);
} // $('#selectLot').selectpicker();
});
class TableController {
/**
* @returns Selected inputs from device list
*/
static getSelectedDevices() {
if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this) == undefined) return [];
return _classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this).filter(element => element.querySelector("input").checked).map(element => element.querySelector("input"));
}
/**
* @returns Selected inputs in current page from device list
*/
static getAllSelectedDevicesInCurrentPage() {
if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this) == undefined) return [];
return _classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this).filter(element => element.querySelector("input").checked).map(element => element.querySelector("input"));
}
/**
* @returns All inputs from device list
*/
static getAllDevices() {
if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this) == undefined) return [];
return _classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this).map(element => element.querySelector("input"));
}
/**
* @returns All inputs from current page in device list
*/
static getAllDevicesInCurrentPage() {
if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this) == undefined) return [];
return _classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this).map(element => element.querySelector("input"));
}
/**
*
* @param {HTMLElement} DOMElements
* @returns Procesed input atributes to an Object class
*/
static ProcessTR(DOMElements) {
return DOMElements.map(element => {
const info = {};
info.checked = element.checked;
Object.values(element.attributes).forEach(attrib => {
info[attrib.nodeName.replace(/-/g, "_")] = attrib.nodeValue;
});
return info;
});
}
}
/**
* Select all functionality
*/
var _tableRows = {
writable: true,
value: () => table.activeRows.length > 0 ? table.activeRows : []
};
var _tableRowsPage = {
writable: true,
value: () => table.pages[table.rows().dt.currentPage - 1]
};
const selectorController = action => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function softInit() {
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
}); // https://github.com/fiduswriter/Simple-DataTables/wiki/Events
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
}
if (action == "softInit") {
softInit();
itemListCheckChanged();
return;
}
function itemListCheckChanged() {
alertInfoDevices.innerHTML = "Selected devices: ".concat(TableController.getSelectedDevices().length, "\n ").concat(TableController.getAllDevices().length != TableController.getSelectedDevices().length ? "<a href=\"#\" class=\"ml-3\">Select all devices (".concat(TableController.getAllDevices().length, ")</a>") : "<a href=\"#\" class=\"ml-3\">Cancel selection</a>");
if (TableController.getSelectedDevices().length <= 0) {
alertInfoDevices.classList.add("d-none");
} else {
alertInfoDevices.classList.remove("d-none");
}
if (TableController.getAllDevices().length == TableController.getSelectedDevices().length) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
} else if (TableController.getAllSelectedDevicesInCurrentPage().length > 0) {
btnSelectAll.indeterminate = true;
} else {
btnSelectAll.checked = false;
btnSelectAll.indeterminate = false;
}
if (TableController.getAllDevices().length == 0) {
btnSelectAll.checked = false;
btnSelectAll.disabled = true;
} else {
btnSelectAll.disabled = false;
}
get_device_list();
}
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => {
ckeckbox.checked = checkedState;
});
itemListCheckChanged();
});
alertInfoDevices.addEventListener("click", () => {
const checkState = TableController.getAllDevices().length == TableController.getSelectedDevices().length;
TableController.getAllDevices().forEach(ckeckbox => {
ckeckbox.checked = !checkState;
});
itemListCheckChanged();
});
softInit();
itemListCheckChanged();
};
window.addEventListener("DOMContentLoaded", () => selectorController());
function deviceSelect() {
const devices_count = TableController.getSelectedDevices().length;
get_device_list();
if (devices_count == 0) {
$("#addingLotModal .pol").show();
$("#addingLotModal .btn-primary").hide();
$("#removeLotModal .pol").show();
$("#removeLotModal .btn-primary").hide();
$("#addingTagModal .pol").show();
$("#addingTagModal .btn-primary").hide();
$("#actionModal .pol").show();
$("#actionModal .btn-primary").hide();
$("#allocateModal .pol").show();
$("#allocateModal .btn-primary").hide();
$("#datawipeModal .pol").show();
$("#datawipeModal .btn-primary").hide();
} else {
$("#addingLotModal .pol").hide();
$("#addingLotModal .btn-primary").show();
$("#removeLotModal .pol").hide();
$("#removeLotModal .btn-primary").show();
$("#actionModal .pol").hide();
$("#actionModal .btn-primary").show();
$("#allocateModal .pol").hide();
$("#allocateModal .btn-primary").show();
$("#datawipeModal .pol").hide();
$("#datawipeModal .btn-primary").show();
$("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
}
}
function removeLot() {
const devices = TableController.getAllDevices();
if (devices.length > 0) {
$("#btnRemoveLots .text-danger").show();
} else {
$("#btnRemoveLots .text-danger").hide();
}
$("#activeRemoveLotModal").click();
}
function removeTag() {
const devices = TableController.getSelectedDevices();
const devices_id = devices.map(dev => dev.data);
if (devices_id.length == 1) {
const url = "/inventory/tag/devices/".concat(devices_id[0], "/del/");
window.location.href = url;
} else {
$("#unlinkTagAlertModal").click();
}
}
function addTag() {
const devices = TableController.getSelectedDevices();
const devices_id = devices.map(dev => dev.data);
if (devices_id.length == 1) {
$("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
} else {
$("#addingTagModal .pol").show();
$("#addingTagModal .btn-primary").hide();
}
$("#addTagAlertModal").click();
}
function newTrade(action) {
let title = "Trade ";
const user_to = $("#user_to").data("email");
const user_from = $("#user_from").data("email");
if (action == "user_from") {
title = "Trade Incoming";
$("#user_to").attr("readonly", "readonly");
$("#user_from").prop("readonly", false);
$("#user_from").val("");
$("#user_to").val(user_to);
} else if (action == "user_to") {
title = "Trade Outgoing";
$("#user_from").attr("readonly", "readonly");
$("#user_to").prop("readonly", false);
$("#user_to").val("");
$("#user_from").val(user_from);
}
$("#tradeLotModal #title-action").html(title);
$("#activeTradeModal").click();
}
function newAction(action) {
$("#actionModal #type").val(action);
$("#actionModal #title-action").html(action);
deviceSelect();
$("#activeActionModal").click();
}
function newAllocate(action) {
$("#allocateModal #type").val(action);
$("#allocateModal #title-action").html(action);
deviceSelect();
$("#activeAllocateModal").click();
}
function newDataWipe(action) {
$("#datawipeModal #type").val(action);
$("#datawipeModal #title-action").html(action);
deviceSelect();
$("#activeDatawipeModal").click();
}
function get_device_list() {
const devices = TableController.getSelectedDevices();
/* Insert the correct count of devices in actions form */
const devices_count = devices.length;
$("#datawipeModal .devices-count").html(devices_count);
$("#allocateModal .devices-count").html(devices_count);
$("#actionModal .devices-count").html(devices_count);
/* Insert the correct value in the input devicesList */
const devices_id = $.map(devices, x => $(x).attr("data")).join(",");
$.map($(".devicesList"), x => {
$(x).val(devices_id);
});
/* Create a list of devices for human representation */
const computer = {
"Desktop": "<i class='bi bi-building'></i>",
"Laptop": "<i class='bi bi-laptop'></i>"
};
const list_devices = devices.map(x => {
let typ = $(x).data("device-type");
const manuf = $(x).data("device-manufacturer");
const dhid = $(x).data("device-dhid");
if (computer[typ]) {
typ = computer[typ];
}
;
return "".concat(typ, " ").concat(manuf, " ").concat(dhid);
});
const description = $.map(list_devices, x => x).join(", ");
$(".enumeration-devices").html(description);
}
function export_file(type_file) {
const devices = TableController.getSelectedDevices();
const devices_id = $.map(devices, x => $(x).attr("data-device-dhid")).join(",");
if (devices_id) {
const url = "/inventory/export/".concat(type_file, "/?ids=").concat(devices_id);
window.location.href = url;
} else {
$("#exportAlertModal").click();
}
}
/**
* Reactive lots button
*/
async function processSelectedDevices() {
class Actions {
constructor() {
this.list = []; // list of petitions of requests @item --> {type: ["Remove" | "Add"], "LotID": string, "devices": number[]}
}
/**
* Manage the actions that will be performed when applying the changes
* @param {EventSource} ev event (Should be a checkbox type)
* @param {Lot} lot lot id
* @param {Device[]} selectedDevices device id
*/
manage(event, lot, selectedDevices) {
event.preventDefault();
const lotID = lot.id;
const srcElement = event.srcElement.parentElement.children[0];
const checked = !srcElement.checked;
const {
indeterminate
} = srcElement;
const found = this.list.filter(list => list.lot.id == lotID)[0];
if (checked) {
if (found && found.type == "Remove") {
const affectedDevices = found.devices.filter(dev => found.lot.devices.includes(dev.id));
if (affectedDevices.length > 0 && found.indeterminate == false) {
// Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id);
} else {
found.type = "Add";
}
} else {
this.list.push({
type: "Add",
lot,
devices: selectedDevices,
indeterminate
});
}
} else if (found && found.type == "Add") {
const affectedDevices = found.devices.filter(dev => !found.lot.devices.includes(dev.id));
if (affectedDevices.length > 0 && found.indeterminate == false) {
// Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id);
} else {
found.type = "Remove";
}
} else {
this.list.push({
type: "Remove",
lot,
devices: selectedDevices,
indeterminate
});
}
if (this.list.length > 0) {
document.getElementById("ApplyDeviceLots").classList.remove("disabled");
} else {
document.getElementById("ApplyDeviceLots").classList.add("disabled");
}
}
/**
* Creates notification to give feedback to user
* @param {string} title notification title
* @param {string | null} toastText notification text
* @param {boolean} isError defines if a toast is a error
*/
notifyUser(title, toastText, isError) {
const toast = document.createElement("div");
toast.classList = "alert alert-dismissible fade show ".concat(isError ? "alert-danger" : "alert-success");
toast.attributes["data-autohide"] = !isError;
toast.attributes.role = "alert";
toast.style = "margin-left: auto; width: fit-content;";
toast.innerHTML = "<strong>".concat(title, "</strong><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>");
if (toastText && toastText.length > 0) {
toast.innerHTML += "<br>".concat(toastText);
}
document.getElementById("NotificationsContainer").appendChild(toast);
if (!isError) {
setTimeout(() => toast.classList.remove("show"), 3000);
}
setTimeout(() => document.getElementById("NotificationsContainer").innerHTML == "", 3500);
}
/**
* Get actions and execute call request to add or remove devices from lots
*/
doActions() {
let requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
this.list.forEach(async action => {
if (action.type == "Add") {
try {
const devicesIDs = action.devices.filter(dev => !action.lot.devices.includes(dev.id)).map(dev => dev.id);
await Api.devices_add(action.lot.id, devicesIDs);
this.notifyUser("Devices sucefully added to selected lot/s", "", false);
} catch (error) {
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
}
} else if (action.type == "Remove") {
try {
const devicesIDs = action.devices.filter(dev => action.lot.devices.includes(dev.id)).map(dev => dev.id);
await Api.devices_remove(action.lot.id, devicesIDs);
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
} catch (error) {
this.notifyUser("Failed to remove devices from selected lot/s", error.responseJSON.message, true);
}
}
requestCount += 1;
if (requestCount == this.list.length) {
this.reRenderTable();
this.list = [];
}
});
$("#confirmLotsModal").modal("hide"); // Hide dialog when click "Save changes"
document.getElementById("dropDownLotsSelector").classList.remove("show");
}
/**
* Re-render list in table
*/
async reRenderTable() {
const newRequest = await Api.doRequest(window.location);
const tmpDiv = document.createElement("div");
tmpDiv.innerHTML = newRequest;
const newTable = document.createElement("table");
newTable.innerHTML = tmpDiv.querySelector("table").innerHTML;
newTable.classList = "table";
const oldTable = document.querySelector(".dataTable-wrapper");
oldTable.parentElement.replaceChild(newTable, oldTable);
table = new simpleDatatables.DataTable(newTable, {
perPage: 20
}); // // Restore state of checkbox
const selectAllBTN = document.getElementById("SelectAllBTN");
selectAllBTN.checked = false;
selectAllBTN.indeterminate = false; // Re-init SelectorController
selectorController("softInit");
}
}
let eventClickActions;
/**
* Generates a list item with a correspondient checkbox state
* @param {Object} lot Lot model server
* @param {Device[]} selectedDevices list selected devices
* @param {HTMLElement} elementTarget
* @param {Action[]} actions
*/
function templateLot(lot, selectedDevices, elementTarget, actions) {
elementTarget.innerHTML = "";
const {
id,
name,
state
} = lot;
const htmlTemplate = "<input class=\"form-check-input\" type=\"checkbox\" id=\"".concat(id, "\" style=\"width: 20px; height: 20px; margin-right: 7px;\">\n <label class=\"form-check-label\" for=\"").concat(id, "\">").concat(name, "</label>");
const doc = document.createElement("li");
doc.innerHTML = htmlTemplate;
switch (state) {
case "true":
doc.children[0].checked = true;
break;
case "false":
doc.children[0].checked = false;
break;
case "indetermined":
doc.children[0].indeterminate = true;
break;
default:
console.warn("This shouldn't be happend: Lot without state: ", lot);
break;
}
doc.children[0].addEventListener("mouseup", ev => actions.manage(ev, lot, selectedDevices));
doc.children[1].addEventListener("mouseup", ev => actions.manage(ev, lot, selectedDevices));
elementTarget.append(doc);
}
const listHTML = $("#LotsSelector"); // Get selected devices
const selectedDevicesID = TableController.ProcessTR(TableController.getSelectedDevices()).map(item => item.data);
if (selectedDevicesID.length <= 0) {
listHTML.html("<li style=\"color: red; text-align: center\">No devices selected</li>");
return;
} // Initialize Actions list, and set checkbox triggers
const actions = new Actions();
if (eventClickActions) {
document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions);
}
eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => {
const modal = $("#confirmLotsModal");
modal.modal({
keyboard: false
});
let list_changes_html = ""; // {type: ["Remove" | "Add"], "LotID": string, "devices": number[]}
actions.list.forEach(action => {
let type;
let devices;
if (action.type == "Add") {
type = "success";
devices = action.devices.filter(dev => !action.lot.devices.includes(dev.id)); // Only show affected devices
} else {
type = "danger";
devices = action.devices.filter(dev => action.lot.devices.includes(dev.id)); // Only show affected devices
}
list_changes_html += "\n <div class=\"card border-primary mb-3 w-100\">\n <div class=\"card-header\" title=\"".concat(action.lotID, "\">").concat(action.lot.name, "</div>\n <div class=\"card-body pt-3\">\n <p class=\"card-text\">\n ").concat(devices.map(item => {
const name = "".concat(item.type, " ").concat(item.manufacturer, " ").concat(item.model);
return "<span class=\"badge bg-".concat(type, "\" title=\"").concat(name, "\">").concat(item.devicehubID, "</span>");
}).join(" "), "\n </p>\n </div>\n </div>");
});
modal.find(".modal-body").html(list_changes_html);
const el = document.getElementById("SaveAllActions");
const elClone = el.cloneNode(true);
el.parentNode.replaceChild(elClone, el);
elClone.addEventListener("click", () => actions.doActions());
modal.modal("show"); // actions.doActions();
});
document.getElementById("ApplyDeviceLots").classList.add("disabled");
try {
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>");
const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots();
lots = lots.map(lot => {
lot.devices = selectedDevices.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0).map(device => parseInt(device.id));
switch (lot.devices.length) {
case 0:
lot.state = "false";
break;
case selectedDevicesID.length:
lot.state = "true";
break;
default:
lot.state = "indetermined";
break;
}
return lot;
});
let lotsList = [];
lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name)));
lotsList = lotsList.flat(); // flat array
listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
} catch (error) {
console.log(error);
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");
}
}

View File

@ -78,10 +78,28 @@ class TableController {
/**
* Select all functionality
*/
window.addEventListener("DOMContentLoaded", () => {
const selectorController = (action) => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function softInit() {
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
// https://github.com/fiduswriter/Simple-DataTables/wiki/Events
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
}
if (action == "softInit") {
softInit();
itemListCheckChanged();
return;
}
function itemListCheckChanged() {
alertInfoDevices.innerHTML = `Selected devices: ${TableController.getSelectedDevices().length}
${TableController.getAllDevices().length != TableController.getSelectedDevices().length
@ -111,11 +129,9 @@ window.addEventListener("DOMContentLoaded", () => {
} else {
btnSelectAll.disabled = false;
}
}
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
get_device_list();
}
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
@ -129,13 +145,12 @@ window.addEventListener("DOMContentLoaded", () => {
itemListCheckChanged()
})
// https://github.com/fiduswriter/Simple-DataTables/wiki/Events
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
softInit();
itemListCheckChanged();
})
}
window.addEventListener("DOMContentLoaded", () => selectorController());
function deviceSelect() {
const devices_count = TableController.getSelectedDevices().length;
@ -277,7 +292,7 @@ function get_device_list() {
"Laptop": "<i class='bi bi-laptop'></i>",
};
list_devices = devices.map((x) => {
const list_devices = devices.map((x) => {
let typ = $(x).data("device-type");
const manuf = $(x).data("device-manufacturer");
const dhid = $(x).data("device-dhid");
@ -287,7 +302,7 @@ function get_device_list() {
return `${typ} ${manuf} ${dhid}`;
});
description = $.map(list_devices, (x) => x).join(", ");
const description = $.map(list_devices, (x) => x).join(", ");
$(".enumeration-devices").html(description);
}
@ -324,19 +339,30 @@ async function processSelectedDevices() {
const lotID = lot.id;
const srcElement = event.srcElement.parentElement.children[0]
const checked = !srcElement.checked;
const { indeterminate } = srcElement
const found = this.list.filter(list => list.lot.id == lotID)[0];
if (checked) {
if (found && found.type == "Remove") {
found.type = "Add";
const affectedDevices = found.devices.filter(dev => found.lot.devices.includes(dev.id))
if (affectedDevices.length > 0 && found.indeterminate == false) { // Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id)
} else {
found.type = "Add";
}
} else {
this.list.push({ type: "Add", lot, devices: selectedDevices });
this.list.push({ type: "Add", lot, devices: selectedDevices, indeterminate });
}
} else if (found && found.type == "Add") {
found.type = "Remove";
const affectedDevices = found.devices.filter(dev => !found.lot.devices.includes(dev.id))
if (affectedDevices.length > 0 && found.indeterminate == false) { // Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id)
} else {
found.type = "Remove";
}
} else {
this.list.push({ type: "Remove", lot, devices: selectedDevices });
this.list.push({ type: "Remove", lot, devices: selectedDevices, indeterminate });
}
if (this.list.length > 0) {
@ -412,22 +438,23 @@ async function processSelectedDevices() {
const tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
// https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
const rowsToRemove = []
for (let i = 0; i < table.activeRows.length; i++) {
const row = table.activeRows[i];
if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
rowsToRemove.push(i)
}
}
table.rows().remove(rowsToRemove);
const newTable = document.createElement("table")
newTable.innerHTML = tmpDiv.querySelector("table").innerHTML
newTable.classList = "table"
// Restore state of checkbox
const oldTable = document.querySelector(".dataTable-wrapper")
oldTable.parentElement.replaceChild(newTable, oldTable)
table = new simpleDatatables.DataTable(newTable, {perPage: 20})
// // Restore state of checkbox
const selectAllBTN = document.getElementById("SelectAllBTN");
selectAllBTN.checked = false;
selectAllBTN.indeterminate = false;
// Re-init SelectorController
selectorController("softInit");
}
}

View File

@ -64,6 +64,16 @@
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('workbench.settings') }}">
<i class="bi bi-tools"></i>
<span>Workbench Settings</span>
</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="https://help.usody.com/" target="_blank">
<i class="bi bi-question-circle"></i>
@ -101,6 +111,15 @@
</a>
</li><!-- End Dashboard Nav -->
<li class="nav-heading">Devices</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.devicelist') }}?only_unassigned=false">
<i class="bi bi-laptop"></i>
<span>All devices</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.snapshotslist') }}">
<i class="bi-menu-button-wide"></i>

View File

@ -29,6 +29,9 @@
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{% if field == form_new_allocate.start_time %}
<span class="text-danger">*</span>
{% endif %}
{{ field(class_="form-control") }}
{% if field.errors %}
<p class="text-danger">

View File

@ -36,9 +36,9 @@
<label for="name" class="form-label">Type *</label>
<select id="type" class="form-control" name="type" required="">
<option value="">Select one Type</option>
<optgroup label="Computer Monitor">
<option value="Monitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Monitor</option>
<optgroup label="Monitor">
<option value="ComputerMonitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Computer Monitor</option>
</optgroup>
<optgroup label="Mobile">
<option value="Smartphone"

View File

@ -124,16 +124,28 @@
<div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label">Physical States</div>
<div class="col-lg-9 col-md-8">{{ device.physical or '' }}</div>
<div class="col-lg-3 col-md-4 label">Physical State</div>
<div class="col-lg-9 col-md-8">
{% if device.physical_status %}
{{ device.physical_status.type }}
{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Trading States</div>
<div class="col-lg-9 col-md-8">{{ device.last_action_trading or ''}}</div>
<div class="col-lg-3 col-md-4 label">Lifecycle State</div>
<div class="col-lg-9 col-md-8">
{% if device.status %}
{{ device.status.type }}
{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Usage States</div>
<div class="col-lg-9 col-md-8">{{ device.usage or '' }}</div>
<div class="col-lg-3 col-md-4 label">Allocated State</div>
<div class="col-lg-9 col-md-8">
{% if device.allocated_status %}
{{ device.allocated_status.type }}
{% endif %}
</div>
</div>
</div>

View File

@ -7,7 +7,11 @@
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
{% if not lot %}
<li class="breadcrumb-item active">Unassigned</li>
{% if unassigned_devices %}
<li class="breadcrumb-item active">Unassigned</li>
{% else %}
<li class="breadcrumb-item active">All devices</li>
{% endif %}
{% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
@ -213,7 +217,7 @@
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnUniqueID" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
Unique Identifiers
Unique Identifiers (Tags)
</button>
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
@ -334,8 +338,11 @@
<th scope="col">Title</th>
<th scope="col">DHID</th>
<th scope="col">Unique Identifiers</th>
<th scope="col">Status</th>
<th scope="col">Lifecycle Status</th>
<th scope="col">Allocated Status</th>
<th scope="col">Physical Status</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@ -351,12 +358,19 @@
/>
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{% if dev.get_type_logo() %}
<i class="{{ dev.get_type_logo() }}" title="{{ dev.type }}"></i>
{% endif %}
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.verbose_name }}
</a>
{% if dev.lots | length > 0 %}
<h6 class="d-inline">
{% for lot in dev.lots %}
<span class="badge rounded-pill bg-light text-dark">{{ lot.name }}</span>
{% endfor %}
</h6>
{% endif %}
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
@ -370,7 +384,14 @@
{% endfor %}
</td>
<td>{% if dev.status %}{{ dev.status.type }}{% endif %}</td>
<td>{% if dev.allocated_status %}{{ dev.allocated_status.type }}{% endif %}</td>
<td>{% if dev.physical_status %}{{ dev.physical_status.type }}{% endif %}</td>
<td>{{ dev.updated.strftime('%H:%M %d-%m-%Y') }}</td>
<td>
<a href="{{ dev.public_link }}" target="_blank">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
@ -430,9 +451,9 @@
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table", {
let table = new simpleDatatables.DataTable("table", {
perPage: 20
})
</script>
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
<script src="{{ url_for('static', filename='js/main_inventory.build.js') }}"></script>
{% endblock main %}

View File

@ -47,6 +47,7 @@
<th scope="col">Type</th>
<th scope="col">Provider</th>
<th scope="col">Device</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
@ -62,6 +63,7 @@
</a>
{% endif %}
</td>
<td>{{ tag.created.strftime('%H:%M %d-%m-%Y') }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,43 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item">{{ page_title }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-6">
<div class="card">
<div class="card-body">
<div class="pt-6 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Download your settings for Workbench</h5>
<p class="text-center small">Please select one of this options</p>
<div class="row pt-3">
<div class="col-5">
<a href="{{ url_for('workbench.settings') }}?opt=register" class="btn btn-primary">Register devices</a>
</div>
<div class="col">
<p class="small">Download the settings only for register devices.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

View File

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

View File

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

View File

View File

@ -0,0 +1,76 @@
import time
import flask
from flask import Blueprint
from flask import current_app as app
from flask import g, make_response, request
from flask_login import login_required
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 GenericMixin
workbench = Blueprint('workbench', __name__, url_prefix='/workbench')
class SettingsView(GenericMixin):
decorators = [login_required]
template_name = 'workbench/settings.html'
page_title = "Workbench Settings"
def dispatch_request(self):
self.get_context()
self.context.update(
{
'page_title': self.page_title,
}
)
self.opt = request.values.get('opt')
if self.opt in ['register']:
return self.download()
return flask.render_template(self.template_name, **self.context)
def download(self):
url = "https://{}/api/inventory/".format(app.config['HOST'])
self.wbContext = {
'token': self.get_token(),
'url': url,
}
options = {"register": self.register}
return options[self.opt]()
def register(self):
data = flask.render_template('workbench/wbSettings.ini', **self.wbContext)
return self.response_download(data)
def response_download(self, data):
bfile = str.encode(data)
output = make_response(bfile)
output.headers['Content-Disposition'] = 'attachment; filename=settings.ini'
output.headers['Content-type'] = 'text/plain'
return output
def get_token(self):
if not g.user.sessions:
ses = Session(user=g.user)
db.session.add(ses)
db.session.commit()
tk = ''
now = time.time()
for s in g.user.sessions:
if s.type == SessionType.Internal and (s.expired == 0 or s.expired > now):
tk = s.token
break
assert tk != ''
token = auth.Auth.encode(tk)
return token
workbench.add_url_rule('/settings/', view_func=SettingsView.as_view('settings'))

View File

@ -11,12 +11,14 @@ from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices
from ereuse_devicehub.labels.views import labels
from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench
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
# NOTE: enable by blueprint to exclude API views

View File

@ -3,12 +3,15 @@
"version": "1.0.0",
"description": "Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)",
"main": "index.js",
"browserslist": "> 0.25%, not dead",
"directories": {
"doc": "docs",
"example": "examples",
"test": "tests"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
@ -17,12 +20,14 @@
},
"scripts": {
"lint:report": "eslint ereuse_devicehub --ext .js --output-file eslint_report.json --format json",
"lint:fix": "eslint ereuse_devicehub --ext .js --fix"
"lint:fix": "eslint ereuse_devicehub --ext .js --fix",
"babel": "babel ereuse_devicehub/static/js/main_inventory.js --out-file ereuse_devicehub/static/js/main_inventory.build.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.17.10",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0"

View File

@ -26,6 +26,7 @@ from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.user.models import Session, User
from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench
STARTT = datetime(year=2000, month=1, day=1, hour=1)
"""A dummy starting time to use in tests."""
@ -61,6 +62,7 @@ def _app(config: TestConfig) -> Devicehub:
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
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
Type;Chassis;Serial Number;Model;Manufacturer;Registered in;Physical state;Trading state;Price;Processor;RAM (MB);Data Storage Size (MB)
Desktop;Microtower;d1s;d1ml;d1mr;Tue Mar 29 18:13:05 2022;;;;p1ml;0;0
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

1 Type Chassis Serial Number Model Manufacturer Registered in Physical state Trading state Allocate state Lifecycle state Price Processor RAM (MB) Data Storage Size (MB)
2 Desktop Microtower d1s d1ml d1mr Tue Mar 29 18:13:05 2022 Mon May 16 19:08:44 2022 p1ml 0 0

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ def test_dependencies():
# Simplejson has a different signature than stdlib json
# should be fixed though
# noinspection PyUnresolvedReferences
import simplejson
import simplejson # noqa: F401
# noinspection PyArgumentList
@ -88,6 +88,7 @@ def test_api_docs(client: Client):
'/users/login/',
'/users/logout/',
'/versions/',
'/workbench/settings/',
}
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
assert docs['components']['securitySchemes']['bearerAuth'] == {

View File

@ -1,30 +1,30 @@
import csv
import hashlib
from datetime import datetime
from io import StringIO, BytesIO
from io import BytesIO, StringIO
from pathlib import Path
from flask import url_for
import pytest
from werkzeug.exceptions import Unauthorized
import teal.marshmallow
from ereuse_utils.test import ANY
from flask import url_for
from werkzeug.exceptions import Unauthorized
from ereuse_devicehub import auth
from ereuse_devicehub.client import Client, UserClient
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.user.models import Session
from ereuse_devicehub.resources.action.models import Snapshot, Allocate, Live
from ereuse_devicehub.resources.documents import documents
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.action.models import Allocate, Live, Snapshot
from ereuse_devicehub.resources.device import models as d
from ereuse_devicehub.resources.documents import documents
from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.hash_reports import ReportHash
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.hash_reports import ReportHash
from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user.models import Session
from tests import conftest
from tests.conftest import file, yaml2json, json_encode
from tests.conftest import file, json_encode, yaml2json
@pytest.mark.mvp
@ -33,27 +33,30 @@ def test_erasure_certificate_public_one(user: UserClient, client: Client):
s = file('erase-sectors.snapshot')
snapshot, _ = user.post(s, res=Snapshot)
doc, response = user.get(res=documents.DocumentDef.t,
item='erasures/{}'.format(
snapshot['device']['id']),
accept=ANY)
doc, response = user.get(
res=documents.DocumentDef.t,
item='erasures/{}'.format(snapshot['device']['id']),
accept=ANY,
)
assert 'html' in response.content_type
assert '<html' in doc
assert '2018' in doc
doc, response = client.get(res=documents.DocumentDef.t,
item='erasures/{}'.format(
snapshot['device']['id']),
query=[('format', 'PDF')],
accept='application/pdf')
doc, response = client.get(
res=documents.DocumentDef.t,
item='erasures/{}'.format(snapshot['device']['id']),
query=[('format', 'PDF')],
accept='application/pdf',
)
assert 'application/pdf' == response.content_type
erasure = next(e for e in snapshot['actions']
if e['type'] == 'EraseSectors')
erasure = next(e for e in snapshot['actions'] if e['type'] == 'EraseSectors')
doc, response = client.get(res=documents.DocumentDef.t,
item='erasures/{}'.format(erasure['id']),
accept=ANY)
doc, response = client.get(
res=documents.DocumentDef.t,
item='erasures/{}'.format(erasure['id']),
accept=ANY,
)
assert 'html' in response.content_type
assert '<html' in doc
assert '2018' in doc
@ -67,30 +70,32 @@ def test_erasure_certificate_private_query(user: UserClient):
s = file('erase-sectors.snapshot')
snapshot, response = user.post(s, res=Snapshot)
doc, response = user.get(res=documents.DocumentDef.t,
item='erasures/',
query=[
('filter', {'ids': [snapshot['device']['id']]})],
accept=ANY)
doc, response = user.get(
res=documents.DocumentDef.t,
item='erasures/',
query=[('filter', {'ids': [snapshot['device']['id']]})],
accept=ANY,
)
assert 'html' in response.content_type
assert '<html' in doc
assert '2018' in doc
doc, response = user.get(res=documents.DocumentDef.t,
item='erasures/',
query=[
('filter', {
'ids': [snapshot['device']['id']]}),
('format', 'PDF')
],
accept='application/pdf')
doc, response = user.get(
res=documents.DocumentDef.t,
item='erasures/',
query=[('filter', {'ids': [snapshot['device']['id']]}), ('format', 'PDF')],
accept='application/pdf',
)
assert 'application/pdf' == response.content_type
@pytest.mark.mvp
def test_erasure_certificate_wrong_id(client: Client):
client.get(res=documents.DocumentDef.t, item='erasures/this-is-not-an-id',
status=teal.marshmallow.ValidationError)
client.get(
res=documents.DocumentDef.t,
item='erasures/this-is-not-an-id',
status=teal.marshmallow.ValidationError,
)
@pytest.mark.mvp
@ -98,20 +103,27 @@ def test_export_csv_permitions(user: UserClient, user2: UserClient, client: Clie
"""test export device information in a csv file with others users."""
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
dev_id = snapshot['device']['id']
csv_user, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})])
csv_user, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})],
)
csv_user2, _ = user2.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})])
csv_user2, _ = user2.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})],
)
_, res = client.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})], status=401)
_, res = client.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})],
status=401,
)
assert res.status_code == 401
assert len(csv_user) > 0
@ -124,36 +136,46 @@ def test_export_csv_actions(user: UserClient, user2: UserClient, client: Client)
acer = yaml2json('acer.happy.battery.snapshot')
snapshot, _ = user.post(json_encode(acer), res=Snapshot)
device_id = snapshot['device']['id']
post_request = {"transaction": "ccc", "name": "John", "endUsers": 1,
"devices": [device_id], "description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00"
}
post_request = {
"transaction": "ccc",
"name": "John",
"endUsers": 1,
"devices": [device_id],
"description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00",
}
user.post(res=Allocate, data=post_request)
hdd = [c for c in acer['components'] if c['type'] == 'HardDrive'][0]
hdd_action = [a for a in hdd['actions']
if a['type'] == 'TestDataStorage'][0]
hdd_action = [a for a in hdd['actions'] if a['type'] == 'TestDataStorage'][0]
hdd_action['lifetime'] += 1000
acer.pop('elapsed')
acer['licence_version'] = '1.0.0'
snapshot, _ = client.post(acer, res=Live)
csv_user, _ = user.get(res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [device_id]})])
csv_user, _ = user.get(
res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [device_id]})],
)
csv_user2, _ = user2.get(res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_user2, _ = user2.get(
res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
_, res = client.get(res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})], status=401)
_, res = client.get(
res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
status=401,
)
assert res.status_code == 401
assert len(csv_user) > 0
@ -167,21 +189,27 @@ def test_live_export_csv2(user: UserClient, client: Client, app: Devicehub):
acer = yaml2json('acer-happy.snapshot-test1')
snapshot, _ = user.post(json_encode(acer), res=Snapshot)
device_id = snapshot['device']['id']
post_request = {"transaction": "ccc", "name": "John", "endUsers": 1,
"devices": [device_id], "description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00"
}
post_request = {
"transaction": "ccc",
"name": "John",
"endUsers": 1,
"devices": [device_id],
"description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00",
}
user.post(res=Allocate, data=post_request)
acer = yaml2json('acer-happy.live-test1')
live, _ = client.post(acer, res=Live)
csv_user, _ = user.get(res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [device_id]})])
csv_user, _ = user.get(
res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [device_id]})],
)
assert "4692" in csv_user
assert "8692" in csv_user
@ -195,11 +223,15 @@ def test_live_example2(user: UserClient, client: Client, app: Devicehub):
acer = yaml2json('acer-happy.snapshot-test1')
snapshot, _ = user.post(json_encode(acer), res=Snapshot)
device_id = snapshot['device']['id']
post_request = {"transaction": "ccc", "name": "John", "endUsers": 1,
"devices": [device_id], "description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00"
post_request = {
"transaction": "ccc",
"name": "John",
"endUsers": 1,
"devices": [device_id],
"description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00",
}
user.post(res=Allocate, data=post_request)
@ -217,27 +249,36 @@ def test_export_basic_snapshot(user: UserClient):
"""Test export device information in a csv file."""
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
dev_id = snapshot['device']['id']
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev_id]})],
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f, delimiter=';', quotechar='"')
export_csv = list(obj_csv)
# Open fixture csv and transform to list
with Path(__file__).parent.joinpath('files').joinpath('basic.csv').open() as csv_file:
with Path(__file__).parent.joinpath('files').joinpath(
'basic.csv'
).open() as csv_file:
obj_csv = csv.reader(csv_file, delimiter=';', quotechar='"')
fixture_csv = list(obj_csv)
assert isinstance(datetime.strptime(export_csv[1][19], '%c'), datetime), \
'Register in field is not a datetime'
assert isinstance(
datetime.strptime(export_csv[1][19], '%c'), datetime
), 'Register in field is not a datetime'
assert fixture_csv[0] == export_csv[0], 'Headers are not equal'
assert fixture_csv[1][:19] == export_csv[1][:19], 'Computer information are not equal'
assert (
fixture_csv[1][:19] == export_csv[1][:19]
), 'Computer information are not equal'
assert fixture_csv[1][20] == export_csv[1][20], 'Computer information are not equal'
assert fixture_csv[1][22:] == export_csv[1][22:], 'Computer information are not equal'
assert (
fixture_csv[1][22:] == export_csv[1][22:]
), 'Computer information are not equal'
@pytest.mark.mvp
@ -245,13 +286,17 @@ def test_export_basic_snapshot(user: UserClient):
def test_check_insert_hash(app: Devicehub, user: UserClient, client: Client):
"""Test export device information in a csv file."""
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
hash3 = hashlib.sha3_256(csv_str.encode('utf-8')).hexdigest()
assert ReportHash.query.filter_by(hash3=hash3).count() == 1
result, status = client.get(res=documents.DocumentDef.t, item='check/', query=[('hash', hash3)])
result, status = client.get(
res=documents.DocumentDef.t, item='check/', query=[('hash', hash3)]
)
assert status.status_code == 200
assert result
@ -266,7 +311,9 @@ def test_check_insert_hash(app: Devicehub, user: UserClient, client: Client):
@pytest.mark.mvp
def test_export_extended(app: Devicehub, user: UserClient):
"""Test a export device with all information and a lot of components."""
snapshot1, _ = user.post(file('real-eee-1001pxd.snapshot.12'), res=Snapshot, status=201)
snapshot1, _ = user.post(
file('real-eee-1001pxd.snapshot.12'), res=Snapshot, status=201
)
snapshot2, _ = user.post(file('complete.export.snapshot'), res=Snapshot, status=201)
dev1_id = snapshot1['device']['id']
dev2_id = snapshot2['device']['id']
@ -276,10 +323,12 @@ def test_export_extended(app: Devicehub, user: UserClient):
db.session.add(pc)
db.session.commit()
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev1_id, dev2_id]})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer'], 'ids': [dev1_id, dev2_id]})],
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f, delimiter=';', quotechar='"')
@ -287,28 +336,50 @@ def test_export_extended(app: Devicehub, user: UserClient):
# Open fixture csv and transform to list
with Path(__file__).parent.joinpath('files').joinpath(
'proposal_extended_csv_report.csv').open() as csv_file:
'proposal_extended_csv_report.csv'
).open() as csv_file:
obj_csv = csv.reader(csv_file, delimiter=';', quotechar='"')
fixture_csv = list(obj_csv)
assert isinstance(datetime.strptime(export_csv[1][19], '%c'), datetime), \
'Register in field is not a datetime'
assert isinstance(
datetime.strptime(export_csv[1][19], '%c'), datetime
), 'Register in field is not a datetime'
assert fixture_csv[0] == export_csv[0], 'Headers are not equal'
assert fixture_csv[1][:19] == export_csv[1][:19], 'Computer information are not equal'
assert (
fixture_csv[1][:19] == export_csv[1][:19]
), 'Computer information are not equal'
assert fixture_csv[1][20] == export_csv[1][20], 'Computer information are not equal'
assert fixture_csv[1][22:82] == export_csv[1][22:82], 'Computer information are not equal'
assert fixture_csv[1][83] == export_csv[1][83], 'Computer information are not equal'
assert fixture_csv[1][86:] == export_csv[1][86:], 'Computer information are not equal'
assert fixture_csv[2][:19] == export_csv[2][:19], 'Computer information are not equal'
assert (
fixture_csv[1][22:83] == export_csv[1][22:83]
), 'Computer information are not equal'
assert fixture_csv[1][84] == export_csv[1][84], 'Computer information are not equal'
assert (
fixture_csv[1][87:] == export_csv[1][87:]
), 'Computer information are not equal'
assert (
fixture_csv[2][:19] == export_csv[2][:19]
), 'Computer information are not equal'
assert fixture_csv[2][20] == export_csv[2][20], 'Computer information are not equal'
assert fixture_csv[2][22:82] == export_csv[2][22:82], 'Computer information are not equal'
assert fixture_csv[2][83] == export_csv[2][83], 'Computer information are not equal'
assert fixture_csv[2][86:106] == export_csv[2][86:106], 'Computer information are not equal'
assert fixture_csv[2][109] == export_csv[2][109], 'Computer information are not equal'
assert fixture_csv[2][112:133] == export_csv[2][112:133], 'Computer information are not equal'
assert fixture_csv[2][135] == export_csv[2][135], 'Computer information are not equal'
assert fixture_csv[2][138:] == export_csv[2][138:], 'Computer information are not equal'
assert (
fixture_csv[2][22:83] == export_csv[2][22:83]
), 'Computer information are not equal'
assert fixture_csv[2][84] == export_csv[2][84], 'Computer information are not equal'
assert (
fixture_csv[2][87:107] == export_csv[2][87:107]
), 'Computer information are not equal'
assert (
fixture_csv[2][110] == export_csv[2][110]
), 'Computer information are not equal'
assert (
fixture_csv[2][113:134] == export_csv[2][113:134]
), 'Computer information are not equal'
assert (
fixture_csv[2][136] == export_csv[2][136]
), 'Computer information are not equal'
assert (
fixture_csv[2][139:] == export_csv[2][139:]
), 'Computer information are not equal'
@pytest.mark.mvp
@ -316,9 +387,9 @@ def test_export_empty(user: UserClient):
"""Test to check works correctly exporting csv without any information,
export a placeholder device.
"""
csv_str, _ = user.get(res=documents.DocumentDef.t,
accept='text/csv',
item='devices/')
csv_str, _ = user.get(
res=documents.DocumentDef.t, accept='text/csv', item='devices/'
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f)
export_csv = list(obj_csv)
@ -330,17 +401,20 @@ def test_export_empty(user: UserClient):
def test_export_computer_monitor(user: UserClient):
"""Test a export device type computer monitor."""
snapshot, _ = user.post(file('computer-monitor.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['ComputerMonitor']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['ComputerMonitor']})],
)
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').open() \
as csv_file:
with Path(__file__).parent.joinpath('files').joinpath(
'computer-monitor.csv'
).open() as csv_file:
obj_csv = csv.reader(csv_file)
fixture_csv = list(obj_csv)
@ -356,15 +430,19 @@ def test_export_computer_monitor(user: UserClient):
def test_export_keyboard(user: UserClient):
"""Test a export device type keyboard."""
snapshot, _ = user.post(file('keyboard.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Keyboard']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Keyboard']})],
)
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('keyboard.csv').open() as csv_file:
with Path(__file__).parent.joinpath('files').joinpath(
'keyboard.csv'
).open() as csv_file:
obj_csv = csv.reader(csv_file)
fixture_csv = list(obj_csv)
@ -382,8 +460,9 @@ def test_export_multiple_different_devices(user: UserClient):
computers, keyboards, monitors, etc..)
"""
# Open fixture csv and transform to list
with Path(__file__).parent.joinpath('files').joinpath('multiples_devices.csv').open() \
as csv_file:
with Path(__file__).parent.joinpath('files').joinpath(
'multiples_devices.csv'
).open() as csv_file:
fixture_csv = list(csv.reader(csv_file))
for row in fixture_csv:
del row[8] # We remove the 'Registered in' column
@ -394,10 +473,12 @@ def test_export_multiple_different_devices(user: UserClient):
snapshot_keyboard, _ = user.post(file('keyboard.snapshot'), res=Snapshot)
snapshot_monitor, _ = user.post(file('computer-monitor.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
query=[('filter', {'type': ['Computer', 'Keyboard', 'Monitor']})],
accept='text/csv')
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
query=[('filter', {'type': ['Computer', 'Keyboard', 'Monitor']})],
accept='text/csv',
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f)
export_csv = list(obj_csv)
@ -410,20 +491,26 @@ def test_export_multiple_different_devices(user: UserClient):
@pytest.mark.mvp
def test_report_devices_stock_control(user: UserClient, user2: UserClient):
# TODO
"""Test export device information in a csv file."""
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
snapshot2, _ = user2.post(file('basic.snapshot2'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='stock/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='stock/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
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('basic-stock.csv').open() as csv_file:
with Path(__file__).parent.joinpath('files').joinpath(
'basic-stock.csv'
).open() as csv_file:
obj_csv = csv.reader(csv_file)
fixture_csv = list(obj_csv)
@ -432,35 +519,42 @@ def test_report_devices_stock_control(user: UserClient, user2: UserClient):
export_csv[0] = export_csv[0][0].split(';')
export_csv[1] = export_csv[1][0].split(';')
assert isinstance(datetime.strptime(export_csv[1][5], '%c'), datetime), \
'Register in field is not a datetime'
fixture_csv[0] = fixture_csv[0][0].split(';')
fixture_csv[1] = fixture_csv[1][0].split(';')
assert isinstance(
datetime.strptime(export_csv[1][5], '%c'), datetime
), 'Register in field is not a datetime'
# Pop dates fields from csv lists to compare them
fixture_csv[1] = fixture_csv[1][0].split(";")
fixture_csv[1] = fixture_csv[1][:5] + fixture_csv[1][6:]
export_csv[1] = export_csv[1][:5] + export_csv[1][6:]
export_header = [";".join(export_csv[0])]
assert fixture_csv[0] == export_header, 'Headers are not equal'
assert fixture_csv[0] == export_csv[0], 'Headers are not equal'
assert fixture_csv[1] == export_csv[1], 'Computer information are not equal'
assert fixture_csv == [export_header, export_csv[1]]
assert fixture_csv == export_csv
@pytest.mark.mvp
def test_get_document_lots(user: UserClient, user2: UserClient):
"""Tests submitting and retreiving all lots."""
l, _ = user.post({'name': 'Lot1', 'description': 'comments,lot1,testcomment-lot1,'}, res=Lot)
l, _ = user.post({'name': 'Lot2', 'description': 'comments,lot2,testcomment-lot2,'}, res=Lot)
l, _ = user2.post({'name': 'Lot3-User2', 'description': 'comments,lot3,testcomment-lot3,'}, res=Lot)
l, _ = user.post(
{'name': 'Lot1', 'description': 'comments,lot1,testcomment-lot1,'}, res=Lot
)
l, _ = user.post(
{'name': 'Lot2', 'description': 'comments,lot2,testcomment-lot2,'}, res=Lot
)
l, _ = user2.post(
{'name': 'Lot3-User2', 'description': 'comments,lot3,testcomment-lot3,'},
res=Lot,
)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='lots/',
accept='text/csv')
csv_str, _ = user.get(res=documents.DocumentDef.t, item='lots/', accept='text/csv')
csv2_str, _ = user2.get(res=documents.DocumentDef.t,
item='lots/',
accept='text/csv')
csv2_str, _ = user2.get(
res=documents.DocumentDef.t, item='lots/', accept='text/csv'
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f)
@ -473,10 +567,17 @@ def test_get_document_lots(user: UserClient, user2: UserClient):
assert len(export_csv) == 3
assert len(export2_csv) == 2
assert export_csv[0] == export2_csv[0] == ['Id', 'Name', 'Registered in', 'Description']
assert (
export_csv[0]
== export2_csv[0]
== ['Id', 'Name', 'Registered in', 'Description']
)
assert export_csv[1][1] == 'Lot1' or 'Lot2'
assert export_csv[1][3] == 'comments,lot1,testcomment-lot1,' or 'comments,lot2,testcomment-lot2,'
assert (
export_csv[1][3] == 'comments,lot1,testcomment-lot1,'
or 'comments,lot2,testcomment-lot2,'
)
assert export2_csv[1][1] == 'Lot3-User2'
assert export2_csv[1][3] == 'comments,lot3,testcomment-lot3,'
@ -485,34 +586,39 @@ def test_get_document_lots(user: UserClient, user2: UserClient):
def test_verify_stamp(user: UserClient, client: Client):
"""Test verify stamp of one export device information in a csv file."""
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='devices/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]},
status=200,
)
assert "alert alert-info" in response
assert not "alert alert-danger" in response
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(b'abc'), 'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(b'abc'), 'example.csv')]},
status=200,
)
assert not "alert alert-info" in response
assert "alert alert-danger" in response
response, _ = client.get(res=documents.DocumentDef.t,
item='stamps/',
accept='text/html',
status=200)
response, _ = client.get(
res=documents.DocumentDef.t, item='stamps/', accept='text/html', status=200
)
assert not "alert alert-info" in response
assert not "alert alert-danger" in response
@ -522,20 +628,23 @@ def test_verify_stamp(user: UserClient, client: Client):
def test_verify_stamp_log_info(user: UserClient, client: Client):
"""Test verify stamp of one export lots-info in a csv file."""
l, _ = user.post({'name': 'Lot1', 'description': 'comments,lot1,testcomment-lot1,'}, res=Lot)
l, _ = user.post({'name': 'Lot2', 'description': 'comments,lot2,testcomment-lot2,'}, res=Lot)
l, _ = user.post(
{'name': 'Lot1', 'description': 'comments,lot1,testcomment-lot1,'}, res=Lot
)
l, _ = user.post(
{'name': 'Lot2', 'description': 'comments,lot2,testcomment-lot2,'}, res=Lot
)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='lots/',
accept='text/csv')
csv_str, _ = user.get(res=documents.DocumentDef.t, item='lots/', accept='text/csv')
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')),
'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]},
status=200,
)
assert "alert alert-info" in response
@ -545,18 +654,21 @@ def test_verify_stamp_devices_stock(user: UserClient, client: Client):
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='stock/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='stock/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')),
'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]},
status=200,
)
assert "alert alert-info" in response
@ -566,11 +678,15 @@ def test_verify_stamp_csv_actions(user: UserClient, client: Client):
acer = yaml2json('acer.happy.battery.snapshot')
snapshot, _ = user.post(json_encode(acer), res=Snapshot)
device_id = snapshot['device']['id']
post_request = {"transaction": "ccc", "name": "John", "endUsers": 1,
"devices": [device_id], "description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00"
post_request = {
"transaction": "ccc",
"name": "John",
"endUsers": 1,
"devices": [device_id],
"description": "aaa",
"finalUserCode": "abcdefjhi",
"startTime": "2020-11-01T02:00:00+00:00",
"endTime": "2020-12-01T02:00:00+00:00",
}
user.post(res=Allocate, data=post_request)
@ -581,18 +697,21 @@ def test_verify_stamp_csv_actions(user: UserClient, client: Client):
acer['licence_version'] = '1.0.0'
snapshot, _ = client.post(acer, res=Live)
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})])
csv_str, _ = user.get(
res=documents.DocumentDef.t,
item='actions/',
accept='text/csv',
query=[('filter', {'type': ['Computer']})],
)
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')),
'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]},
status=200,
)
assert "alert alert-info" in response
@ -602,35 +721,38 @@ def test_verify_stamp_erasure_certificate(user: UserClient, client: Client):
s = file('erase-sectors.snapshot')
snapshot, response = user.post(s, res=Snapshot)
doc, _ = user.get(res=documents.DocumentDef.t,
item='erasures/',
query=[('filter', {'ids': [snapshot['device']['id']]})],
accept=ANY)
doc, _ = user.get(
res=documents.DocumentDef.t,
item='erasures/',
query=[('filter', {'ids': [snapshot['device']['id']]})],
accept=ANY,
)
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(doc, 'utf-8')),
'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(bytes(doc, 'utf-8')), 'example.csv')]},
status=200,
)
assert "alert alert-danger" in response
doc, _ = user.get(res=documents.DocumentDef.t,
item='erasures/',
query=[
('filter', {'ids': [snapshot['device']['id']]}),
('format', 'PDF')
],
accept='application/pdf')
doc, _ = user.get(
res=documents.DocumentDef.t,
item='erasures/',
query=[('filter', {'ids': [snapshot['device']['id']]}), ('format', 'PDF')],
accept='application/pdf',
)
response, _ = client.post(res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(doc),
'example.csv')]},
status=200)
response, _ = client.post(
res=documents.DocumentDef.t,
item='stamps/',
content_type='multipart/form-data',
accept='text/html',
data={'docUpload': [(BytesIO(doc), 'example.csv')]},
status=200,
)
assert "alert alert-info" in response
@ -639,11 +761,10 @@ def test_get_document_internal_stats(user: UserClient, user2: UserClient):
"""Tests for get the internal stats."""
# csv_str, _ = user.get(res=documents.DocumentDef.t,
# item='internalstats/')
csv_str, _ = user.get(res=documents.DocumentDef.t,
item='internalstats/',
accept='text/csv',
query=[])
# item='internalstats/')
csv_str, _ = user.get(
res=documents.DocumentDef.t, item='internalstats/', accept='text/csv', query=[]
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f)
@ -651,10 +772,9 @@ def test_get_document_internal_stats(user: UserClient, user2: UserClient):
assert len(export_csv) == 1
csv_str, _ = user2.get(res=documents.DocumentDef.t,
item='internalstats/',
accept='text/csv',
query=[])
csv_str, _ = user2.get(
res=documents.DocumentDef.t, item='internalstats/', accept='text/csv', query=[]
)
f = StringIO(csv_str)
obj_csv = csv.reader(f, f)
@ -675,8 +795,9 @@ def test_get_wbconf(user: UserClient):
assert 'WB_ERASE =' in env
# assert 'WB_ERASE = True' in env
session = Session.query.filter_by(user_id=user.user['id'],
type=SessionType.Internal).first()
session = Session.query.filter_by(
user_id=user.user['id'], type=SessionType.Internal
).first()
token = session.token
token = auth.Auth.encode(session.token)
assert token in env
@ -694,7 +815,7 @@ def test_trade_documents(user: UserClient):
'filename': 'test.pdf',
'hash': 'bbbbbbbb',
'url': 'http://www.ereuse.org/',
'lot': lot['id']
'lot': lot['id'],
}
doc, _ = user.post(res=TradeDocument, data=request_post)
assert doc['filename'] == request_post['filename']
@ -710,13 +831,15 @@ def test_trade_documents_with_weight(user: UserClient):
lot, _ = user.post({'name': 'MyLot'}, res=Lot)
# long url
url = 'http://www.ereuse.org/apapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapaaaa',
url = (
'http://www.ereuse.org/apapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapaaaa',
)
request_post = {
'filename': 'test.pdf',
'hash': 'bbbbbbbb',
'url': url,
'weight': 15,
'lot': lot['id']
'lot': lot['id'],
}
doc, _ = user.post(res=TradeDocument, data=request_post)
assert doc['weight'] == request_post['weight']

View File

@ -224,11 +224,12 @@ def test_export_devices(user3: UserClientFlask):
), 'Computer information are not equal'
assert fixture_csv[1][20] == export_csv[1][20], 'Computer information are not equal'
assert (
fixture_csv[1][22:82] == export_csv[1][22:82]
fixture_csv[1][22:83] == export_csv[1][22:83]
), 'Computer information are not equal'
assert fixture_csv[1][83] == export_csv[1][83], 'Computer information are not equal'
assert fixture_csv[1][84] == export_csv[1][84], 'Computer information are not equal'
assert (
fixture_csv[1][86:] == export_csv[1][86:]
fixture_csv[1][88:] == export_csv[1][88:]
), 'Computer information are not equal'
@ -842,3 +843,26 @@ def test_action_datawipe(user3: UserClientFlask):
assert dev.actions[-1].type == 'DataWipe'
assert 'Action &#34;DataWipe&#34; created successfully!' in body
assert dev.devicehub_id in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_wb_settings(user3: UserClientFlask):
uri = '/workbench/settings/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Download your settings for Workbench" in body
assert "Workbench Settings" in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_wb_settings_register(user3: UserClientFlask):
uri = '/workbench/settings/?opt=register'
body, status = user3.get(uri)
assert status == '200 OK'
assert "TOKEN = " in body
assert "URL = https://" in body
assert "/api/inventory/" in body