diff --git a/docs/lots.rst b/docs/lots.rst index e3cc83a3..03943e21 100644 --- a/docs/lots.rst +++ b/docs/lots.rst @@ -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 /devices/?id=&id=``; idem for removing devices. - Sharing lots ************ Sharing a lot means giving certain permissions to users, like reading diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 08b6eeca..d58fcaf9 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -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): diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 503632e6..2df8fd69 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -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( diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 09451277..c08ec3b8 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -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.""" diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index dce6af62..b99e622f 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -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 diff --git a/ereuse_devicehub/static/css/style.css b/ereuse_devicehub/static/css/style.css index 1fd44f19..1766a804 100644 --- a/ereuse_devicehub/static/css/style.css +++ b/ereuse_devicehub/static/css/style.css @@ -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% { diff --git a/ereuse_devicehub/static/img/logo_usody_clock.png b/ereuse_devicehub/static/img/logo_usody_clock.png new file mode 100644 index 00000000..30344765 Binary files /dev/null and b/ereuse_devicehub/static/img/logo_usody_clock.png differ diff --git a/ereuse_devicehub/static/img/logo_usody_clock.svg b/ereuse_devicehub/static/img/logo_usody_clock.svg new file mode 100644 index 00000000..4d03247e --- /dev/null +++ b/ereuse_devicehub/static/img/logo_usody_clock.svg @@ -0,0 +1,83 @@ + + + + + + + + Usody + + + + + + + + + + + + diff --git a/ereuse_devicehub/static/js/api.js b/ereuse_devicehub/static/js/api.js index 190e5d42..552544d2 100644 --- a/ereuse_devicehub/static/js/api.js +++ b/ereuse_devicehub/static/js/api.js @@ -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; }, diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index cc6aaca4..49e16b3b 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -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 + ? `Select all devices (${TableController.getAllDevices().length})` + : "Cancel selection" + }`; + 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 diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index f10172e5..fe014107 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -6,7 +6,7 @@ diff --git a/ereuse_devicehub/templates/ereuse_devicehub/user_login.html b/ereuse_devicehub/templates/ereuse_devicehub/user_login.html index b234927c..f010e3d7 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/user_login.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/user_login.html @@ -13,7 +13,7 @@ diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index 7a96c4a9..f18c1e0e 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -26,6 +26,14 @@ + + + + @@ -69,6 +77,50 @@ +
+
Incoming Lots
+ +
+ {% for lot in device.lots %} + {% if lot.is_incoming %} + + {% endif %} + {% endfor %} +
+ +
Outgoing Lots
+ +
+ {% for lot in device.lots %} + {% if lot.is_outgoing %} + + {% endif %} + {% endfor %} +
+ +
Temporary Lots
+ +
+ {% for lot in device.lots %} + {% if lot.is_temporary %} + + {% endif %} + {% endfor %} +
+
+
Status Details
diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 3a52529a..968312a5 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -201,12 +201,6 @@ Metrics Spreadsheet -
  • - - - Public Links - -
  • @@ -314,6 +308,10 @@
  • {% endif %} + +
    diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index 0df56652..ac41ef09 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -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