Merge branch 'testing' into feature/#3396-add-columns-status

This commit is contained in:
Cayo Puigdefabregas 2022-05-16 10:16:23 +02:00
commit c1665ef4af
14 changed files with 335 additions and 94 deletions

View File

@ -133,6 +133,7 @@ class FilterForm(FlaskForm):
super().__init__(*args, **kwargs)
self.lots = lots
self.lot_id = lot_id
self.only_unassigned = kwargs.pop('only_unassigned', True)
self._get_types()
def _get_types(self):
@ -148,9 +149,9 @@ class FilterForm(FlaskForm):
device_ids = (d.id for d in self.lot.devices)
self.devices = Device.query.filter(Device.id.in_(device_ids))
else:
self.devices = Device.query.filter(Device.owner_id == g.user.id).filter_by(
lots=None
)
self.devices = Device.query.filter(Device.owner_id == g.user.id)
if self.only_unassigned:
self.devices = self.devices.filter_by(lots=None)
def search(self):
self.filter_from_lots()

View File

@ -1,5 +1,6 @@
import csv
import logging
from distutils.util import strtobool
from io import StringIO
import flask
@ -40,60 +41,64 @@ logger = logging.getLogger(__name__)
class DeviceListMix(GenericMixView):
template_name = 'inventory/device_list.html'
def get_context(self, lot_id):
def get_context(self, lot_id, only_unassigned=True):
super().get_context()
lots = self.context['lots']
form_filter = FilterForm(lots, lot_id)
form_filter = FilterForm(lots, lot_id, only_unassigned=only_unassigned)
devices = form_filter.search()
lot = None
tags = (
Tag.query.filter(Tag.owner_id == current_user.id)
.filter(Tag.device_id.is_(None))
.order_by(Tag.id.asc())
)
if lot_id:
lot = lots.filter(Lot.id == lot_id).one()
form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(lot=lot.id)
form_new_trade = TradeForm(
lot=lot.id,
user_to=g.user.email,
user_from=g.user.email,
)
else:
form_new_action = NewActionForm()
form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm()
form_new_trade = ''
action_devices = form_new_action.devices.data
list_devices = []
if action_devices:
list_devices.extend([int(x) for x in action_devices.split(",")])
form_new_action = NewActionForm(lot=lot_id)
self.context.update(
{
'devices': devices,
'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'form_new_allocate': AllocateForm(lot=lot_id),
'form_new_datawipe': DataWipeForm(lot=lot_id),
'form_new_trade': form_new_trade,
'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(),
'lot': lot,
'tags': tags,
'list_devices': list_devices,
'tags': self.get_user_tags(),
'list_devices': self.get_selected_devices(form_new_action),
'unassigned_devices': only_unassigned,
}
)
return self.context
def get_user_tags(self):
return (
Tag.query.filter(Tag.owner_id == current_user.id)
.filter(Tag.device_id.is_(None))
.order_by(Tag.id.asc())
)
def get_selected_devices(self, action_form):
"""Retrieve selected devices (when action form is submited)"""
action_devices = action_form.devices.data
if action_devices:
return [int(x) for x in action_devices.split(",")]
return []
class DeviceListView(DeviceListMix):
def dispatch_request(self, lot_id=None):
self.get_context(lot_id)
only_unassigned = request.args.get(
'only_unassigned', default=True, type=strtobool
)
self.get_context(lot_id, only_unassigned)
return flask.render_template(self.template_name, **self.context)

View File

@ -2,7 +2,7 @@
function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError(`attempted to ${ action } private static field before its declaration`); } }
function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError("attempted to " + action + " private static field before its declaration"); } }
function _classCheckPrivateStaticAccess(receiver, classConstructor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } }
@ -95,32 +95,56 @@ var _tableRowsPage = {
writable: true,
value: () => table.pages[table.rows().dt.currentPage - 1]
};
window.addEventListener("DOMContentLoaded", () => {
const selectorController = action => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function itemListCheckChanged() {
const listDevices = TableController.getAllDevicesInCurrentPage();
const isAllChecked = listDevices.map(itm => itm.checked);
function softInit() {
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
}); // https://github.com/fiduswriter/Simple-DataTables/wiki/Events
if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
alertInfoDevices.innerHTML = "Selected devices: ".concat(TableController.getSelectedDevices().length, "\n ").concat(TableController.getAllDevices().length != TableController.getSelectedDevices().length ? "<a href=\"#\" class=\"ml-3\">Select all devices (".concat(TableController.getAllDevices().length, ")</a>") : "<a href=\"#\" class=\"ml-3\">Cancel selection</a>");
alertInfoDevices.classList.remove("d-none");
} else if (isAllChecked.every(bool => bool == false)) {
btnSelectAll.checked = false;
btnSelectAll.indeterminate = false;
alertInfoDevices.classList.add("d-none");
} else {
btnSelectAll.indeterminate = true;
alertInfoDevices.classList.add("d-none");
}
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
}
if (action == "softInit") {
softInit();
itemListCheckChanged();
return;
}
function itemListCheckChanged() {
alertInfoDevices.innerHTML = "Selected devices: ".concat(TableController.getSelectedDevices().length, "\n ").concat(TableController.getAllDevices().length != TableController.getSelectedDevices().length ? "<a href=\"#\" class=\"ml-3\">Select all devices (".concat(TableController.getAllDevices().length, ")</a>") : "<a href=\"#\" class=\"ml-3\">Cancel selection</a>");
if (TableController.getSelectedDevices().length <= 0) {
alertInfoDevices.classList.add("d-none");
} else {
alertInfoDevices.classList.remove("d-none");
}
if (TableController.getAllDevices().length == TableController.getSelectedDevices().length) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
} else if (TableController.getAllSelectedDevicesInCurrentPage().length > 0) {
btnSelectAll.indeterminate = true;
} else {
btnSelectAll.checked = false;
btnSelectAll.indeterminate = false;
}
if (TableController.getAllDevices().length == 0) {
btnSelectAll.checked = false;
btnSelectAll.disabled = true;
} else {
btnSelectAll.disabled = false;
}
get_device_list();
}
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
});
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => {
@ -134,12 +158,12 @@ window.addEventListener("DOMContentLoaded", () => {
ckeckbox.checked = !checkState;
});
itemListCheckChanged();
}); // https://github.com/fiduswriter/Simple-DataTables/wiki/Events
});
softInit();
itemListCheckChanged();
};
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
});
window.addEventListener("DOMContentLoaded", () => selectorController());
function deviceSelect() {
const devices_count = TableController.getSelectedDevices().length;
@ -327,25 +351,44 @@ async function processSelectedDevices() {
const lotID = lot.id;
const srcElement = event.srcElement.parentElement.children[0];
const checked = !srcElement.checked;
const {
indeterminate
} = srcElement;
const found = this.list.filter(list => list.lot.id == lotID)[0];
if (checked) {
if (found && found.type == "Remove") {
found.type = "Add";
const affectedDevices = found.devices.filter(dev => found.lot.devices.includes(dev.id));
if (affectedDevices.length > 0 && found.indeterminate == false) {
// Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id);
} else {
found.type = "Add";
}
} else {
this.list.push({
type: "Add",
lot,
devices: selectedDevices
devices: selectedDevices,
indeterminate
});
}
} else if (found && found.type == "Add") {
found.type = "Remove";
const affectedDevices = found.devices.filter(dev => !found.lot.devices.includes(dev.id));
if (affectedDevices.length > 0 && found.indeterminate == false) {
// Remove action from list
actions.list = actions.list.filter(x => x.lot.id != found.lot.id);
} else {
found.type = "Remove";
}
} else {
this.list.push({
type: "Remove",
lot,
devices: selectedDevices
devices: selectedDevices,
indeterminate
});
}
@ -430,23 +473,20 @@ async function processSelectedDevices() {
const newRequest = await Api.doRequest(window.location);
const tmpDiv = document.createElement("div");
tmpDiv.innerHTML = newRequest;
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value); // https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
const rowsToRemove = [];
for (let i = 0; i < table.activeRows.length; i++) {
const row = table.activeRows[i];
if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
rowsToRemove.push(i);
}
}
table.rows().remove(rowsToRemove); // Restore state of checkbox
const newTable = document.createElement("table");
newTable.innerHTML = tmpDiv.querySelector("table").innerHTML;
newTable.classList = "table";
const oldTable = document.querySelector(".dataTable-wrapper");
oldTable.parentElement.replaceChild(newTable, oldTable);
table = new simpleDatatables.DataTable(newTable, {
perPage: 20
}); // // Restore state of checkbox
const selectAllBTN = document.getElementById("SelectAllBTN");
selectAllBTN.checked = false;
selectAllBTN.indeterminate = false;
selectAllBTN.indeterminate = false; // Re-init SelectorController
selectorController("softInit");
}
}

View File

@ -78,10 +78,28 @@ class TableController {
/**
* Select all functionality
*/
window.addEventListener("DOMContentLoaded", () => {
const selectorController = (action) => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function softInit() {
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
// https://github.com/fiduswriter/Simple-DataTables/wiki/Events
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
}
if (action == "softInit") {
softInit();
itemListCheckChanged();
return;
}
function itemListCheckChanged() {
alertInfoDevices.innerHTML = `Selected devices: ${TableController.getSelectedDevices().length}
${TableController.getAllDevices().length != TableController.getSelectedDevices().length
@ -114,11 +132,7 @@ window.addEventListener("DOMContentLoaded", () => {
get_device_list();
}
TableController.getAllDevices().forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => { ckeckbox.checked = checkedState });
@ -131,13 +145,12 @@ window.addEventListener("DOMContentLoaded", () => {
itemListCheckChanged()
})
// https://github.com/fiduswriter/Simple-DataTables/wiki/Events
table.on("datatable.page", () => itemListCheckChanged());
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
softInit();
itemListCheckChanged();
})
}
window.addEventListener("DOMContentLoaded", () => selectorController());
function deviceSelect() {
const devices_count = TableController.getSelectedDevices().length;
@ -425,22 +438,23 @@ async function processSelectedDevices() {
const tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
const newTable = document.createElement("table")
newTable.innerHTML = tmpDiv.querySelector("table").innerHTML
newTable.classList = "table"
// https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
const rowsToRemove = []
for (let i = 0; i < table.activeRows.length; i++) {
const row = table.activeRows[i];
if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
rowsToRemove.push(i)
}
}
table.rows().remove(rowsToRemove);
const oldTable = document.querySelector(".dataTable-wrapper")
oldTable.parentElement.replaceChild(newTable, oldTable)
// Restore state of checkbox
table = new simpleDatatables.DataTable(newTable, {perPage: 20})
// // Restore state of checkbox
const selectAllBTN = document.getElementById("SelectAllBTN");
selectAllBTN.checked = false;
selectAllBTN.indeterminate = false;
// Re-init SelectorController
selectorController("softInit");
}
}

View File

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

View File

@ -7,7 +7,11 @@
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
{% if not lot %}
<li class="breadcrumb-item active">Unassigned</li>
{% if unassigned_devices %}
<li class="breadcrumb-item active">Unassigned</li>
{% else %}
<li class="breadcrumb-item active">All devices</li>
{% endif %}
{% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
@ -359,6 +363,13 @@
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.verbose_name }}
</a>
{% if dev.lots | length > 0 %}
<h6 class="d-inline">
{% for lot in dev.lots %}
<span class="badge rounded-pill bg-light text-dark">{{ lot.name }}</span>
{% endfor %}
</h6>
{% endif %}
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
@ -434,7 +445,7 @@
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table", {
let table = new simpleDatatables.DataTable("table", {
perPage: 20
})
</script>

View File

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

View File

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

View File

View File

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

View File

@ -10,11 +10,13 @@ from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices
from ereuse_devicehub.labels.views import labels
from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench
app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
app.register_blueprint(core)
app.register_blueprint(devices)
app.register_blueprint(labels)
app.register_blueprint(workbench)
# configure & enable CSRF of Flask-WTF
# NOTE: enable by blueprint to exclude API views

View File

@ -24,6 +24,7 @@ from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.user.models import Session, User
from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench
STARTT = datetime(year=2000, month=1, day=1, hour=1)
"""A dummy starting time to use in tests."""
@ -58,6 +59,7 @@ def _app(config: TestConfig) -> Devicehub:
app.register_blueprint(core)
app.register_blueprint(devices)
app.register_blueprint(labels)
app.register_blueprint(workbench)
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
app.config['PROFILE'] = True
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])

View File

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

View File

@ -843,3 +843,26 @@ def test_action_datawipe(user3: UserClientFlask):
assert dev.actions[-1].type == 'DataWipe'
assert 'Action &#34;DataWipe&#34; created successfully!' in body
assert dev.devicehub_id in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_wb_settings(user3: UserClientFlask):
uri = '/workbench/settings/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Download your settings for Workbench" in body
assert "Workbench Settings" in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_wb_settings_register(user3: UserClientFlask):
uri = '/workbench/settings/?opt=register'
body, status = user3.get(uri)
assert status == '200 OK'
assert "TOKEN = " in body
assert "URL = https://" in body
assert "/api/inventory/" in body