Merge branch 'testing' into changes/4025-new-hid

This commit is contained in:
Cayo Puigdefabregas 2022-12-01 15:59:52 +01:00
commit 39f19f676b
31 changed files with 1212 additions and 326 deletions

View file

@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
ml).
## testing
- [added] #414 add new vars in the settings file for wb.
## [2.5.0] - 2022-11-30
- [added] #407 erasure section with tabs in top.
- [added] #411 add new generic device as Other.
- [changed] #409 add backend pagination instead of javascript.
- [changed] #410 change teh top search for advanced search.
- [fixed] #412 show in snapshots log, type upload correctly.
- [fixed] #413 put order in documents.
- [fixed] #415 put prefix of lot in result of search.
## [2.4.3] - 2022-11-18
- [added] #386 add registration module.
- [added] #387 add template settings for Secure Erasure.
- [added] #397 add obada standard export.
- [added] #402 add reset password module.
- [added] #406 add orphans disks page.
- [changed] #391 add dhid in table and export of Erasure section.
- [changed] #395 change response for the new api to workbench.
- [changed] #396 modularize commands.
- [fixed] #388 lock update different motherboard with the same id.
- [fixed] #389 some datastorage without placeholder.
- [fixed] #390 fix image in form edit device.
- [fixed] #398 placeholder in new components.
- [fixed] #399 add api_host in config.
- [fixed] #401 db_host need to be api address.
- [fixed] #403 change delimiter in obada export.
- [fixed] #404 javascript select all devices.
- [fixed] #405 update pillow.
## [2.4.2] - 2022-10-18
- [added] #373 Enhancement - UX Lots.

View file

@ -30,7 +30,6 @@ from teal.enums import Country, Currency, Layouts, Subdivision
from teal.marshmallow import EnumField
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.schemas import Thing
project = 'Devicehub'
copyright = '2020, eReuse.org team'
@ -56,7 +55,7 @@ extensions = [
'sphinx.ext.viewcode',
'sphinxcontrib.plantuml',
'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc'
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
@ -126,15 +125,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -144,18 +140,20 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Devicehub.tex', 'Devicehub Documentation',
'eReuse.org team', 'manual'),
(
master_doc,
'Devicehub.tex',
'Devicehub Documentation',
'eReuse.org team',
'manual',
),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'devicehub', 'Devicehub Documentation',
[author], 1)
]
man_pages = [(master_doc, 'devicehub', 'Devicehub Documentation', [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@ -163,9 +161,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Devicehub', 'Devicehub Documentation',
author, 'Devicehub', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'Devicehub',
'Devicehub Documentation',
author,
'Devicehub',
'One line description of project.',
'Miscellaneous',
),
]
# -- Extension configuration -------------------------------------------------
@ -199,6 +203,7 @@ class DhlistDirective(Directive):
This requires :py:class:`ereuse_devicehub.resources.schemas.SchemaMeta`.
You will find in that module more information.
"""
has_content = False
# Definition of passed-in options
@ -216,7 +221,7 @@ class DhlistDirective(Directive):
sections = []
sections.append(self.links(things)) # Make index
for thng in things: # type: Thing
for thng in things:
# Generate a section for each class, with a title,
# fields description and a paragraph
section = n.section(ids=[self._id(thng)])
@ -228,7 +233,9 @@ class DhlistDirective(Directive):
for key, f in thng._own:
name = n.field_name(text=f.data_key or key)
body = [
self.parse('{} {}'.format(self.type(f), f.metadata.get('description', '')))
self.parse(
'{} {}'.format(self.type(f), f.metadata.get('description', ''))
)
]
if isinstance(f, EnumField):
body.append(self._parse_enum_field(f))
@ -244,6 +251,7 @@ class DhlistDirective(Directive):
def _parse_enum_field(self, f):
from ereuse_devicehub.resources.device import states
if issubclass(f.enum, (Subdivision, Currency, Country, Layouts, states.State)):
return self.parse(f.enum.__doc__)
else:
@ -298,7 +306,7 @@ class DhlistDirective(Directive):
def parse(self, text) -> n.container:
"""Parses text possibly containing ReST stuff and adds it in
a node."""
a node."""
p = n.container('')
self.state.nested_parse(StringList(string2lines(inspect.cleandoc(text))), 0, p)
return p

View file

@ -1 +1 @@
__version__ = "2.4.2"
__version__ = "2.5.0"

View file

@ -1,6 +1,5 @@
from distutils.version import StrictVersion
from itertools import chain
from typing import Set
from decouple import config
from teal.auth import TokenAuth
@ -44,7 +43,7 @@ class DevicehubConfig(Config):
import_resource(metric_def),
),
)
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
PASSWORD_SCHEMES = {'pbkdf2_sha256'}
SECRET_KEY = config('SECRET_KEY')
DB_USER = config('DB_USER', 'dhub')
DB_PASSWORD = config('DB_PASSWORD', 'ereuse')

View file

@ -1,7 +1,6 @@
import itertools
import json
from pathlib import Path
from typing import Set
import click
import click_spinner
@ -109,7 +108,7 @@ class Dummy:
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
print('done.')
sample_pc = None # We treat this one as a special sample for demonstrations
pcs = set() # type: Set[int]
pcs = set()
with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar:
for path in bar:
with path.open() as f:

View file

@ -50,11 +50,15 @@ from ereuse_devicehub.resources.device.models import (
Keyboard,
Laptop,
MemoryCardReader,
Monitor,
Mouse,
Other,
Placeholder,
Projector,
Server,
Smartphone,
Tablet,
TelevisionSet,
)
from ereuse_devicehub.resources.documents.models import DataWipeDocument
from ereuse_devicehub.resources.enums import Severity
@ -81,16 +85,23 @@ DEVICES = {
],
"Mobile, tablet & smartphone": [
"All Mobile",
"Mobile",
"Tablet",
"Smartphone",
"Cellphone",
],
"Drives & Storage": [
"All DataStorage",
"HardDrives",
"HardDrive",
"SolidStageDrive",
],
"Accessories": [
"All Accessories",
"Mouse",
"MemoryCardReader",
"SAI",
"Keyboard",
],
"Other Devices": ["Other"],
}
COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
@ -98,6 +109,8 @@ COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"]
STORAGE = ["HardDrive", "SolidStateDrive"]
ACCESSORIES = ["Mouse", "MemoryCardReader", "SAI", "Keyboard"]
OTHERS = ["Other"]
class AdvancedSearchForm(FlaskForm):
@ -170,7 +183,7 @@ class FilterForm(FlaskForm):
# Generic Filters
if "All Devices" == self.device_type:
filter_type = COMPUTERS + MONITORS + MOBILE
filter_type = COMPUTERS + MONITORS + MOBILE + OTHERS
elif "All Computers" == self.device_type:
filter_type = COMPUTERS
@ -184,6 +197,9 @@ class FilterForm(FlaskForm):
elif "All DataStorage" == self.device_type:
filter_type = STORAGE
elif "All Accessories" == self.device_type:
filter_type = ACCESSORIES
if filter_type:
self.devices = self.devices.filter(Device.type.in_(filter_type))
@ -374,10 +390,14 @@ class NewDeviceForm(FlaskForm):
"Tablet": Tablet,
"Cellphone": Cellphone,
"ComputerMonitor": ComputerMonitor,
"Monitor": Monitor,
"TelevisionSet": TelevisionSet,
"Projector": Projector,
"Mouse": Mouse,
"Keyboard": Keyboard,
"SAI": SAI,
"MemoryCardReader": MemoryCardReader,
"Other": Other,
}
def reset_from_obj(self):

View file

@ -55,15 +55,26 @@ devices = Blueprint('inventory', __name__, url_prefix='/inventory')
logger = logging.getLogger(__name__)
PER_PAGE = 20
class DeviceListMixin(GenericMixin):
template_name = 'inventory/device_list.html'
def get_context(self, lot_id=None, all_devices=False):
super().get_context()
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', PER_PAGE))
filter = request.args.get('filter', "All+Computers")
# import pdb; pdb.set_trace()
lots = self.context['lots']
form_filter = FilterForm(lots, lot_id, all_devices=all_devices)
devices = form_filter.search()
devices = form_filter.search().paginate(page=page, per_page=per_page)
devices.first = per_page * devices.page - per_page + 1
devices.last = len(devices.items) + devices.first - 1
lot = None
form_transfer = ''
form_delivery = ''
@ -92,6 +103,7 @@ class DeviceListMixin(GenericMixin):
'tags': self.get_user_tags(),
'list_devices': self.get_selected_devices(form_new_action),
'all_devices': all_devices,
'filter': filter,
}
)
@ -115,15 +127,43 @@ class DeviceListMixin(GenericMixin):
class ErasureListView(DeviceListMixin):
template_name = 'inventory/erasure_list.html'
def dispatch_request(self):
def dispatch_request(self, orphans=0):
self.get_context()
self.get_devices()
self.get_devices(orphans)
return flask.render_template(self.template_name, **self.context)
def get_devices(self):
def get_devices(self, orphans):
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', PER_PAGE))
erasure = EraseBasic.query.filter_by(author=g.user).order_by(
EraseBasic.created.desc()
)
if orphans:
schema = app.config.get('SCHEMA')
_user = g.user.id
sql = f"""
select action.id from {schema}.action as action
inner join {schema}.erase_basic as erase
on action.id=erase.id
inner join {schema}.device as device
on device.id=action.parent_id
inner join {schema}.placeholder as placeholder
on placeholder.binding_id=device.id
where (action.parent_id is null or placeholder.kangaroo=true)
and action.author_id='{_user}'
"""
ids = (e[0] for e in db.session.execute(sql))
erasure = (
EraseBasic.query.filter(EraseBasic.id.in_(ids))
.filter_by(author=g.user)
.order_by(EraseBasic.created.desc())
)
self.context['orphans'] = True
erasure = erasure.paginate(page=page, per_page=per_page)
erasure.first = per_page * erasure.page - per_page + 1
erasure.last = len(erasure.items) + erasure.first - 1
self.context['erasure'] = erasure
@ -1174,43 +1214,17 @@ class SnapshotListView(GenericMixin):
return flask.render_template(self.template_name, **self.context)
def get_snapshots_log(self):
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', PER_PAGE))
snapshots_log = SnapshotsLog.query.filter(
SnapshotsLog.owner == g.user
).order_by(SnapshotsLog.created.desc())
logs = {}
for snap in snapshots_log:
try:
system_uuid = snap.snapshot.device.system_uuid or ''
except AttributeError:
system_uuid = ''
if snap.snapshot_uuid not in logs:
logs[snap.snapshot_uuid] = {
'sid': snap.sid,
'snapshot_uuid': snap.snapshot_uuid,
'version': snap.version,
'device': snap.get_device(),
'system_uuid': system_uuid,
'status': snap.get_status(),
'severity': snap.severity,
'created': snap.created,
'type_device': snap.get_type_device(),
'original_dhid': snap.get_original_dhid(),
'new_device': snap.get_new_device(),
}
continue
if snap.created > logs[snap.snapshot_uuid]['created']:
logs[snap.snapshot_uuid]['created'] = snap.created
if snap.severity > logs[snap.snapshot_uuid]['severity']:
logs[snap.snapshot_uuid]['severity'] = snap.severity
logs[snap.snapshot_uuid]['status'] = snap.get_status()
result = sorted(logs.values(), key=lambda d: d['created'])
result.reverse()
return result
snapshots_log = snapshots_log.paginate(page=page, per_page=per_page)
snapshots_log.first = per_page * snapshots_log.page - per_page + 1
snapshots_log.last = len(snapshots_log.items) + snapshots_log.first - 1
return snapshots_log
class SnapshotDetailView(GenericMixin):
@ -1340,10 +1354,17 @@ class PlaceholderLogListView(GenericMixin):
return flask.render_template(self.template_name, **self.context)
def get_placeholders_log(self):
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', PER_PAGE))
placeholder_log = PlaceholdersLog.query.filter(
PlaceholdersLog.owner == g.user
).order_by(PlaceholdersLog.created.desc())
placeholder_log = placeholder_log.paginate(page=page, per_page=per_page)
placeholder_log.first = per_page * placeholder_log.page - per_page + 1
placeholder_log.last = len(placeholder_log.items) + placeholder_log.first - 1
return placeholder_log
@ -1452,3 +1473,7 @@ devices.add_url_rule(
devices.add_url_rule(
'/device/erasure/', view_func=ErasureListView.as_view('device_erasure_list')
)
devices.add_url_rule(
'/device/erasure/<int:orphans>/',
view_func=ErasureListView.as_view('device_erasure_list_orphans'),
)

View file

@ -0,0 +1,39 @@
"""device other
Revision ID: 410aadae7652
Revises: d65745749e34
Create Date: 2022-11-29 12:00:40.272121
"""
import sqlalchemy as sa
from alembic import context, op
# revision identifiers, used by Alembic.
revision = '410aadae7652'
down_revision = 'd65745749e34'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'other',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.device.id'],
),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_table('other', schema=f'{get_inv()}')

View file

@ -0,0 +1,35 @@
"""add settings_version to snapshots
Revision ID: af038a8a388c
Revises: 410aadae7652
Create Date: 2022-11-30 16:21:05.768024
"""
import citext
import sqlalchemy as sa
from alembic import context, op
# revision identifiers, used by Alembic.
revision = 'af038a8a388c'
down_revision = '410aadae7652'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.add_column(
'snapshot',
sa.Column('settings_version', citext.CIText(), nullable=True),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_column('snapshot', 'settings_version', schema=f'{get_inv()}')

View file

@ -4,7 +4,7 @@ from contextlib import suppress
from datetime import datetime
from fractions import Fraction
from math import hypot
from typing import Iterator, List, Optional, Type, TypeVar
from typing import Iterator, List, Optional, TypeVar
import dateutil.parser
from ereuse_utils import getter, text
@ -404,7 +404,7 @@ class Computer(Device):
chassis value.
"""
COMPONENTS = list(Component.__subclasses__()) # type: List[Type[Component]]
COMPONENTS = list(Component.__subclasses__())
COMPONENTS.remove(Motherboard)
def __init__(self, node: dict) -> None:

View file

@ -73,11 +73,25 @@ class SnapshotsLog(Thing):
snapshots = []
for s in self.snapshot.device.actions:
if s == self.snapshot:
continue
break
if s.type == self.snapshot.type:
snapshots.append(s)
return snapshots and 'Update' or 'New Device'
def get_system_uuid(self):
try:
return self.snapshot.device.system_uuid or ''
except AttributeError:
return ''
def get_version(self):
if not self.snapshot:
return self.version
settings_version = self.snapshot.settings_version or ''
settings_version = "".join([x[0] for x in settings_version.split(' ') if x])
return "{} ({})".format(self.version, settings_version)
class PlaceholdersLog(Thing):
"""A Placeholder log."""

View file

@ -1,7 +1,6 @@
from datetime import datetime, timezone
from typing import List
from ereuse_workbench.computer import Component, Computer, DataStorage
from ereuse_workbench.computer import Computer, DataStorage
from ereuse_workbench.utils import Dumpeable
@ -24,8 +23,8 @@ class Snapshot(Dumpeable):
self.endTime = datetime.now(timezone.utc)
self.closed = False
self.elapsed = None
self.device = None # type: Computer
self.components = None # type: List[Component]
self.device = None
self.components = None
self._storages = None
def computer(self):

View file

@ -676,6 +676,7 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
of time it took to complete.
"""
sid = Column(CIText(), nullable=True)
settings_version = Column(CIText(), nullable=True)
is_server_erase = Column(Boolean(), nullable=True)
def get_last_lifetimes(self):

View file

@ -453,6 +453,7 @@ class Snapshot(ActionWithOneDevice):
'Order is preserved, so the component num 0 when'
'submitting is the component num 0 when returning it back.',
)
settings_version = String(required=False)
@validates_schema
def validate_workbench_version(self, data: dict):

View file

@ -4,7 +4,11 @@ from teal.resource import Converters, Resource
from ereuse_devicehub.resources.device import schemas
from ereuse_devicehub.resources.device.models import Manufacturer
from ereuse_devicehub.resources.device.views import DeviceView, DeviceMergeView, ManufacturerView
from ereuse_devicehub.resources.device.views import (
DeviceMergeView,
DeviceView,
ManufacturerView,
)
class DeviceDef(Resource):
@ -13,25 +17,42 @@ class DeviceDef(Resource):
ID_CONVERTER = Converters.string
AUTH = False # We manage this at each view
def __init__(self, app,
import_name=__name__,
static_folder='static',
static_url_path=None,
template_folder='templates',
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder='static',
static_url_path=None,
template_folder='templates',
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
device_merge = DeviceMergeView.as_view('merge-devices', definition=self, auth=app.auth)
device_merge = DeviceMergeView.as_view(
'merge-devices', definition=self, auth=app.auth
)
if self.AUTH:
device_merge = app.auth.requires_auth(device_merge)
path = '/<{value}:dev1_id>/merge/<{value}:dev2_id>'.format(value=self.ID_CONVERTER.value)
path = '/<{value}:dev1_id>/merge/<{value}:dev2_id>'.format(
value=self.ID_CONVERTER.value
)
# self.add_url_rule(path, view_func=device_merge, methods={'POST'})
@ -40,11 +61,31 @@ class ComputerDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Computer
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class DesktopDef(ComputerDef):
@ -66,11 +107,31 @@ class MonitorDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Monitor
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class ComputerMonitorDef(MonitorDef):
@ -83,15 +144,40 @@ class TelevisionSetDef(MonitorDef):
SCHEMA = schemas.TelevisionSet
class ProjectorDef(MonitorDef):
VIEW = None
SCHEMA = schemas.Projector
class MobileDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Mobile
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class SmartphoneDef(MobileDef):
@ -113,11 +199,31 @@ class ComponentDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Component
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class GraphicCardDef(ComponentDef):
@ -184,11 +290,31 @@ class ComputerAccessoryDef(DeviceDef):
VIEW = None
SCHEMA = schemas.ComputerAccessory
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class MouseDef(ComputerAccessoryDef):
@ -215,11 +341,31 @@ class NetworkingDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Networking
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class RouterDef(NetworkingDef):
@ -246,11 +392,31 @@ class PrinterDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Printer
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class LabelPrinterDef(PrinterDef):
@ -262,11 +428,31 @@ class SoundDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Sound
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class MicrophoneDef(SoundDef):
@ -278,11 +464,31 @@ class VideoDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Video
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class VideoScalerDef(VideoDef):
@ -299,11 +505,31 @@ class CookingDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Cooking
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class Mixer(CookingDef):
@ -315,11 +541,31 @@ class DIYAndGardeningDef(DeviceDef):
VIEW = None
SCHEMA = schemas.DIYAndGardening
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class DrillDef(DIYAndGardeningDef):
@ -331,22 +577,62 @@ class PackOfScrewdriversDef(DIYAndGardeningDef):
VIEW = None
SCHEMA = schemas.PackOfScrewdrivers
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class HomeDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Home
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class DehumidifierDef(HomeDef):
@ -363,11 +649,31 @@ class RecreationDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Recreation
def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
class BikeDef(RecreationDef):
@ -389,3 +695,35 @@ class ManufacturerDef(Resource):
"""Loads the manufacturers to the database."""
if exclude_schema != 'common':
Manufacturer.add_all_to_session(db.session)
class OtherDef(DeviceDef):
VIEW = None
SCHEMA = schemas.Computer
SCHEMA = schemas.Other
def __init__(
self,
app,
import_name=__name__,
static_folder=None,
static_url_path=None,
template_folder=None,
url_prefix=None,
subdomain=None,
url_defaults=None,
root_path=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)

View file

@ -635,6 +635,14 @@ class Device(Thing):
return self.binding.device.devicehub_id
return self.devicehub_id
@property
def my_partner(self):
if self.placeholder and self.placeholder.binding:
return self.placeholder.binding
if self.binding:
return self.binding.device
return self
@property
def get_updated(self):
if self.placeholder and self.placeholder.binding:
@ -1394,6 +1402,19 @@ class DataStorage(JoinedComponentTableMixin, Component):
except LookupError:
return None
@property
def orphan(self):
if not self.parent:
return True
if self.parent.placeholder and self.parent.placeholder.kangaroo:
return True
if self.parent.binding and self.parent.binding.kangaroo:
return True
return False
class HardDrive(DataStorage):
pass
@ -1684,3 +1705,11 @@ def create_code_tag(mapper, connection, device):
# from flask_sqlalchemy import event
# event.listen(Device, 'after_insert', create_code_tag, propagate=True)
class Other(Device):
"""
Used for put in there all devices than not have actualy a class
"""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)

View file

@ -270,6 +270,10 @@ class TelevisionSet(Monitor):
__doc__ = m.TelevisionSet.__doc__
class Projector(Monitor):
__doc__ = m.Projector.__doc__
class Mobile(Device):
__doc__ = m.Mobile.__doc__
@ -578,3 +582,7 @@ class Bike(Recreation):
class Racket(Recreation):
pass
class Other(Device):
pass

View file

@ -306,7 +306,7 @@ class LotDeviceView(LotBaseChildrenView):
dev_qry = Device.query.filter(Device.id.in_(ids)).filter(Device.owner == g.user)
for dev in dev_qry:
if isinstance(dev, DataStorage) and dev.parent:
if isinstance(dev, DataStorage) and not dev.orphan:
continue
devices.add(dev)

View file

@ -172,7 +172,9 @@ class TradeDocument(Thing):
return sorted(ev for ev in actions if ev.severity >= Severity.Warning)
def __lt__(self, other):
return self.id < other.id
if self.id and other.id:
return self.id < other.id
return False
def __str__(self) -> str:
return '{0.file_name}'.format(self)

View file

@ -48,12 +48,12 @@
/**
* Search bar toggle
*/
if (select(".search-bar-toggle")) {
on("click", ".search-bar-toggle", (e) => {
select(".search-bar").classList.toggle("search-bar-show")
})
}
*/
/**
* Navbar links active state on scroll

View file

@ -8,6 +8,12 @@
</div><!-- End Logo -->
<div class="search-bar">
<form class="search-form d-flex align-items-center" method="get" action="/inventory/search/">
<input class="" type="text" name="q" placeholder="Search" title="Enter search keyword">
<button type="submit" title="Search"><i class="bi bi-search"></i></button>
</form>
</div><!-- End Search Bar -->
<div class="search-bar d-none">
<form class="search-form d-flex align-items-center" method="" id="SearchForm" action="#">
<input class="dropdown-toggle" type="text" name="query" placeholder="Search" title="Enter search keyword"
autocomplete="off" id="dropdownSearch" data-bs-toggle="dropdown" aria-expanded="false">

View file

@ -46,7 +46,13 @@
</optgroup>
<optgroup label="Monitor">
<option value="ComputerMonitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Computer Monitor</option>
{% if form.type.data == 'ComputerMonitor' %} selected="selected"{% endif %}>Computer Monitor</option>
<option value="Monitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Monitor</option>
<option value="TelevisionSet"
{% if form.type.data == 'TelevisionSet' %} selected="selected"{% endif %}>TelevisionSet</option>
<option value="Projector"
{% if form.type.data == 'Projector' %} selected="selected"{% endif %}>Projector</option>
</optgroup>
<optgroup label="Mobile">
<option value="Smartphone"
@ -66,6 +72,10 @@
<option value="Keyboard"
{% if form.type.data == 'Keyboard' %} selected="selected"{% endif %}>Keyboard</option>
</optgroup>
<optgroup label="Other Type of Device">
<option value="Other"
{% if form.type.data == 'Other' %} selected="selected"{% endif %}>Other</option>
</optgroup>
</select>
<small class="text-muted form-text">Type of devices</small>
{% if form.type.errors %}

View file

@ -335,6 +335,8 @@
{% for f in form_filter %}
{{ f }}
{% endfor %}
<input type="hidden" class="d-none" value="1" name="page" />
<input type="hidden" class="d-none" value="{{ devices.per_page }}" name="per_page" />
<input type="submit" class="ms-2 btn btn-primary" value="Filter" />
</div>
</form>
@ -344,6 +346,38 @@
<em>{{ form_filter.filter.data or "Computer" }}</em>
</p>
<div class="dataTable-top" style="float: left;">
<div class="dataTable-dropdown">
<label>
<select class="dataTable-selector">
<option value="5"{% if devices.per_page == 5 %} selected="selected"{% endif %}>
5
</option>
<option value="10"{% if devices.per_page == 10 %} selected="selected"{% endif %}>
10
</option>
<option value="15"{% if devices.per_page == 15 %} selected="selected"{% endif %}>
15
</option>
<option value="20"{% if devices.per_page == 20 %} selected="selected"{% endif %}>
20
</option>
<option value="25"{% if devices.per_page == 25 %} selected="selected"{% endif %}>
25
</option>
<option value="50"{% if devices.per_page == 50 %} selected="selected"{% endif %}>
50
</option>
<option value="100"{% if devices.per_page == 100 %} selected="selected"{% endif %}>
100
</option>
</select> entries per page
</label>
</div>
<div class="dataTable-search">
</div>
</div>
<div class="dataTable-container">
<table class="table">
<thead>
<tr>
@ -362,7 +396,7 @@
</tr>
</thead>
<tbody>
{% for dev in devices %}
{% for dev in devices.items %}
{% if dev.placeholder and (not dev.parent_id or dev.parent.placeholder.kangaroo) %}
<tr>
<td>
@ -421,6 +455,57 @@
{% endfor %}
</tbody>
</table>
<div class="dataTable-bottom">
<div class="dataTable-info">
Showing {{ devices.first }} to {{ devices.last }} of {{ devices.total }} entries
</div>
<nav class="dataTable-pagination">
<ul class="dataTable-pagination-list">
{% if devices.has_prev %}
<li class="pager">
{% if all_devices %}
<a href="{{ url_for('inventory.alldevicelist', page=devices.prev_num, per_page=devices.per_page, filter=filter) }}"></a>
{% elif lot %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id, page=devices.prev_num, per_page=devices.per_page, filter=filter) }}"></a>
{% else %}
<a href="{{ url_for('inventory.devicelist', page=devices.prev_num, per_page=devices.per_page, filter=filter) }}"></a>
{% endif %}
</li>
{% endif %}
{% for page in devices.iter_pages() %}
{% if page %}
{% if page == devices.page %}
<li class="active"><a href="javascript:void()">{{ page }}</a></li>
{% else %}
<li class="">
{% if all_devices %}
<a href="{{ url_for('inventory.alldevicelist', page=page, per_page=devices.per_page, filter=filter) }}">
{% elif lot %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id, page=page, per_page=devices.per_page, filter=filter) }}">
{% else %}
<a href="{{ url_for('inventory.devicelist', page=page, per_page=devices.per_page, filter=filter) }}">
{% endif %}
{{ page }}
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if devices.has_next %}
<li class="pager">
{% if all_devices %}
<a href="{{ url_for('inventory.alldevicelist', page=devices.next_num, per_page=devices.per_page, filter=filter) }}"></a>
{% elif lot %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id, page=devices.next_num, per_page=devices.per_page, filter=filter) }}"></a>
{% else %}
<a href="{{ url_for('inventory.devicelist', page=devices.next_num, per_page=devices.per_page, filter=filter) }}"></a>
{% endif %}
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
</div>
@ -592,10 +677,25 @@
{% include "inventory/alert_lots_changes.html" %}
<!-- Custom Code -->
<script>
$(document).ready(() => {
$(".dataTable-selector").on("change", function() {
const per_page = $('.dataTable-selector').val();
{% if all_devices %}
window.location.href = "{{ url_for('inventory.alldevicelist', page=1) }}&filter={{ filter }}&per_page="+per_page;
{% elif lot %}
window.location.href = "{{ url_for('inventory.lotdevicelist', lot_id=lot.id, page=1) }}&filter={{ filter }}&per_page="+per_page;
{% else %}
window.location.href = "{{ url_for('inventory.devicelist', page=1) }}&filter={{ filter }}&per_page="+per_page;
{% endif %}
});
});
</script>
<script>
let table = new simpleDatatables.DataTable("table", {
perPageSelect: [5, 10, 15, 20, 25, 50, 100],
perPage: 20
footer: false,
paging: false,
})
</script>
{% if config['DEBUG'] %}

View file

@ -18,9 +18,54 @@
<div class="card">
<div class="card-body pt-3" style="min-height: 650px;">
<div class="tab-content pt-1">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<a href="{{ url_for('inventory.device_erasure_list') }}" class="nav-link{% if not orphans %} active{% endif %}">
All hard drives
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('inventory.device_erasure_list_orphans', orphans=1) }}" class="nav-link{% if orphans %} active{% endif %}">
Hard drives without device
</a>
</li>
</ul>
<div class="tab-content pt-2">
<div id="devices-list" class="tab-pane fade devices-list active show">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
{% if orphans %}
<div class="btn-group dropdown ml-1">
<button id="btnLots" type="button" onclick="processSelectedDevices()" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-folder2"></i>
Lots
<span class="caret"></span>
</button>
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnLots" id="dropDownLotsSelector">
<div class="row w-100">
<div class="input-group mb-3 mx-2">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1"><i class="bi bi-search"></i></span>
</div>
<input type="text" class="form-control" id="lots-search" placeholder="search" aria-label="search" aria-describedby="basic-addon1">
</div>
</div>
<h6 class="dropdown-header">Select lots where to store the selected devices</h6>
<ul class="mx-3" id="LotsSelector"></ul>
<li><hr /></li>
<li>
<a href="#" class="dropdown-item" id="ApplyDeviceLots">
<i class="bi bi-check"></i>
Apply
</a>
</li>
</ul>
</div>
{% endif %}
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-reply"></i>
@ -42,8 +87,65 @@
</li>
</ul>
</div>
{% if orphans %}
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
Labels
</button>
<ul class="dropdown-menu" aria-labelledby="btnTags">
<li>
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
{% for f in form_print_labels %}
{{ f }}
{% endfor %}
<a href="javascript:$('#print_labels').submit()" class="dropdown-item">
<i class="bi bi-printer"></i>
Print labels
</a>
</form>
</li>
</ul>
</div>
{% endif %}
<div id="select-devices-info" class="alert alert-info mb-0 mt-3 d-none" role="alert">
If this text is showing is because there are an error
</div>
<div class="tab-content pt-2">
<div class="dataTable-top" style="float: left;">
<div class="dataTable-dropdown">
<label>
<select class="dataTable-selector">
<option value="5"{% if erasure.per_page == 5 %} selected="selected"{% endif %}>
5
</option>
<option value="10"{% if erasure.per_page == 10 %} selected="selected"{% endif %}>
10
</option>
<option value="15"{% if erasure.per_page == 15 %} selected="selected"{% endif %}>
15
</option>
<option value="20"{% if erasure.per_page == 20 %} selected="selected"{% endif %}>
20
</option>
<option value="25"{% if erasure.per_page == 25 %} selected="selected"{% endif %}>
25
</option>
<option value="50"{% if erasure.per_page == 50 %} selected="selected"{% endif %}>
50
</option>
<option value="100"{% if erasure.per_page == 100 %} selected="selected"{% endif %}>
100
</option>
</select> entries per page
</label>
</div>
<div class="dataTable-search">
</div>
</div>
<div class="dataTable-container">
<table class="table">
<thead>
<tr>
@ -59,10 +161,10 @@
</tr>
</thead>
<tbody>
{% for ac in erasure %}
{% for ac in erasure.items %}
<tr>
<td>
<input type="checkbox" class="deviceSelect" data="{{ ac.device.id }}"
<input type="checkbox" class="deviceSelect" data="{{ ac.device.my_partner.id }}"
data-device-type="{{ ac.device.type }}" data-device-manufacturer="{{ ac.device.manufacturer }}"
data-device-dhid="{{ ac.device.dhid }}" data-device-vname="{{ ac.device.verbose_name }}"
data-action-erasure="{{ ac.id }}"
@ -74,11 +176,24 @@
<td>
{% if ac.device.phid() %}
<a href="{{ url_for('inventory.device_details', id=ac.device.dhid)}}">
{% if ac.device.get_type_logo() %}
<i class="{{ ac.device.get_type_logo() }}" title="{{ ac.device.type }}"></i>
{% endif %}
{{ ac.device.serial_number.upper() }}
</a>
{% else %}
{% if ac.device.get_type_logo() %}
<i class="{{ ac.device.get_type_logo() }}" title="{{ ac.device.type }}"></i>
{% endif %}
{{ ac.device.serial_number.upper() }}
{% endif %}
{% if ac.device.my_partner.lots | length > 0 %}
<h6 class="d-inline">
{% for lot in ac.device.my_partner.get_lots_for_template() %}
<span class="badge rounded-pill bg-light text-dark">{{ lot }}</span>
{% endfor %}
</h6>
{% endif %}
</td>
<td>
{% if ac.device.phid() %}
@ -91,7 +206,7 @@
</td>
<td>
<a href="{{ url_for('inventory.export', export_id='snapshot') }}?id={{ ac.snapshot.uuid }}">
{{ ac.snapshot.uuid }}
{{ ac.snapshot.uuid }}
</a>
</td>
<td>
@ -115,135 +230,54 @@
{% endfor %}
</tbody>
</table>
</div>
<div class="dataTable-bottom">
<div class="dataTable-info">
Showing {{ erasure.first }} to {{ erasure.last }} of {{ erasure.total }} entries
</div>
<nav class="dataTable-pagination">
<ul class="dataTable-pagination-list">
{% if erasure.has_prev %}
<li class="pager">
{% if orphans %}
<a href="{{ url_for('inventory.device_erasure_list_orphans', orphans=1, page=erasure.prev_num, per_page=erasure.per_page) }}"></a>
{% else %}
<a href="{{ url_for('inventory.device_erasure_list', page=erasure.prev_num, per_page=erasure.per_page) }}"></a>
{% endif %}
</li>
{% endif %}
{% for page in erasure.iter_pages() %}
{% if page %}
{% if page == erasure.page %}
<li class="active"><a href="javascript:void()">{{ page }}</a></li>
{% else %}
<li class="">
{% if orphans %}
<a href="{{ url_for('inventory.device_erasure_list_orphans', orphans=1, page=page, per_page=erasure.per_page) }}">
{{ page }}
</a>
{% else %}
<a href="{{ url_for('inventory.device_erasure_list', page=page, per_page=erasure.per_page) }}">
{{ page }}
</a>
{% endif %}
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if erasure.has_next %}
<li class="pager">
{% if orphans %}
<a href="{{ url_for('inventory.device_erasure_list_orphans', orphans=1, page=erasure.next_num, per_page=erasure.per_page) }}"></a>
{% else %}
<a href="{{ url_for('inventory.device_erasure_list', page=erasure.next_num, per_page=erasure.per_page) }}"></a>
{% endif %}
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% if lot and not lot.is_temporary %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Uploaded on</th>
</tr>
</thead>
<tbody>
{% for doc in lot.trade.documents %}
<tr>
<td>
{% if doc.url %}
<a href="{{ doc.url.to_text() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%H:%M %d-%m-%Y')}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="edit-transfer" class="tab-pane fade edit-transfer">
<h5 class="card-title">Transfer</h5>
<form method="post" action="{{ url_for('inventory.edit_transfer', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_transfer.csrf_token }}
{% for field in form_transfer %}
{% if field != form_transfer.csrf_token %}
<div class="col-12">
{% if field != form_transfer.type %}
{{ field.label(class_="form-label") }}
{% if field == form_transfer.code %}
<span class="text-danger">*</span>
{% endif %}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<div id="edit-delivery-note" class="tab-pane fade edit-delivery-note">
<h5 class="card-title">Delivery Note</h5>
<form method="post" action="{{ url_for('inventory.delivery_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_delivery.csrf_token }}
{% for field in form_delivery %}
{% if field != form_delivery.csrf_token %}
<div class="col-12">
{% if field != form_delivery.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
<div id="edit-receiver-note" class="tab-pane fade edit-receiver-note">
<h5 class="card-title">Receiver Note</h5>
<form method="post" action="{{ url_for('inventory.receiver_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_receiver.csrf_token }}
{% for field in form_receiver %}
{% if field != form_receiver.csrf_token %}
<div class="col-12">
{% if field != form_receiver.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
{% endif %}
</div><!-- End Bordered Tabs -->
</div>
@ -255,21 +289,32 @@
</div>
</div>
</section>
{% include "inventory/lot_delete_modal.html" %}
{% include "inventory/actions.html" %}
{% include "inventory/allocate.html" %}
{% include "inventory/data_wipe.html" %}
{% include "inventory/trade.html" %}
{% include "inventory/alert_export_error.html" %}
{% include "inventory/alert_lots_changes.html" %}
<!-- Custom Code -->
<script>
$(document).ready(() => {
$(".dataTable-selector").on("change", function() {
const per_page = $('.dataTable-selector').val();
{% if orphans %}
window.location.href = "{{ url_for('inventory.device_erasure_list_orphans', orphans=1, page=1) }}&per_page="+per_page;
{% else %}
window.location.href = "{{ url_for('inventory.device_erasure_list', page=1) }}&per_page="+per_page;
{% endif %}
});
});
</script>
<script>
let table = new simpleDatatables.DataTable("table", {
perPageSelect: [5, 10, 15, 20, 25, 50, 100],
perPage: 20
//perPageSelect: [5, 10, 15, 20, 25, 50, 100],
//perPage: 20,
footer: false,
paging: false,
})
</script>
{% if config['DEBUG'] %}
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% else %}

View file

@ -22,6 +22,38 @@
<div class="tab-content pt-5">
<div id="devices-list" class="tab-pane fade devices-list active show">
<div class="tab-content pt-2">
<div class="dataTable-top" style="float: left;">
<div class="dataTable-dropdown">
<label>
<select class="dataTable-selector">
<option value="5"{% if placeholders_log.per_page == 5 %} selected="selected"{% endif %}>
5
</option>
<option value="10"{% if placeholders_log.per_page == 10 %} selected="selected"{% endif %}>
10
</option>
<option value="15"{% if placeholders_log.per_page == 15 %} selected="selected"{% endif %}>
15
</option>
<option value="20"{% if placeholders_log.per_page == 20 %} selected="selected"{% endif %}>
20
</option>
<option value="25"{% if placeholders_log.per_page == 25 %} selected="selected"{% endif %}>
25
</option>
<option value="50"{% if placeholders_log.per_page == 50 %} selected="selected"{% endif %}>
50
</option>
<option value="100"{% if placeholders_log.per_page == 100 %} selected="selected"{% endif %}>
100
</option>
</select> entries per page
</label>
</div>
<div class="dataTable-search">
</div>
</div>
<div class="dataTable-container">
<table class="table">
<thead>
<tr>
@ -34,7 +66,7 @@
</tr>
</thead>
<tbody>
{% for log in placeholders_log %}
{% for log in placeholders_log.items %}
<tr>
<td>
{{ log.phid }}
@ -58,6 +90,38 @@
{% endfor %}
</tbody>
</table>
<div class="dataTable-bottom">
<div class="dataTable-info">
Showing {{ placeholders_log.first }} to {{ placeholders_log.last }} of {{ placeholders_log.total }} entries
</div>
<nav class="dataTable-pagination">
<ul class="dataTable-pagination-list">
{% if placeholders_log.has_prev %}
<li class="pager">
<a href="{{ url_for('inventory.placeholder_logs', page=placeholders_log.prev_num, per_page=placeholders_log.per_page) }}"></a>
</li>
{% endif %}
{% for page in placeholders_log.iter_pages() %}
{% if page %}
{% if page == placeholders_log.page %}
<li class="active"><a href="javascript:void()">{{ page }}</a></li>
{% else %}
<li class="">
<a href="{{ url_for('inventory.placeholder_logs', page=page, per_page=placeholders_log.per_page) }}">
{{ page }}
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if placeholders_log.has_next %}
<li class="pager">
<a href="{{ url_for('inventory.placeholder_logs', page=placeholders_log.next_num, per_page=placeholders_log.per_page) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
@ -75,6 +139,18 @@
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
$(document).ready(() => {
$(".dataTable-selector").on("change", function() {
const per_page = $('.dataTable-selector').val();
window.location.href = "{{ url_for('inventory.placeholder_logs', page=1) }}&per_page="+per_page;
});
});
</script>
<script>
let table = new simpleDatatables.DataTable("table", {
footer: false,
paging: false,
})
</script>
{% endblock main %}

View file

@ -16,7 +16,7 @@
<div class="col-xl-12">
<div class="card">
<div class="card d-none">
<div class="tab-content pt-2">
<div class="flex mt-4 mb-4">
<form method="get" class="ms-4">
@ -307,8 +307,8 @@
</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>
{% for lot in dev.get_lots_for_template() %}
<span class="badge rounded-pill bg-light text-dark">{{ lot }}</span>
{% endfor %}
</h6>
{% endif %}

View file

@ -22,6 +22,38 @@
<div class="tab-content pt-5">
<div id="devices-list" class="tab-pane fade devices-list active show">
<div class="tab-content pt-2">
<div class="dataTable-top" style="float: left;">
<div class="dataTable-dropdown">
<label>
<select class="dataTable-selector">
<option value="5"{% if snapshots_log.per_page == 5 %} selected="selected"{% endif %}>
5
</option>
<option value="10"{% if snapshots_log.per_page == 10 %} selected="selected"{% endif %}>
10
</option>
<option value="15"{% if snapshots_log.per_page == 15 %} selected="selected"{% endif %}>
15
</option>
<option value="20"{% if snapshots_log.per_page == 20 %} selected="selected"{% endif %}>
20
</option>
<option value="25"{% if snapshots_log.per_page == 25 %} selected="selected"{% endif %}>
25
</option>
<option value="50"{% if snapshots_log.per_page == 50 %} selected="selected"{% endif %}>
50
</option>
<option value="100"{% if snapshots_log.per_page == 100 %} selected="selected"{% endif %}>
100
</option>
</select> entries per page
</label>
</div>
<div class="dataTable-search">
</div>
</div>
<div class="dataTable-container">
<table class="table">
<thead>
<tr>
@ -29,7 +61,6 @@
<th scope="col">Snapshot UUID</th>
<th scope="col">Version</th>
<th scope="col">DHID</th>
<th scope="col">System UUID</th>
<th scope="col">Status</th>
<th scope="col">Type Upload</th>
<th scope="col">Type Device</th>
@ -39,7 +70,7 @@
</tr>
</thead>
<tbody>
{% for snap in snapshots_log %}
{% for snap in snapshots_log.items %}
<tr>
<td>
{% if snap.sid and snap.snapshot_uuid %}
@ -56,29 +87,26 @@
{% endif %}
</td>
<td>
{{ snap.version }}
{{ snap.get_version() }}
</td>
<td>
{% if snap.device %}
{% if snap.get_device() %}
<a href="{{ url_for('inventory.device_details', id=snap.device) }}">
{{ snap.device }}
{{ snap.get_device() }}
</a>
{% endif %}
</td>
<td>
{{ snap.system_uuid }}
{{ snap.get_status() }}
</td>
<td>
{{ snap.status }}
{{ snap.get_new_device() }}
</td>
<td>
{{ snap.new_device }}
{{ snap.get_type_device() }}
</td>
<td>
{{ snap.type_device }}
</td>
<td>
{{ snap.original_dhid }}
{{ snap.get_original_dhid() }}
</td>
<td>{{ snap.created.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
@ -93,6 +121,38 @@
</tbody>
</table>
<div class="dataTable-bottom">
<div class="dataTable-info">
Showing {{ snapshots_log.first }} to {{ snapshots_log.last }} of {{ snapshots_log.total }} entries
</div>
<nav class="dataTable-pagination">
<ul class="dataTable-pagination-list">
{% if snapshots_log.has_prev %}
<li class="pager">
<a href="{{ url_for('inventory.snapshotslist', page=snapshots_log.prev_num, per_page=snapshots_log.per_page) }}"></a>
</li>
{% endif %}
{% for page in snapshots_log.iter_pages() %}
{% if page %}
{% if page == snapshots_log.page %}
<li class="active"><a href="javascript:void()">{{ page }}</a></li>
{% else %}
<li class="">
<a href="{{ url_for('inventory.snapshotslist', page=page, per_page=snapshots_log.per_page) }}">
{{ page }}
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if snapshots_log.has_next %}
<li class="pager">
<a href="{{ url_for('inventory.snapshotslist', page=snapshots_log.next_num, per_page=snapshots_log.per_page) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
@ -109,6 +169,18 @@
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
$(document).ready(() => {
$(".dataTable-selector").on("change", function() {
const per_page = $('.dataTable-selector').val();
window.location.href = "{{ url_for('inventory.snapshotslist', page=1) }}&per_page="+per_page;
});
});
</script>
<script>
let table = new simpleDatatables.DataTable("table", {
footer: false,
paging: false,
})
</script>
{% endblock main %}

View file

@ -32,7 +32,7 @@
</div>
<div class="row">
<div class="col qr">
<div id="{{ dev.devicehub_id }}"></div>
<div id="{{ dev.dhid }}"></div>
</div>
<div class="col dhid">
<div style="padding-top: 55px">
@ -41,7 +41,7 @@
data-model="{{ dev.model or '' }}"
data-tags="{{ dev.list_tags() }}"
data-phid="{{ dev.phid() }}"
data-sid="{{ dev.sid or '' }}">{{ dev.devicehub_id }}</b>
data-sid="{{ dev.sid or '' }}">{{ dev.dhid }}</b>
</div>
</div>
</div>
@ -192,7 +192,7 @@
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
<script type="text/javascript">
{% for dev in devices %}
qr_draw("{{ dev.public_link }}", "#{{ dev.devicehub_id }}")
qr_draw("{{ dev.public_link }}", "#{{ dev.dhid }}")
{% endfor %}
</script>
{% endblock main %}

View file

@ -14,8 +14,8 @@ WB_SMART_TEST = short
WB_ERASE = EraseBasic
WB_ERASE_STEPS = 1
WB_ERASE_LEADING_ZEROS = False
VERSION = "Basic Erasure (BE)"
WB_DEBUG = True
{% elif baseline_erease %}
DH_HOST = {{ api_host }}
DH_DATABASE = {{ schema }}
@ -28,6 +28,11 @@ WB_SMART_TEST = short
WB_ERASE = EraseSectors
WB_ERASE_STEPS = {{ erase_steps }}
WB_ERASE_LEADING_ZEROS = True
VERSION = {%if erase_steps < 3 %}"Baseline Secure Erasure (BSE)"{% else %}"Enhanced Secure Erasure (ESE)"{% endif %}
WB_DEBUG = True
{% endif %}
{% else %}
SNAPSHOTS_PATH = /mnt
LOGS_PATH = /mnt
VERSION = "Basic Metadata (BM)"
{% endif %}

View file

@ -56,6 +56,7 @@ def test_api_docs(client: Client):
'/inventory/device/{id}/',
'/inventory/device/{dhid}/binding/',
'/inventory/device/erasure/',
'/inventory/device/erasure/{orphans}/',
'/inventory/all/device/',
'/inventory/export/{export_id}/',
'/inventory/lot/add/',
@ -116,4 +117,4 @@ def test_api_docs(client: Client):
'scheme': 'basic',
'name': 'Authorization',
}
assert len(docs['definitions']) == 132
assert len(docs['definitions']) == 134

View file

@ -2368,6 +2368,9 @@ def test_upload_snapshot_smartphone(user3: UserClientFlask):
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_list_erasures(user3: UserClientFlask):
from flask import current_app as app
app.config['SCHEMA'] = 'test'
uri = '/inventory/upload-snapshot/'
file_name = 'erase-sectors-2-hdd.snapshot.yaml'
body, status = user3.get(uri)
@ -2393,6 +2396,19 @@ def test_list_erasures(user3: UserClientFlask):
assert status == '200 OK'
assert txt in body
uri = '/inventory/device/erasure/1/'
body, status = user3.get(uri)
assert status == '200 OK'
assert txt not in body
dev = Device.query.first()
dev.binding.kangaroo = True
db.session.commit()
body, status = user3.get(uri)
assert status == '200 OK'
assert txt in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
@ -2454,6 +2470,7 @@ def test_bug_3831_documents(user3: UserClientFlask):
assert 'Delete Lot' in body
assert 'Incoming Lot' in body
lot_id = Lot.query.all()[1].id
uri = f'/inventory/lot/{lot_id}/trade-document/add/'
body, status = user3.get(uri)
@ -2471,8 +2488,16 @@ def test_bug_3831_documents(user3: UserClientFlask):
}
uri = f'/inventory/lot/{lot_id}/trade-document/add/'
# body, status = user3.post(uri, data=data, content_type="multipart/form-data")
# assert status == '200 OK'
body, status = user3.post(uri, data=data, content_type="multipart/form-data")
assert status == '200 OK'
# Second document
uri = f'/inventory/lot/{lot_id}/trade-document/add/'
file_upload = (BytesIO(b_file), file_name)
data['file'] = file_upload
data['csrf_token'] = generate_csrf()
body, status = user3.post(uri, data=data, content_type="multipart/form-data")
assert status == '200 OK'
@pytest.mark.mvp