Merge commit '7177b0fbfda37e495387834c7f2342b78f6559c8' into feature/confirm-trade-changes

This commit is contained in:
RubenPX 2022-05-06 08:26:37 +02:00
commit eb5c109220
15 changed files with 287 additions and 65 deletions

View File

@ -9,6 +9,12 @@ dags-with-materialized-paths-using-postgres-ltree/>`_ you have
a low-level technical implementation of how lots and their
relationships are mapped.
Getting lots
************
You can get lots list by ``GET /lots/``
There are one optional filter ``type``, only works with this 3 values ``temporary``, ``incoming`` and ``outgoing``
Create lots
***********
You create a lot by ``POST /lots/`` a `JSON Lot object <https://
@ -28,7 +34,6 @@ And for devices is all the same:
``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``;
idem for removing devices.
Sharing lots
************
Sharing a lot means giving certain permissions to users, like reading

View File

@ -593,30 +593,68 @@ class NewActionForm(ActionFormMix):
class AllocateForm(ActionFormMix):
start_time = DateField('Start time')
end_time = DateField('End time')
final_user_code = StringField('Final user code', [validators.length(max=50)])
transaction = StringField('Transaction', [validators.length(max=50)])
end_users = IntegerField('End users')
end_time = DateField('End time', [validators.Optional()])
final_user_code = StringField(
'Final user code', [validators.Optional(), validators.length(max=50)]
)
transaction = StringField(
'Transaction', [validators.Optional(), validators.length(max=50)]
)
end_users = IntegerField('End users', [validators.Optional()])
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not super().validate(extra_validators):
return False
if self.type.data not in ['Allocate', 'Deallocate']:
return False
if not self.validate_dates():
return False
if not self.check_devices():
return False
return True
def validate_dates(self):
start_time = self.start_time.data
end_time = self.end_time.data
if not start_time:
self.start_time.errors = ['Not a valid date value.!']
return False
if start_time and end_time and end_time < start_time:
error = ['The action cannot finish before it starts.']
self.start_time.errors = error
self.end_time.errors = error
is_valid = False
return False
if not self.end_users.data:
self.end_users.errors = ["You need to specify a number of users"]
is_valid = False
if not end_time:
self.end_time.data = self.start_time.data
return is_valid
return True
def check_devices(self):
if self.type.data == 'Allocate':
txt = "You need deallocate before allocate this device again"
for device in self._devices:
if device.allocated:
self.devices.errors = [txt]
return False
device.allocated = True
if self.type.data == 'Deallocate':
txt = "Sorry some of this devices are actually deallocate"
for device in self._devices:
if not device.allocated:
self.devices.errors = [txt]
return False
device.allocated = False
return True
class DataWipeDocumentForm(Form):

View File

@ -421,7 +421,6 @@ class ExportsView(View):
'metrics': self.metrics,
'devices': self.devices_list,
'certificates': self.erasure,
'links': self.public_links,
}
if export_id not in export_ids:
@ -497,19 +496,6 @@ class ExportsView(View):
return self.response_csv(data, "actions_export.csv")
def public_links(self):
# get a csv with the publink links of this devices
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
cw.writerow(['links'])
host_url = request.host_url
for dev in self.find_devices():
code = dev.devicehub_id
link = [f"{host_url}devices/{code}"]
cw.writerow(link)
return self.response_csv(data, "links.csv")
def erasure(self):
template = self.build_erasure_certificate()
res = flask_weasyprint.render_pdf(

View File

@ -10,7 +10,7 @@ from typing import Dict, List, Set
from boltons import urlutils
from citext import CIText
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
from flask import g
from flask import g, request
from flask_sqlalchemy import event
from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column
@ -297,6 +297,11 @@ class Device(Thing):
actions.reverse()
return actions
@property
def public_link(self) -> str:
host_url = request.host_url.strip('/')
return "{}{}".format(host_url, self.url.to_text())
@property
def url(self) -> urlutils.URL:
"""The URL where to GET this device."""

View File

@ -29,6 +29,7 @@ class LotView(View):
"""
format = EnumField(LotFormat, missing=None)
search = f.Str(missing=None)
type = f.Str(missing=None)
def post(self):
l = request.get_json()
@ -88,6 +89,7 @@ class LotView(View):
else:
query = Lot.query
query = self.visibility_filter(query)
query = self.type_filter(query, args)
if args['search']:
query = query.filter(Lot.name.ilike(args['search'] + '%'))
lots = query.paginate(per_page=6 if args['search'] else query.count())
@ -104,6 +106,21 @@ class LotView(View):
Lot.owner_id == g.user.id))
return query
def type_filter(self, query, args):
lot_type = args.get('type')
# temporary
if lot_type == "temporary":
return query.filter(Lot.trade == None)
if lot_type == "incoming":
return query.filter(Lot.trade and Trade.user_to == g.user)
if lot_type == "outgoing":
return query.filter(Lot.trade and Trade.user_from == g.user)
return query
def query(self, args):
query = Lot.query.distinct()
return query

View File

@ -19,12 +19,12 @@ body {
}
a {
color: #4154f1;
color: #6c757d ;
text-decoration: none;
}
a:hover {
color: #717ff5;
color: #cc0066;
text-decoration: none;
}
@ -56,7 +56,7 @@ h1, h2, h3, h4, h5, h6 {
font-size: 24px;
margin-bottom: 0;
font-weight: 600;
color: #012970;
color: #993365;
}
/*--------------------------------------------------------------
@ -176,6 +176,31 @@ h1, h2, h3, h4, h5, h6 {
opacity: 0;
}
}
.btn-primary {
background-color: #993365;
border-color: #993365;
color: #fff;
}
.btn-primary:hover, .btn-primary:focus {
background-color: #cc0066;
border-color: #cc0066;
color: #fff;
}
.btn-danger {
background-color: #b3b1b1;
border-color: #b3b1b1;
color: #fff;
}
.btn-danger:hover {
background-color: #645e5f;
border-color: #645e5f;
color: #fff;
}
/* Light Backgrounds */
.bg-primary-light {
background-color: #cfe2ff;
@ -326,12 +351,12 @@ h1, h2, h3, h4, h5, h6 {
color: #2c384e;
}
.nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus {
color: #4154f1;
color: #993365;
}
.nav-tabs-bordered .nav-link.active {
background-color: #fff;
color: #4154f1;
border-bottom: 2px solid #4154f1;
color: #993365;
border-bottom: 2px solid #993365;
}
/*--------------------------------------------------------------
@ -370,7 +395,7 @@ h1, h2, h3, h4, h5, h6 {
font-size: 32px;
padding-left: 10px;
cursor: pointer;
color: #012970;
color: #993365;
}
.header .search-bar {
min-width: 360px;
@ -439,7 +464,7 @@ h1, h2, h3, h4, h5, h6 {
color: #012970;
}
.header-nav .nav-profile {
color: #012970;
color: #993365;
}
.header-nav .nav-profile img {
max-height: 36px;
@ -606,7 +631,7 @@ h1, h2, h3, h4, h5, h6 {
align-items: center;
font-size: 15px;
font-weight: 600;
color: #4154f1;
color: #993365;
transition: 0.3;
background: #f6f9ff;
padding: 10px 15px;
@ -615,21 +640,21 @@ h1, h2, h3, h4, h5, h6 {
.sidebar-nav .nav-link i {
font-size: 16px;
margin-right: 10px;
color: #4154f1;
color: #993365;
}
.sidebar-nav .nav-link.collapsed {
color: #012970;
color: #6c757d;
background: #fff;
}
.sidebar-nav .nav-link.collapsed i {
color: #899bbd;
}
.sidebar-nav .nav-link:hover {
color: #4154f1;
color: #993365;
background: #f6f9ff;
}
.sidebar-nav .nav-link:hover i {
color: #4154f1;
color: #993365;
}
.sidebar-nav .nav-link .bi-chevron-down {
margin-right: 0;
@ -660,7 +685,7 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 50%;
}
.sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active {
color: #4154f1;
color: #993365;
}
.sidebar-nav .nav-content a.active i {
background-color: #4154f1;
@ -1003,7 +1028,7 @@ h1, h2, h3, h4, h5, h6 {
padding: 12px 15px;
}
.contact .php-email-form button[type=submit] {
background: #4154f1;
background: #993365;
border: 0;
padding: 10px 30px;
color: #fff;
@ -1011,7 +1036,15 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 4px;
}
.contact .php-email-form button[type=submit]:hover {
background: #5969f3;
background: #993365;
}
button[type=submit] {
background-color: #993365;
border-color: #993365;
}
button[type=submit]:hover {
background-color: #993365;
border-color: #993365;
}
@-webkit-keyframes animate-loading {
0% {

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="188.02727mm"
height="48.976315mm"
viewBox="0 0 188.02727 48.976315"
version="1.1"
id="svg20276"
sodipodi:docname="logo_usody_clock.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview20278"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.82671156"
inkscape:cx="332.03842"
inkscape:cy="-66.528645"
inkscape:window-width="1680"
inkscape:window-height="1013"
inkscape:window-x="1280"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs20273" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-82.275303,-65.555746)">
<text
xml:space="preserve"
style="font-size:50.8px;line-height:1.25;font-family:sans-serif;stroke-width:0.264583"
x="126.75784"
y="103.84124"
id="text1904"><tspan
sodipodi:role="line"
id="tspan1902"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
x="126.75784"
y="103.84124">Usod<tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#993265;fill-opacity:1"
id="tspan8044">y</tspan></tspan></text>
<g
id="g14774-8"
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1"
transform="matrix(0.083748,0,0,0.08393574,81.384743,64.67498)">
<g
id="g14772-6"
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1">
<path
d="M 499.5,230.1 C 485.2,95.8 364.4,-2 230,12.4 157,20.2 92,59.8 51.5,121.1 c -6.2,9.4 -3.6,22.1 5.8,28.3 9.4,6.2 22.1,3.6 28.3,-5.8 33.8,-51.1 88,-84.1 148.8,-90.5 111.9,-11.9 212.6,69.4 224.5,181.3 5.8,54.2 -9.9,107.4 -44.2,149.9 -34.3,42.4 -83,69 -137.2,74.7 -74,8 -146.6,-25.6 -188.9,-86 l 37.7,8.1 c 11.1,2.3 21.9,-4.6 24.3,-15.7 2.4,-11 -4.6,-21.9 -15.7,-24.3 L 53.4,323.6 c -11,-2.4 -21.9,4.6 -24.3,15.7 l -17.5,81.5 c -2.4,11 4.7,21.7 15.7,24.3 12.4,2.9 22.2,-6.1 24.3,-15.7 L 57.9,400 c 46.1,63.3 120.2,101 198.3,101 8.5,0 17.1,-0.4 25.7,-1.4 65.1,-6.9 123.6,-38.8 164.7,-89.7 41,-50.8 59.8,-114.7 52.9,-179.8 z"
id="path14768-0"
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1" />
<path
d="m 271.8,140.3 c -11.3,0 -20.4,9.1 -20.4,20.4 V 256 c 0,5.4 2.2,10.6 6,14.4 l 95.3,95.3 c 9.5,10.7 24.9,4 28.9,0 8,-8 8,-20.9 0,-28.9 l -89.3,-89.3 v -86.8 c -0.1,-11.2 -9.2,-20.4 -20.5,-20.4 z"
id="path14770-4"
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1" />
</g>
</g>
<g
id="g14772"
transform="matrix(0.083748,0,0,0.08393574,81.384743,64.67498)">
<path
id="path14768"
d="M 255.01758,10.996094 C 246.74651,11.038013 238.4,11.500391 230,12.400391 c -73,7.8 -138,47.399218 -178.5,108.699219 -6.2,9.4 -3.599219,22.10078 5.800781,28.30078 9.4,6.2 22.098828,3.59922 28.298828,-5.80078 33.800001,-51.100001 88.000781,-84.100001 148.800781,-90.500001 111.9,-11.9 212.6,69.400781 224.5,181.300781 5.8,54.2 -9.90117,107.40039 -44.20117,149.90039 C 380.39922,426.70078 331.7,453.3 277.5,459 c -8.97135,0.96988 -17.92013,1.30083 -26.79883,1.07422 l 0.0449,40.76172 c 1.81975,0.041 3.62871,0.16406 5.45313,0.16406 8.5,0 17.10117,-0.40039 25.70117,-1.40039 65.1,-6.9 123.59922,-38.79922 164.69922,-89.69922 41,-50.8 59.80039,-114.70078 52.90039,-179.80078 C 486.09375,104.19336 379.08362,10.36731 255.01758,10.996094 Z" />
<path
d="m 271.8,140.3 c -11.3,0 -20.4,9.1 -20.4,20.4 V 256 c 0,5.4 2.2,10.6 6,14.4 l 95.3,95.3 c 9.5,10.7 24.9,4 28.9,0 8,-8 8,-20.9 0,-28.9 l -89.3,-89.3 v -86.8 c -0.1,-11.2 -9.2,-20.4 -20.5,-20.4 z"
id="path14770" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -4,7 +4,7 @@ const Api = {
* @returns get lots
*/
async get_lots() {
const request = await this.doRequest(API_URLS.lots, "GET", null);
const request = await this.doRequest(`${API_URLS.lots}?type=temporary`, "GET", null);
if (request != undefined) return request.items;
throw request;
},

View File

@ -80,6 +80,7 @@ class TableController {
*/
window.addEventListener("DOMContentLoaded", () => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function itemListCheckChanged() {
const listDevices = TableController.getAllDevicesInCurrentPage()
@ -88,11 +89,20 @@ window.addEventListener("DOMContentLoaded", () => {
if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
alertInfoDevices.innerHTML = `Selected devices: ${TableController.getSelectedDevices().length}
${
TableController.getAllDevices().length != TableController.getSelectedDevices().length
? `<a href="#" class="ml-3">Select all devices (${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")
}
}
@ -103,6 +113,13 @@ window.addEventListener("DOMContentLoaded", () => {
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => { ckeckbox.checked = checkedState });
itemListCheckChanged()
})
alertInfoDevices.addEventListener("click", () => {
const checkState = TableController.getAllDevices().length == TableController.getSelectedDevices().length
TableController.getAllDevices().forEach(ckeckbox => { ckeckbox.checked = !checkState });
itemListCheckChanged()
})
// https://github.com/fiduswriter/Simple-DataTables/wiki/Events

View File

@ -6,7 +6,7 @@
<div class="d-flex align-items-center justify-content-between">
<a href="{{ url_for('inventory.devicelist')}}" class="logo d-flex align-items-center">
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
<img src="{{ url_for('static', filename='img/logo_usody_clock.png') }}" alt="">
</a>
<i class="bi bi-list toggle-sidebar-btn"></i>
</div><!-- End Logo -->

View File

@ -13,7 +13,7 @@
<div class="d-flex justify-content-center py-4">
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
<img src="{{ url_for('static', filename='img/logo_usody_clock.png') }}" alt="">
</a>
</div><!-- End Logo -->

View File

@ -26,6 +26,14 @@
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#type">Type</button>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ device.public_link }}" target="_blank">Web</a>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#status">Status</button>
</li>
@ -69,6 +77,50 @@
</div>
</div>
<div class="tab-pane fade profile-overview" id="lots">
<h5 class="card-title">Incoming Lots</h5>
<div class="row">
{% for lot in device.lots %}
{% if lot.is_incoming %}
<div class="col">
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<span>{{ lot.name }}</span>
</a>
</div>
{% endif %}
{% endfor %}
</div>
<h5 class="card-title">Outgoing Lots</h5>
<div class="row">
{% for lot in device.lots %}
{% if lot.is_outgoing %}
<div class="col">
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<span>{{ lot.name }}</span>
</a>
</div>
{% endif %}
{% endfor %}
</div>
<h5 class="card-title">Temporary Lots</h5>
<div class="row">
{% for lot in device.lots %}
{% if lot.is_temporary %}
<div class="col">
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<span>{{ lot.name }}</span>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5>
<div class="row">

View File

@ -201,12 +201,6 @@
Metrics Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('links')" class="dropdown-item">
<i class="bi bi-link-45deg"></i>
Public Links
</a>
</li>
<li>
<a href="javascript:export_file('certificates')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
@ -314,6 +308,10 @@
</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">
<form method="get">
<div class="d-flex mt-4 mb-4">

View File

@ -243,18 +243,6 @@ def test_export_metrics(user3: UserClientFlask):
assert body == ''
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_export_links(user3: UserClientFlask):
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
uri = "/inventory/export/links/?ids={id}".format(id=snap.device.devicehub_id)
body, status = user3.get(uri)
assert status == '200 OK'
body = body.split("\n")
assert ['links', 'http://localhost/devices/O48N2', ''] == body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_export_certificates(user3: UserClientFlask):
@ -668,7 +656,7 @@ def test_action_allocate_error_required(user3: UserClientFlask):
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Action Allocate error' in body
assert 'You need to specify a number of users!' in body
assert 'Not a valid date value.' in body
@pytest.mark.mvp