Merge pull request #408 from eReuse/feature/3988-real-pagination

Feature/3988 real pagination
This commit is contained in:
cayop 2022-11-24 13:55:42 +01:00 committed by GitHub
commit 4ae9eeb4c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 841 additions and 178 deletions

View File

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

View File

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

View File

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

View File

@ -118,16 +118,36 @@ class ErasureListView(DeviceListMixin):
def dispatch_request(self, orphans=0): def dispatch_request(self, orphans=0):
self.get_context() self.get_context()
self.get_devices(orphans) self.get_devices(orphans)
if orphans:
self.context['orphans'] = True
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
def get_devices(self, orphans): def get_devices(self, orphans):
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 5))
erasure = EraseBasic.query.filter_by(author=g.user).order_by( erasure = EraseBasic.query.filter_by(author=g.user).order_by(
EraseBasic.created.desc() EraseBasic.created.desc()
) )
if orphans: if orphans:
erasure = [e for e in erasure if e.device.orphan] schema = app.config.get('SCHEMA')
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
"""
ids = (e[0] for e in db.session.execute(sql))
erasure = EraseBasic.query.filter(EraseBasic.id.in_(ids)).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 self.context['erasure'] = erasure
@ -138,8 +158,17 @@ class DeviceListView(DeviceListMixin):
class AllDeviceListView(DeviceListMixin): class AllDeviceListView(DeviceListMixin):
template_name = 'inventory/all_device_list.html'
def dispatch_request(self): def dispatch_request(self):
self.get_context(all_devices=True) self.get_context(all_devices=True)
# import pdb; pdb.set_trace()
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 5))
devices = self.context['devices'].paginate(page=page, per_page=per_page)
devices.first = per_page * devices.page - per_page + 1
devices.last = len(devices.items) + devices.first - 1
self.context['devices'] = devices
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)

View File

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

View File

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

View File

@ -0,0 +1,671 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.alldevicelist')}}">Inventory</a></li>
{% if not lot %}
<li class="breadcrumb-item active">All devices</li>
{% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% elif lot.is_incoming %}
<li class="breadcrumb-item active">Incoming Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% elif lot.is_outgoing %}
<li class="breadcrumb-item active">Outgoing Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% endif %}
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
<div class="card">
{% if lot %}
<div class="card-body pt-3">
<!-- Bordered Tabs -->
<div class="d-flex align-items-center justify-content-between row">
<div class="col-sm-12 col-md-5">
<h3>
<a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a>
</h3>
</div>
<div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions -->
{% if lot.is_temporary or not lot.transfer.closed %}
{% if lot and lot.is_temporary %}
<a type="button" href="{{ url_for('inventory.lot_new_transfer', lot_id=lot.id, type_id='outgoing') }}" class="btn btn-primary doTransfer" >
Create Outgoing Lot
</a>
<a type="button" href="{{ url_for('inventory.lot_new_transfer', lot_id=lot.id, type_id='incoming') }}" class="btn btn-primary doTransfer">
Create Incoming Lot
</a>
{% endif %}
<a class="text-danger" href="javascript:removeLot()">
<i class="bi bi-trash"></i> Delete Lot
</a>
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="card-body pt-3" style="min-height: 650px;">
<!-- Bordered Tabs -->
{% if lot %}
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#devices-list">Devices</button>
</li>
{% if lot and not lot.is_temporary %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button>
</li>
{% if lot.transfer %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-transfer">
Transfer ({% if lot.transfer.closed %}<span class="text-danger">Closed</span>{% else %}<span class="text-success">Open</span>{% endif %})
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-delivery-note">
Delivery Note
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-receiver-note">
Receiver Note
</button>
</li>
{% endif %}
{% endif %}
</ul>
{% endif %}
<div class="tab-content pt-1">
<div id="devices-list" class="tab-pane fade devices-list active show">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
<div class="btn-group dropdown ml-1">
<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>
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus"></i>
Actions
</button>
<span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span>
<span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span>
<span class="d-none" id="activeDatawipeModal" data-bs-toggle="modal" data-bs-target="#datawipeModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnActions">
<li>
Status actions
</li>
<li>
<a href="javascript:newAction('Recycling')" class="dropdown-item">
<i class="bi bi-recycle"></i>
Recycling
</a>
</li>
<li>
<a href="javascript:newAction('Use')" class="dropdown-item">
<i class="bi bi-play-circle-fill"></i>
Use
</a>
</li>
<li>
<a href="javascript:newAction('Refurbish')" class="dropdown-item">
<i class="bi bi-tools"></i>
Refurbish
</a>
</li>
<li>
<a href="javascript:newAction('Management')" class="dropdown-item">
<i class="bi bi-mastodon"></i>
Management
</a>
</li>
<li>
Allocation
</li>
<li>
<a href="javascript:newAllocate('Allocate')" class="dropdown-item">
<i class="bi bi-house-fill"></i>
Allocate
</a>
</li>
<li>
<a href="javascript:newAllocate('Deallocate')" class="dropdown-item">
<i class="bi bi-house"></i>
Deallocate
</a>
</li>
<li>
Physical actions
</li>
<li>
<a href="javascript:newAction('ToPrepare')" class="dropdown-item">
<i class="bi bi-tools"></i>
ToPrepare
</a>
</li>
<li>
<a href="javascript:newAction('Prepare')" class="dropdown-item">
<i class="bi bi-egg"></i>
Prepare
</a>
</li>
<li>
<a href="javascript:newDataWipe('DataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
DataWipe
</a>
</li>
<li>
<a href="javascript:newAction('ToRepair')" class="dropdown-item">
<i class="bi bi-screwdriver"></i>
ToRepair
</a>
</li>
<li>
<a href="javascript:newAction('Ready')" class="dropdown-item">
<i class="bi bi-check2-all"></i>
Ready
</a>
</li>
</ul>
</div>
<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>
Exports
</button>
<span class="d-none" id="exportAlertModal" data-bs-toggle="modal" data-bs-target="#exportErrorModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnExport">
<li>
<a href="javascript:export_file('devices')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Devices Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('devices_lots')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Devices Lots Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('obada_standard')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Obada Standard Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('certificates')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
Erasure Certificate
</a>
</li>
</ul>
</div>
<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>
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-laptop"></i>
Placeholders
</button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_upload_placeholder', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.upload_placeholder') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-upload"></i>
Upload Spreadsheet
</a>
</li>
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_device_add', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.device_add') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-plus"></i>
Create a new
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-laptop"></i>
Snapshots
</button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_upload_snapshot', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-upload"></i>
Upload files
</a>
</li>
</ul>
</div>
<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">
<form method="get">
<div class="d-flex mt-4 mb-4">
{% for f in form_filter %}
{{ f }}
{% endfor %}
<input type="submit" class="ms-2 btn btn-primary" value="Filter" />
</div>
</form>
<p class="mt-3">
Displaying devices of type
<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>
<th scope="col">Select</th>
<th scope="col">Title</th>
<th scope="col">DHID</th>
<th scope="col">PHID</th>
<th scope="col">Type</th>
<th scope="col">Unique Identifiers</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="YYYY-MM-DD">Updated in</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm:ss">Registered in</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for dev in devices.items %}
{% if dev.placeholder and (not dev.parent_id or dev.parent.placeholder.kangaroo) %}
<tr>
<td>
<input type="checkbox" class="deviceSelect" data="{{ dev.id }}"
data-device-type="{{ dev.type }}" data-device-manufacturer="{{ dev.manufacturer }}"
data-device-dhid="{{ dev.devicehub_id }}" data-device-vname="{{ dev.verbose_name }}"
{% if form_new_allocate.type.data and dev.id in list_devices %}
checked="checked"
{% endif %}
/>
</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 %}
{{ dev.verbose_name }}
</a>
{% if dev.lots | length > 0 %}
<h6 class="d-inline">
{% for lot in dev.get_lots_for_template() %}
<span class="badge rounded-pill bg-light text-dark">{{ lot }}</span>
{% endfor %}
</h6>
{% endif %}
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.devicehub_id }}
</a>
</td>
<td>
{{ dev.binding and dev.binding.phid or dev.placeholder and dev.placeholder.phid or '' }}
</td>
<td>
{{ dev.is_abstract() }}
</td>
<td>
{% for t in dev.tags | sort(attribute="id") %}
<a href="{{ url_for('labels.label_details', id=t.id)}}">{{ t.id }}</a>
{% if not loop.last %},{% endif %}
{% 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.get_updated.strftime('%Y-%m-%d %H:%M:%S')}}</td>
<td>{{ dev.created.strftime('%Y-%m-%d %H:%M:%S')}}</td>
<td>
<a href="{{ dev.public_link }}" target="_blank">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</td>
</tr>
{% endif %}
{% 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">
<a href="{{ url_for('inventory.alldevicelist', page=devices.prev_num, per_page=devices.per_page) }}"></a>
</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="">
<a href="{{ url_for('inventory.alldevicelist', page=page, per_page=devices.per_page) }}">
{{ page }}
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if devices.has_next %}
<li class="pager">
<a href="{{ url_for('inventory.alldevicelist', page=devices.next_num, per_page=devices.per_page) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
</div>
{% if lot and not lot.is_temporary %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{{ url_for('inventory.trade_document_add', lot_id=lot.id)}}" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th>
</tr>
</thead>
<tbody>
{% for doc in lot.documents %}
<tr>
<td>
{% if doc.get_url() %}
<a href="{{ doc.get_url() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%Y-%m-%d %H:%M')}}
</td>
</tr>
{% endfor %}
{% for doc in lot.trade.documents %}
<tr>
<td>
{% if doc.get_url() %}
<a href="{{ doc.get_url() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%Y-%m-%d %H:%M')}}
</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>
</div>
</div>
<div id="NotificationsContainer" style="position: absolute; bottom: 0; right: 0; margin: 10px; margin-top: 70px; width: calc(100% - 310px);"></div>
</div>
</div>
</section>
{% 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();
window.location.href = "{{ url_for('inventory.alldevicelist', page=1) }}&per_page="+per_page;
});
});
</script>
<script>
let table = new simpleDatatables.DataTable("table", {
footer: false,
paging: false,
})
</script>
{% if config['DEBUG'] %}
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% else %}
<script src="{{ url_for('static', filename='js/main_inventory.build.js') }}"></script>
{% endif %}
{% endblock main %}

View File

@ -7,11 +7,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li> <li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
{% if not lot %} {% if not lot %}
{% if all_devices %} <li class="breadcrumb-item active">Unassigned</li>
<li class="breadcrumb-item active">All devices</li>
{% else %}
<li class="breadcrumb-item active">Unassigned</li>
{% endif %}
{% elif lot.is_temporary %} {% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li> <li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li> <li class="breadcrumb-item active">{{ lot.name }}</li>
@ -230,17 +226,10 @@
</a> </a>
</li> </li>
<li> <li>
{% if not all_devices %}
<span class="dropdown-item" style="color: #999ea4;"> <span class="dropdown-item" style="color: #999ea4;">
<i class="bi bi-file-spreadsheet"></i> <i class="bi bi-file-spreadsheet"></i>
Devices Lots Spreadsheet Devices Lots Spreadsheet
</span> </span>
{% else %}
<a href="javascript:export_file('devices_lots')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Devices Lots Spreadsheet
</a>
{% endif %}
</li> </li>
<li> <li>
<a href="javascript:export_file('obada_standard')" class="dropdown-item"> <a href="javascript:export_file('obada_standard')" class="dropdown-item">

View File

@ -109,7 +109,43 @@
</div> </div>
{% endif %} {% 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="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"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -125,7 +161,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for ac in erasure %} {% for ac in erasure.items %}
<tr> <tr>
<td> <td>
<input type="checkbox" class="deviceSelect" data="{{ ac.device.my_partner.id }}" <input type="checkbox" class="deviceSelect" data="{{ ac.device.my_partner.id }}"
@ -170,7 +206,7 @@
</td> </td>
<td> <td>
<a href="{{ url_for('inventory.export', export_id='snapshot') }}?id={{ ac.snapshot.uuid }}"> <a href="{{ url_for('inventory.export', export_id='snapshot') }}?id={{ ac.snapshot.uuid }}">
{{ ac.snapshot.uuid }} {{ ac.snapshot.uuid }}
</a> </a>
</td> </td>
<td> <td>
@ -194,135 +230,54 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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>
</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><!-- End Bordered Tabs -->
</div> </div>
@ -334,21 +289,32 @@
</div> </div>
</div> </div>
</section> </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_export_error.html" %}
{% include "inventory/alert_lots_changes.html" %} {% include "inventory/alert_lots_changes.html" %}
<!-- Custom Code --> <!-- 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> <script>
let table = new simpleDatatables.DataTable("table", { let table = new simpleDatatables.DataTable("table", {
perPageSelect: [5, 10, 15, 20, 25, 50, 100], //perPageSelect: [5, 10, 15, 20, 25, 50, 100],
perPage: 20 //perPage: 20,
footer: false,
paging: false,
}) })
</script> </script>
{% if config['DEBUG'] %} {% if config['DEBUG'] %}
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script> <script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% else %} {% else %}

View File

@ -2368,6 +2368,9 @@ def test_upload_snapshot_smartphone(user3: UserClientFlask):
@pytest.mark.mvp @pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_list_erasures(user3: UserClientFlask): def test_list_erasures(user3: UserClientFlask):
from flask import current_app as app
app.config['SCHEMA'] = 'test'
uri = '/inventory/upload-snapshot/' uri = '/inventory/upload-snapshot/'
file_name = 'erase-sectors-2-hdd.snapshot.yaml' file_name = 'erase-sectors-2-hdd.snapshot.yaml'
body, status = user3.get(uri) body, status = user3.get(uri)